package engine import ( "bytes" "encoding/binary" "errors" "math/big" "sort" "strings" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) var ( errEmptySequenceIdentityFields = errors.New("sequence identity requires at least one field") errInvalidSequenceIdentityNumber = errors.New("invalid sequence identity number") errUnsupportedSequenceIdentityValue = errors.New("unsupported sequence identity value") ) type sequenceIdentityDiagnosticCodes struct { invalid Code conflict Code } type sequenceIdentityRecord struct { tuple string index int location document.Location } // normalizeSequenceIdentityFields treats identity field names as a set. func normalizeSequenceIdentityFields(fields []string) ([]string, error) { if len(fields) == 0 { return nil, errEmptySequenceIdentityFields } normalized := append([]string(nil), fields...) sort.Strings(normalized) write := 0 for _, field := range normalized { if write != 0 && normalized[write-1] == field { continue } normalized[write] = field write++ } return normalized[:write], nil } // normalizeSequenceIdentityNumber returns coefficient and exponent without // expanding powers of ten. The represented value is coefficient * 10^exponent. func normalizeSequenceIdentityNumber(value string) (string, error) { if value == "" { return "", errInvalidSequenceIdentityNumber } negative := false if value[0] == '-' { negative = true value = value[1:] } if value == "" { return "", errInvalidSequenceIdentityNumber } exponent := new(big.Int) if index := strings.IndexAny(value, "eE"); index >= 0 { if strings.IndexAny(value[index+1:], "eE") >= 0 { return "", errInvalidSequenceIdentityNumber } text := value[index+1:] if text == "" { return "", errInvalidSequenceIdentityNumber } if _, ok := exponent.SetString(text, 10); !ok { return "", errInvalidSequenceIdentityNumber } value = value[:index] } point := strings.IndexByte(value, '.') if point >= 0 && strings.IndexByte(value[point+1:], '.') >= 0 { return "", errInvalidSequenceIdentityNumber } integerPart := value fractionPart := "" if point >= 0 { integerPart = value[:point] fractionPart = value[point+1:] } if integerPart == "" || (point >= 0 && fractionPart == "") { return "", errInvalidSequenceIdentityNumber } if len(integerPart) > 1 && integerPart[0] == '0' { return "", errInvalidSequenceIdentityNumber } digits := integerPart + fractionPart for _, digit := range digits { if digit < '0' || digit > '9' { return "", errInvalidSequenceIdentityNumber } } coefficient := strings.TrimLeft(digits, "0") if coefficient == "" { return "0e0", nil } exponent.Sub(exponent, big.NewInt(int64(len(fractionPart)))) trimmed := strings.TrimRight(coefficient, "0") exponent.Add(exponent, big.NewInt(int64(len(coefficient)-len(trimmed)))) coefficient = trimmed if negative { coefficient = "-" + coefficient } return coefficient + "e" + exponent.String(), nil } func sequenceScalarIdentityComponent(node *document.Node) (byte, string, error) { if node == nil { return 0, "", errUnsupportedSequenceIdentityValue } switch node.Kind { case document.String: return 's', node.Text, nil case document.Number: normalized, err := normalizeSequenceIdentityNumber(node.Text) return 'n', normalized, err case document.Boolean: if node.Text != "true" && node.Text != "false" { return 0, "", errUnsupportedSequenceIdentityValue } return 'b', node.Text, nil case document.Null: return 'z', "", nil default: return 0, "", errUnsupportedSequenceIdentityValue } } func sequenceIdentityTuple(values []*document.Node) (string, error) { var encoded bytes.Buffer for _, value := range values { kind, component, err := sequenceScalarIdentityComponent(value) if err != nil { return "", err } encoded.WriteByte(kind) var length [binary.MaxVarintLen64]byte n := binary.PutUvarint(length[:], uint64(len(component))) encoded.Write(length[:n]) encoded.WriteString(component) } return encoded.String(), nil } // sequenceMappingIndex is built once per record. Structural fields are omitted // by entry index, so a directive is excluded only at its actual occurrence. type sequenceMappingIndex struct { entries []document.Entry byKey map[string]int } func newSequenceMappingIndex(record *document.Node) sequenceMappingIndex { index := sequenceMappingIndex{byKey: make(map[string]int)} if record == nil || record.Kind != document.Mapping { return index } index.entries = record.Mapping directiveIndex := -1 if directive, exists := inheritanceDirective(record); exists { for entryIndex := range record.Mapping { if record.Mapping[entryIndex].Key == directive.Key && record.Mapping[entryIndex].KeyLocation == directive.KeyLocation { directiveIndex = entryIndex break } } } for entryIndex, entry := range record.Mapping { if entryIndex == directiveIndex { continue } if _, exists := index.byKey[entry.Key]; !exists { index.byKey[entry.Key] = entryIndex } } return index } func (index sequenceMappingIndex) identity(fields []string) (string, []document.Location, string, int) { values := make([]*document.Node, len(fields)) locations := make([]document.Location, len(fields)) for fieldIndex, field := range fields { entryIndex, exists := index.byKey[field] if !exists { return "", nil, field, -1 } entry := index.entries[entryIndex] if entry.Value == nil || (entry.Value.Kind != document.String && entry.Value.Kind != document.Number && entry.Value.Kind != document.Boolean && entry.Value.Kind != document.Null) { return "", nil, field, entryIndex } if _, _, err := sequenceScalarIdentityComponent(entry.Value); err != nil { return "", nil, field, entryIndex } values[fieldIndex] = entry.Value locations[fieldIndex] = entry.ValueLocation if entry.Value != nil && entry.Value.Location != (document.Location{}) { locations[fieldIndex] = entry.Value.Location } } tuple, err := sequenceIdentityTuple(values) if err != nil { return "", nil, fields[0], -2 } return tuple, locations, "", -3 } // validateSequenceIdentity checks mapping shape, direct identity fields, and // uniqueness in one tuple-to-first-location index. It does not alter the nodes. func validateSequenceIdentity(sequence *document.Node, path Path, fields []string, codes sequenceIdentityDiagnosticCodes) (map[string]sequenceIdentityRecord, []Diagnostic) { return validateSequenceIdentityFrom(sequence, path, fields, codes, 0) } func validateSequenceIdentityFrom(sequence *document.Node, path Path, fields []string, codes sequenceIdentityDiagnosticCodes, startIndex int) (map[string]sequenceIdentityRecord, []Diagnostic) { normalized, err := normalizeSequenceIdentityFields(fields) if err != nil { location := (*Location)(nil) if sequence != nil { converted := publicLocation(sequence.Location) location = &converted } return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: path, Message: "sequence identity requires at least one field", Primary: location}} } return validateNormalizedSequenceIdentityFrom(sequence, path, normalized, codes, startIndex) } func validateNormalizedSequenceIdentityFrom(sequence *document.Node, path Path, normalized []string, codes sequenceIdentityDiagnosticCodes, startIndex int) (map[string]sequenceIdentityRecord, []Diagnostic) { if sequence == nil || sequence.Kind != document.Sequence { location := (*Location)(nil) if sequence != nil { converted := publicLocation(sequence.Location) location = &converted } return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: path, Message: "identified collection must be a sequence", Primary: location}} } firstByTuple := make(map[string]sequenceIdentityRecord, len(sequence.Sequence)) for itemIndex, item := range sequence.Sequence { itemPath := path.Index(startIndex + itemIndex) if item == nil || item.Kind != document.Mapping { var primary *Location if item != nil { converted := publicLocation(item.Location) primary = &converted } return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: itemPath, Message: "identified collection items must be mappings", Primary: primary}} } mapping := newSequenceMappingIndex(item) tuple, locations, missing, entryIndex := mapping.identity(normalized) if entryIndex == -1 { location := publicLocation(item.Location) return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: itemPath.Key(missing), Message: "identified collection item is missing an identity field", Primary: &location}} } if entryIndex == -2 || entryIndex >= 0 { location := publicLocation(item.Location) fieldPath := itemPath.Key(missing) if entryIndex >= 0 { location = publicLocation(item.Mapping[entryIndex].ValueLocation) if value := item.Mapping[entryIndex].Value; value != nil { location = publicLocation(value.Location) } } return nil, []Diagnostic{{Code: codes.invalid, Severity: SeverityError, Path: fieldPath, Message: "identity fields must be scalar values", Primary: &location}} } if first, duplicate := firstByTuple[tuple]; duplicate { current := publicLocation(locations[0]) return nil, []Diagnostic{{Code: codes.conflict, Severity: SeverityError, Path: path, Message: "identified collection contains duplicate identities", Primary: ptrLocation(publicLocation(first.location)), Related: []Location{current}}} } firstByTuple[tuple] = sequenceIdentityRecord{tuple: tuple, index: itemIndex, location: locations[0]} } return firstByTuple, nil } func ptrLocation(location Location) *Location { return &location }