package engine import ( "path" "strings" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) const directiveKey = "configkit" type importDeclaration struct { path string glob string destination Path placement placementMode placementPath Path placementLocation *Location optional bool location Location } func splitImportDirectives(root *document.Node) (*document.Node, []importDeclaration, []Diagnostic) { body := document.Clone(root) var declarations []importDeclaration directiveIndex := -1 for index, entry := range body.Mapping { if entry.Key != directiveKey { continue } directiveIndex = index parsed, diagnostics := parseImportDirective(entry.Value, Keys(directiveKey)) if len(diagnostics) != 0 { return nil, nil, diagnostics } declarations = parsed break } if directiveIndex >= 0 { body.Mapping = append(body.Mapping[:directiveIndex], body.Mapping[directiveIndex+1:]...) } if diagnostics := nestedDirectiveDiagnostics(body, Path{}); len(diagnostics) != 0 { return nil, nil, diagnostics } return body, declarations, nil } func parseImportDirective(node *document.Node, directivePath Path) ([]importDeclaration, []Diagnostic) { if node.Kind != document.Mapping { return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, directivePath, "configuration directive must be a mapping", node)} } var declarations []importDeclaration for _, entry := range node.Mapping { fieldPath := directivePath.Key(entry.Key) if entry.Key != "imports" { return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "configuration directive field is not recognized", entry.Value)} } if entry.Value.Kind != document.Sequence { return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "imports must be a sequence", entry.Value)} } for index, item := range entry.Value.Sequence { declaration, diagnostics := parseImportDeclaration(item, fieldPath.Index(index)) if len(diagnostics) != 0 { return nil, diagnostics } declarations = append(declarations, declaration) } } return declarations, nil } func parseImportDeclaration(node *document.Node, declarationPath Path) (importDeclaration, []Diagnostic) { if node.Kind != document.Mapping { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration must be a mapping", node)} } declaration := importDeclaration{location: publicLocation(node.Location), placementPath: declarationPath.clone()} seenTarget := false for _, entry := range node.Mapping { fieldPath := declarationPath.Key(entry.Key) switch entry.Key { case "path", "glob": if seenTarget || entry.Value.Kind != document.String || entry.Value.Text == "" || strings.TrimSpace(entry.Value.Text) != entry.Value.Text { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration requires exactly one nonempty path or glob string", entry.Value)} } seenTarget = true declaration.location = publicLocation(entry.Value.Location) if entry.Key == "path" { declaration.path = entry.Value.Text } else { declaration.glob = entry.Value.Text } case "optional": if entry.Value.Kind != document.Boolean { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "optional must be a boolean", entry.Value)} } declaration.optional = entry.Value.Text == "true" case "destination": if entry.Value.Kind != document.Sequence { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must be a sequence of strings", entry.Value)} } relative := Path{segments: make([]pathSegment, 0, len(entry.Value.Sequence))} for index, item := range entry.Value.Sequence { memberPath := fieldPath.Index(index) if item.Kind != document.String { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be strings", item)} } member := Path{segments: []pathSegment{{kind: pathSegmentKey, key: item.Text}}} if _, err := normalizeDestination(member); err != nil { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be non-reserved mapping keys", item)} } relative.segments = append(relative.segments, pathSegment{kind: pathSegmentKey, key: item.Text}) } normalized, err := normalizeDestination(relative) if err != nil { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must contain only non-reserved mapping keys", entry.Value)} } declaration.destination = normalized case "placement": declaration.placementPath = fieldPath.clone() placementLocation := publicLocation(entry.Value.Location) declaration.placementLocation = &placementLocation if entry.Value.Kind != document.String { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", entry.Value)} } switch entry.Value.Text { case "mapping": declaration.placement = placementModeMapping case "sequence-item": declaration.placement = placementModeSequenceItem default: return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", entry.Value)} } default: return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)} } } if !seenTarget { return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration requires exactly one path or glob", node)} } return declaration, nil } func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic { if node.Kind == document.Mapping { for _, entry := range node.Mapping { childPath := current.Key(entry.Key) if entry.Key == directiveKey { location := publicLocation(entry.KeyLocation) return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}} } if diagnostics := nestedDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 { return diagnostics } } } else if node.Kind == document.Sequence { for index, child := range node.Sequence { if diagnostics := nestedDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 { return diagnostics } } } return nil } func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node { return fragmentVersionClaimProjected(root, rule, -1, nil) } func fragmentVersionClaimProjected(root *document.Node, rule *VersionRule, sourceIndex int, ranges []sequenceRange) *document.Node { return fragmentVersionClaimWithIndex(root, rule, sourceIndex, indexSequenceRanges(ranges)) } func fragmentVersionClaimWithIndex(root *document.Node, rule *VersionRule, sourceIndex int, ranges sequenceRangeIndex) *document.Node { if rule == nil { return nil } current := root currentPath := Path{} if len(rule.Path.segments) == 0 { return current } for _, segment := range rule.Path.segments { switch current.Kind { case document.Mapping: if segment.kind == pathSegmentIndex { return current } index := documentEntryIndex(current.Mapping, segment.key) if index < 0 { return nil } current = current.Mapping[index].Value currentPath = currentPath.Key(segment.key) case document.Sequence: index, valid := schemaIndex(segment) if !valid { return current } if assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: currentPath.String()}]; found { if index < assigned.start || index >= assigned.end { return nil } index -= assigned.start } if index < 0 || index >= len(current.Sequence) { return nil } current = current.Sequence[index] currentPath = currentPath.Index(index) default: // A scalar or null at an ancestor replaces the protected subtree. return current } } return current } func validImportPath(value string) bool { if value == "" || strings.TrimSpace(value) != value || path.IsAbs(value) || strings.HasPrefix(value, "//") { return false } return !hasImportScheme(value) } func hasImportScheme(value string) bool { colon := strings.IndexByte(value, ':') slash := strings.IndexByte(value, '/') if colon <= 0 || (slash >= 0 && slash < colon) { return false } for index := 0; index < colon; index++ { character := value[index] if index == 0 { if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') { return false } continue } if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') && (character < '0' || character > '9') && character != '+' && character != '-' && character != '.' { return false } } return true } func nodeImportDiagnostic(code Code, path Path, message string, node *document.Node) Diagnostic { location := publicLocation(node.Location) return Diagnostic{Code: code, Severity: SeverityError, Path: path, Message: message, Primary: &location} } func overlayDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic { if node.Kind == document.Mapping { for _, entry := range node.Mapping { childPath := current.Key(entry.Key) if entry.Key == directiveKey { location := publicLocation(entry.KeyLocation) return []Diagnostic{{ Code: CodeOverlayDirective, Severity: SeverityError, Path: childPath, Message: "overlays cannot contain configuration directives", Primary: &location, }} } if diagnostics := overlayDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 { return diagnostics } } } else if node.Kind == document.Sequence { for index, child := range node.Sequence { if diagnostics := overlayDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 { return diagnostics } } } return nil } func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic { var diagnostics []Diagnostic if node.Kind == document.Mapping { for _, entry := range node.Mapping { childPath := path.Key(entry.Key) if entry.Key == directiveKey { location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column} diagnostics = append(diagnostics, Diagnostic{ Code: CodeImportDisabled, Severity: SeverityError, Path: childPath, Message: "configuration directives require imports to be enabled", Primary: &location, }) } diagnostics = append(diagnostics, disabledDirectiveDiagnostics(entry.Value, childPath)...) } } else if node.Kind == document.Sequence { for index, child := range node.Sequence { diagnostics = append(diagnostics, disabledDirectiveDiagnostics(child, path.Index(index))...) } } return diagnostics }