package configkit import ( "path" "strings" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) const directiveKey = "configkit" type importDeclaration struct { path string glob string 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)} 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" 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 { if rule == nil { return nil } current := root if len(rule.Path.segments) == 0 { return current } for _, segment := range rule.Path.segments { if current.Kind != document.Mapping { return current } if segment.kind == pathSegmentIndex { return nil } key := segment.key index := documentEntryIndex(current.Mapping, key) if index < 0 { return nil } current = current.Mapping[index].Value } 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 profileDirectiveDiagnostics(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: CodeProfileDirective, Severity: SeverityError, Path: childPath, Message: "profiles cannot contain configuration directives", Primary: &location, }} } if diagnostics := profileDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 { return diagnostics } } } else if node.Kind == document.Sequence { for index, child := range node.Sequence { if diagnostics := profileDirectiveDiagnostics(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 }