package engine import ( "encoding" "math" "reflect" "regexp" "strconv" "time" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) var integerPattern = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`) var unsignedIntegerPattern = regexp.MustCompile(`^(0|[1-9][0-9]*)$`) type decodeIssue struct { code Code path Path message string location document.Location } type inputPresence struct { location document.Location } type decodedConfiguration struct { value reflect.Value presence map[string]inputPresence origins map[string]Origin baseline *document.Node final *document.Node } type decodeContext struct { versionPath *Path } func decodeConfiguration(schema *schemaDescriptor, defaults reflect.Value, input *document.Node) (decodedConfiguration, []decodeIssue) { return decodeConfigurationWithContext(schema, defaults, input, decodeContext{}) } func decodeConfigurationWithContext(schema *schemaDescriptor, defaults reflect.Value, input *document.Node, context decodeContext) (decodedConfiguration, []decodeIssue) { if schema == nil || schema.root == nil || !defaults.IsValid() || defaults.Type() != schema.root.typeOf { return decodedConfiguration{}, []decodeIssue{{code: CodeSchemaUnsupportedType, message: "configuration defaults do not match the schema"}} } baseline, projectionIssue := projectConfiguration(schema, defaults, defaultProjectionLimits()) if projectionIssue != nil { return decodedConfiguration{}, []decodeIssue{decodeIssueFromProjection(projectionIssue)} } working, cloneIssue := cloneConfiguration(schema, defaults, defaultProjectionLimits()) if cloneIssue != nil { return decodedConfiguration{}, []decodeIssue{decodeIssueFromProjection(cloneIssue)} } presence := make(map[string]inputPresence) if input != nil { collectPresence(input, Path{}, presence) decoded, issues := decodeValue(input, working, schema.root, Path{}, schema, context) if len(issues) != 0 { return decodedConfiguration{}, issues } working = decoded } final, projectionIssue := projectConfiguration(schema, working, defaultProjectionLimits()) if projectionIssue != nil { return decodedConfiguration{}, []decodeIssue{decodeIssueFromProjection(projectionIssue)} } origins := make(map[string]Origin) collectOrigins(final, Path{}, presence, origins) return decodedConfiguration{ value: working, presence: presence, origins: origins, baseline: baseline, final: final, }, nil } func decodeValue(node *document.Node, base reflect.Value, descriptor *typeDescriptor, path Path, schema *schemaDescriptor, context decodeContext) (reflect.Value, []decodeIssue) { if node.Kind == document.Null { switch descriptor.kind { case descriptorPointer, descriptorMap, descriptorSlice, descriptorDynamic: return reflect.Zero(descriptor.typeOf), nil case descriptorSecret: return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeSecretRequired, path, node, "required secret reference cannot be null")} default: return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeNull, path, node, "null is not allowed for this configuration type")} } } if descriptor.kind == descriptorPointer { terminal, ok := terminalDescriptor(descriptor) if !ok { return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeType, path, node, "configuration type cannot represent a non-null value")} } var pointers []*typeDescriptor currentBase := base for current := descriptor; current != terminal; current = current.element { pointers = append(pointers, current) if currentBase.IsNil() { currentBase = reflect.Zero(current.typeOf.Elem()) } else { currentBase = currentBase.Elem() } } decoded, issues := decodeValue(node, currentBase, terminal, path, schema, context) if len(issues) != 0 { return reflect.Value{}, issues } for i := len(pointers) - 1; i >= 0; i-- { result := reflect.New(pointers[i].typeOf.Elem()) result.Elem().Set(decoded) decoded = result } return decoded, nil } switch descriptor.kind { case descriptorStruct: return decodeStruct(node, base, descriptor, path, schema, context) case descriptorMap: return decodeMap(node, base, descriptor, path, schema, context) case descriptorSlice: return decodeSlice(node, descriptor, path, schema, context) case descriptorArray: return decodeArray(node, descriptor, path, schema, context) case descriptorDynamic: return decodeDynamic(node, base, path, schema, context) case descriptorString: if node.Kind != document.String && !(pathMatches(context.versionPath, path) && isIntegerVersion(node)) { return typeMismatch(path, node, "string") } result := reflect.New(descriptor.typeOf).Elem() result.SetString(node.Text) return result, nil case descriptorBoolean: if node.Kind != document.Boolean { return typeMismatch(path, node, "boolean") } result := reflect.New(descriptor.typeOf).Elem() result.SetBool(node.Text == "true") return result, nil case descriptorSigned: if node.Kind != document.Number || !integerPattern.MatchString(node.Text) { return typeMismatch(path, node, "integer") } value, err := strconv.ParseInt(node.Text, 10, descriptor.typeOf.Bits()) if err != nil { return rangeFailure(path, node, "integer") } result := reflect.New(descriptor.typeOf).Elem() result.SetInt(value) return result, nil case descriptorUnsigned: if node.Kind != document.Number || !unsignedIntegerPattern.MatchString(node.Text) { return typeMismatch(path, node, "unsigned integer") } value, err := strconv.ParseUint(node.Text, 10, descriptor.typeOf.Bits()) if err != nil { return rangeFailure(path, node, "unsigned integer") } result := reflect.New(descriptor.typeOf).Elem() result.SetUint(value) return result, nil case descriptorFloat: if node.Kind != document.Number { return typeMismatch(path, node, "number") } value, err := strconv.ParseFloat(node.Text, descriptor.typeOf.Bits()) if err != nil || math.IsNaN(value) || math.IsInf(value, 0) { return rangeFailure(path, node, "number") } result := reflect.New(descriptor.typeOf).Elem() result.SetFloat(value) return result, nil case descriptorDuration: if node.Kind != document.String { return typeMismatch(path, node, "duration string") } value, err := time.ParseDuration(node.Text) if err != nil { return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeDuration, path, node, "duration string is invalid")} } result := reflect.New(descriptor.typeOf).Elem() result.SetInt(int64(value)) return result, nil case descriptorSecret: if node.Kind != document.String { return typeMismatch(path, node, "secret environment name") } return schema.secrets.Make(SecretState{Name: node.Text, Specified: true}), nil case descriptorText: if node.Kind != document.String { return typeMismatch(path, node, "text string") } result := reflect.New(descriptor.typeOf).Elem() unmarshaler := result.Addr().Interface().(encoding.TextUnmarshaler) if err := unmarshaler.UnmarshalText([]byte(node.Text)); err != nil { return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeText, path, node, "text value is invalid for its configuration type")} } return result, nil default: return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeSchemaUnsupportedType, path, node, "configuration type is not supported")} } } func decodeStruct(node *document.Node, base reflect.Value, descriptor *typeDescriptor, path Path, schema *schemaDescriptor, context decodeContext) (reflect.Value, []decodeIssue) { if node.Kind != document.Mapping { return typeMismatch(path, node, "mapping") } result := reflect.New(descriptor.typeOf).Elem() result.Set(base) var issues []decodeIssue for _, entry := range node.Mapping { childPath := path.Key(entry.Key) if position, ok := descriptor.fieldByName[entry.Key]; ok { field := descriptor.fields[position] target, ok := fieldForDecode(result, field.index, true) if !ok { issues = append(issues, decodeIssue{code: CodeSchemaUnsupportedType, path: childPath, message: "configuration field could not be addressed", location: entry.ValueLocation}) continue } decoded, fieldIssues := decodeValue(entry.Value, target, field.descriptor, childPath, schema, context) if len(fieldIssues) != 0 { issues = append(issues, fieldIssues...) continue } target.Set(decoded) continue } if descriptor.inlineMap != nil { if issue := decodeInlineMapEntry(result, *descriptor.inlineMap, entry, childPath, schema, context); issue != nil { issues = append(issues, issue...) } continue } issues = append(issues, decodeIssue{code: CodeDecodeUnknownField, path: childPath, message: "configuration field is not declared", location: entry.KeyLocation}) } return result, issues } func decodeInlineMapEntry(target reflect.Value, field fieldDescriptor, entry document.Entry, path Path, schema *schemaDescriptor, context decodeContext) []decodeIssue { mapValue, ok := fieldForDecode(target, field.index, true) if !ok || mapValue.Kind() != reflect.Map { return []decodeIssue{{code: CodeSchemaInline, path: path, message: "inline map could not be addressed", location: entry.KeyLocation}} } if mapValue.IsNil() { mapValue.Set(reflect.MakeMap(mapValue.Type())) } key := reflect.New(mapValue.Type().Key()).Elem() key.SetString(entry.Key) base := mapValue.MapIndex(key) if !base.IsValid() { base = reflect.Zero(mapValue.Type().Elem()) } decoded, issues := decodeValue(entry.Value, base, field.descriptor.element, path, schema, context) if len(issues) == 0 { mapValue.SetMapIndex(key, decoded) } return issues } func decodeMap(node *document.Node, base reflect.Value, descriptor *typeDescriptor, path Path, schema *schemaDescriptor, context decodeContext) (reflect.Value, []decodeIssue) { if node.Kind != document.Mapping { return typeMismatch(path, node, "mapping") } result := base if result.IsNil() { result = reflect.MakeMap(descriptor.typeOf) } var issues []decodeIssue for _, entry := range node.Mapping { key := reflect.New(descriptor.typeOf.Key()).Elem() key.SetString(entry.Key) entryBase := result.MapIndex(key) if !entryBase.IsValid() { entryBase = reflect.Zero(descriptor.typeOf.Elem()) } decoded, entryIssues := decodeValue(entry.Value, entryBase, descriptor.element, path.Key(entry.Key), schema, context) if len(entryIssues) != 0 { issues = append(issues, entryIssues...) continue } result.SetMapIndex(key, decoded) } return result, issues } func decodeSlice(node *document.Node, descriptor *typeDescriptor, path Path, schema *schemaDescriptor, context decodeContext) (reflect.Value, []decodeIssue) { if node.Kind != document.Sequence { return typeMismatch(path, node, "sequence") } result := reflect.MakeSlice(descriptor.typeOf, len(node.Sequence), len(node.Sequence)) var issues []decodeIssue for i, child := range node.Sequence { decoded, childIssues := decodeValue(child, reflect.Zero(descriptor.typeOf.Elem()), descriptor.element, path.Index(i), schema, context) if len(childIssues) != 0 { issues = append(issues, childIssues...) continue } result.Index(i).Set(decoded) } return result, issues } func decodeArray(node *document.Node, descriptor *typeDescriptor, path Path, schema *schemaDescriptor, context decodeContext) (reflect.Value, []decodeIssue) { if node.Kind != document.Sequence { return typeMismatch(path, node, "sequence") } if len(node.Sequence) != descriptor.length { return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeArrayLength, path, node, "configuration array has the wrong length")} } result := reflect.New(descriptor.typeOf).Elem() var issues []decodeIssue for i, child := range node.Sequence { decoded, childIssues := decodeValue(child, reflect.Zero(descriptor.typeOf.Elem()), descriptor.element, path.Index(i), schema, context) if len(childIssues) != 0 { issues = append(issues, childIssues...) continue } result.Index(i).Set(decoded) } return result, issues } func decodeDynamic(node *document.Node, base reflect.Value, path Path, schema *schemaDescriptor, context decodeContext) (reflect.Value, []decodeIssue) { if node.Kind == document.Mapping && base.IsValid() && !base.IsNil() { concrete := base.Elem() operation := valueOperation{schema: schema} descriptor, issue := operation.dynamicDescriptor(concrete.Type(), path) if issue == nil && acceptsMapping(descriptor) { decoded, issues := decodeValue(node, concrete, descriptor, path, schema, context) if len(issues) != 0 { return reflect.Value{}, issues } result := reflect.New(base.Type()).Elem() result.Set(decoded) return result, nil } } var value any switch node.Kind { case document.Null: return reflect.Zero(base.Type()), nil case document.String: value = node.Text case document.Boolean: value = node.Text == "true" case document.Number: if integerPattern.MatchString(node.Text) { parsed, err := strconv.ParseInt(node.Text, 10, 64) if err != nil { return rangeFailure(path, node, "dynamic integer") } value = parsed } else { parsed, err := strconv.ParseFloat(node.Text, 64) if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) { return rangeFailure(path, node, "dynamic number") } value = parsed } case document.Sequence: sequence := make([]any, len(node.Sequence)) var issues []decodeIssue for i, child := range node.Sequence { decoded, childIssues := decodeDynamic(child, reflect.Zero(reflect.TypeFor[any]()), path.Index(i), schema, context) if len(childIssues) != 0 { issues = append(issues, childIssues...) continue } sequence[i] = decoded.Interface() } if len(issues) != 0 { return reflect.Value{}, issues } value = sequence case document.Mapping: mapping := make(map[string]any, len(node.Mapping)) var issues []decodeIssue for _, entry := range node.Mapping { decoded, childIssues := decodeDynamic(entry.Value, reflect.Zero(reflect.TypeFor[any]()), path.Key(entry.Key), schema, context) if len(childIssues) != 0 { issues = append(issues, childIssues...) continue } mapping[entry.Key] = decoded.Interface() } if len(issues) != 0 { return reflect.Value{}, issues } value = mapping default: return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeType, path, node, "dynamic configuration value has an unsupported kind")} } result := reflect.New(base.Type()).Elem() if value != nil { result.Set(reflect.ValueOf(value)) } return result, nil } func acceptsMapping(descriptor *typeDescriptor) bool { terminal, ok := terminalDescriptor(descriptor) if !ok { return false } return terminal.kind == descriptorStruct || terminal.kind == descriptorMap || terminal.kind == descriptorDynamic } func fieldForDecode(root reflect.Value, index []int, allocate bool) (reflect.Value, bool) { current := root for position, fieldIndex := range index { if current.Kind() != reflect.Struct { return reflect.Value{}, false } current = current.Field(fieldIndex) if position == len(index)-1 { return current, current.CanSet() } for current.Kind() == reflect.Pointer { if current.IsNil() { if !allocate || !current.CanSet() { return reflect.Value{}, false } current.Set(reflect.New(current.Type().Elem())) } current = current.Elem() } } return reflect.Value{}, false } func collectPresence(node *document.Node, path Path, presence map[string]inputPresence) { presence[path.String()] = inputPresence{location: node.Location} switch node.Kind { case document.Mapping: for _, entry := range node.Mapping { collectPresence(entry.Value, path.Key(entry.Key), presence) } case document.Sequence: for i, child := range node.Sequence { collectPresence(child, path.Index(i), presence) } } } func collectOrigins(node *document.Node, path Path, presence map[string]inputPresence, origins map[string]Origin) { if input, ok := presence[path.String()]; ok { origins[path.String()] = Origin{Layer: LayerBase, Locations: []Location{{SourceID: input.location.SourceID, Line: input.location.Line, Column: input.location.Column}}} } else { origins[path.String()] = Origin{Layer: LayerDefault} } switch node.Kind { case document.Mapping: for _, entry := range node.Mapping { collectOrigins(entry.Value, path.Key(entry.Key), presence, origins) } case document.Sequence: for i, child := range node.Sequence { collectOrigins(child, path.Index(i), presence, origins) } } } func decodeIssueFromProjection(issue *projectionIssue) decodeIssue { return decodeIssue{code: issue.code, path: issue.path.clone(), message: issue.message} } func newDecodeIssue(code Code, path Path, node *document.Node, message string) decodeIssue { return decodeIssue{code: code, path: path.clone(), message: message, location: node.Location} } func typeMismatch(path Path, node *document.Node, expected string) (reflect.Value, []decodeIssue) { return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeType, path, node, "configuration value must be a "+expected)} } func rangeFailure(path Path, node *document.Node, expected string) (reflect.Value, []decodeIssue) { return reflect.Value{}, []decodeIssue{newDecodeIssue(CodeDecodeRange, path, node, expected+" is outside the supported range")} }