package engine import ( "path" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type importSelector uint8 const ( importSelectorPath importSelector = iota + 1 importSelectorDirectory importSelectorGlob ) type importFieldSite struct { path Path location Location } // importDeclarationSpec preserves authored presence until discovery policy is resolved. type importDeclarationSpec struct { path Path location Location selector importSelector paths []string selectorSites []importFieldSite glob string globSegments []string required *bool recursive *bool follow *bool hidden *bool pattern []string patternSet bool exclude []string excludeSet bool depth int depthSet bool destination Path placement placementMode fieldSites map[string]importFieldSite } // importDefaults are the engine's private copy of load-wide discovery defaults. type importDefaults struct { required *bool follow *bool hidden *bool recursive *bool pattern []string } // ImportDefaults transports root-owned load defaults across the facade // boundary. Load options snapshot this value before source acquisition. type ImportDefaults struct { // Required controls absence handling when non-nil. Required *bool // Follow controls symlink following when non-nil. Follow *bool // Hidden controls hidden-entry discovery when non-nil. Hidden *bool // Recursive controls descendant directory traversal when non-nil. Recursive *bool // Pattern filters directory-search filenames; nil and empty differ. Pattern []string } // OptionsWithImportDefaults copies root-owned defaults into ordinary options. func OptionsWithImportDefaults[T any](options Options[T], value ImportDefaults) Options[T] { options.importDefaults = privateImportDefaults(value) return options } // LayeredOptionsWithImportDefaults copies root-owned defaults into grouped options. func LayeredOptionsWithImportDefaults[T any](options LayeredOptions[T], value ImportDefaults) LayeredOptions[T] { options.importDefaults = privateImportDefaults(value) return options } func privateImportDefaults(value ImportDefaults) importDefaults { return snapshotImportDefaults(importDefaults{ required: value.Required, follow: value.Follow, hidden: value.Hidden, recursive: value.Recursive, pattern: value.Pattern, }) } type resolvedImportPolicy struct { required bool follow bool hidden bool recursive bool pattern []string depth int depthSet bool exclude []string destination Path placement placementMode } func snapshotImportDefaults(value importDefaults) importDefaults { return importDefaults{ required: cloneBool(value.required), follow: cloneBool(value.follow), hidden: cloneBool(value.hidden), recursive: cloneBool(value.recursive), pattern: cloneStrings(value.pattern), } } func cloneBool(value *bool) *bool { if value == nil { return nil } cloned := *value return &cloned } func cloneStrings(values []string) []string { if values == nil { return nil } return append(make([]string, 0, len(values)), values...) } func validateImportDefaults(value importDefaults) []Diagnostic { for _, pattern := range value.pattern { if _, err := path.Match(pattern, ""); err != nil { return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "import default pattern is invalid")} } } return nil } func resolveImportPolicy(spec importDeclarationSpec, defaults importDefaults) (resolvedImportPolicy, []Diagnostic) { defaults = snapshotImportDefaults(defaults) policy := resolvedImportPolicy{ required: boolOr(spec.required, defaults.required, false), follow: true, hidden: true, recursive: false, destination: spec.destination.clone(), placement: spec.placement, depth: spec.depth, depthSet: spec.depthSet, exclude: cloneStrings(spec.exclude), } if spec.selector == importSelectorDirectory || spec.selector == importSelectorGlob { policy.follow = boolOr(spec.follow, defaults.follow, true) policy.hidden = boolOr(spec.hidden, defaults.hidden, true) } if spec.selector == importSelectorDirectory { policy.recursive = boolOr(spec.recursive, defaults.recursive, false) if spec.patternSet { policy.pattern = cloneStrings(spec.pattern) } else { policy.pattern = cloneStrings(defaults.pattern) } } if spec.depthSet && !policy.recursive { site := spec.fieldSites["depth"] location := site.location return resolvedImportPolicy{}, []Diagnostic{{ Code: CodeImportInvalid, Severity: SeverityError, Path: site.path.clone(), Message: "depth requires effective recursive discovery", Primary: &location, }} } return policy, nil } func boolOr(explicit, application *bool, fallback bool) bool { if explicit != nil { return *explicit } if application != nil { return *application } return fallback } // parseImportSpecificationDirective validates every declaration before callers select candidates. func parseImportSpecificationDirective(node *document.Node, directivePath Path) ([]importDeclarationSpec, []Diagnostic) { if node.Kind != document.Mapping { return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, directivePath, "configuration directive must be a mapping", node)} } var specs []importDeclarationSpec var diagnostics []Diagnostic for _, entry := range node.Mapping { fieldPath := directivePath.Key(entry.Key) if entry.Key != "imports" { diagnostics = append(diagnostics, nodeImportDiagnostic(CodeImportInvalid, fieldPath, "configuration directive field is not recognized", entry.Value)) continue } if entry.Value.Kind != document.Sequence { diagnostics = append(diagnostics, nodeImportDiagnostic(CodeImportInvalid, fieldPath, "imports must be a sequence", entry.Value)) continue } for index, declarationNode := range entry.Value.Sequence { spec, issues := parseImportDeclarationSpec(declarationNode, fieldPath.Index(index)) if len(issues) != 0 { diagnostics = append(diagnostics, issues...) continue } specs = append(specs, spec) } } return specs, canonicalDiagnostics(diagnostics) } func parseImportDeclarationSpec(node *document.Node, declarationPath Path) (importDeclarationSpec, []Diagnostic) { if node.Kind != document.Mapping { return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration must be a mapping", node)} } spec := importDeclarationSpec{ path: declarationPath.clone(), location: publicLocation(node.Location), placement: placementModeMapping, fieldSites: make(map[string]importFieldSite, len(node.Mapping)), } selectors := 0 selectorIndex := -1 for index, entry := range node.Mapping { if selectorForField(entry.Key) != 0 { selectors++ selectorIndex = index } } if selectors != 1 { return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration requires exactly one path, dir, or glob selector", node)} } selectorEntry := node.Mapping[selectorIndex] spec.selector = selectorForField(selectorEntry.Key) for _, entry := range node.Mapping { fieldPath := declarationPath.Key(entry.Key) spec.fieldSites[entry.Key] = importFieldSite{path: fieldPath.clone(), location: publicLocation(entry.Value.Location)} if selectorForField(entry.Key) != 0 { continue } if !importFieldApplies(entry.Key, spec.selector) { return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not supported by this selector", entry.Value)} } switch entry.Key { case "required", "recursive", "follow", "hidden": if entry.Value.Kind != document.Boolean { return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, entry.Key+" must be a boolean", entry.Value)} } value := entry.Value.Text == "true" switch entry.Key { case "required": spec.required = &value case "recursive": spec.recursive = &value case "follow": spec.follow = &value case "hidden": spec.hidden = &value } case "pattern": values, origins, ok, badIndex := parseStringSet(entry.Value) if !ok { return importDeclarationSpec{}, []Diagnostic{stringSetDiagnostic(entry.Value, fieldPath, "pattern", badIndex)} } if badIndex, ok := invalidWildcard(values); ok { badPath, badNode := listMemberSite(entry.Value, fieldPath, origins[badIndex]) return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, badPath, "pattern contains an invalid wildcard expression", badNode)} } spec.pattern, spec.patternSet = values, true case "exclude": values, origins, ok, badIndex := parseStringSet(entry.Value) if !ok { return importDeclarationSpec{}, []Diagnostic{stringSetDiagnostic(entry.Value, fieldPath, "exclude", badIndex)} } if badIndex, ok := invalidWildcard(values); ok { badPath, badNode := listMemberSite(entry.Value, fieldPath, origins[badIndex]) return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, badPath, "exclude contains an invalid wildcard expression", badNode)} } spec.exclude, spec.excludeSet = values, true case "depth": depth, ok := parseImportDepth(entry.Value) if !ok { return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "depth must be a nonnegative integer", entry.Value)} } spec.depth, spec.depthSet = depth, true case "destination": destination, diagnostic := parseImportDestination(entry.Value, fieldPath) if diagnostic != nil { return importDeclarationSpec{}, []Diagnostic{*diagnostic} } spec.destination = destination case "placement": if entry.Value.Kind != document.String { return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", entry.Value)} } switch entry.Value.Text { case "mapping": spec.placement = placementModeMapping case "sequence-item": spec.placement = placementModeSequenceItem default: return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "placement must be mapping or sequence-item", entry.Value)} } default: return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)} } } selectorPath := declarationPath.Key(selectorEntry.Key) spec.fieldSites[selectorEntry.Key] = importFieldSite{path: selectorPath.clone(), location: publicLocation(selectorEntry.Value.Location)} switch spec.selector { case importSelectorPath, importSelectorDirectory: values, origins, ok, badIndex := parseStringSet(selectorEntry.Value) if !ok { name := selectorEntry.Key return importDeclarationSpec{}, []Diagnostic{stringSetDiagnostic(selectorEntry.Value, selectorPath, name, badIndex)} } for index, value := range values { if !validImportPath(value) { badPath, badNode := listMemberSite(selectorEntry.Value, selectorPath, origins[index]) return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, badPath, selectorEntry.Key+" contains an invalid source path", badNode)} } badPath, badNode := listMemberSite(selectorEntry.Value, selectorPath, origins[index]) spec.selectorSites = append(spec.selectorSites, importFieldSite{path: badPath.clone(), location: publicLocation(badNode.Location)}) } spec.paths = values case importSelectorGlob: if selectorEntry.Value.Kind != document.String || !validImportPath(selectorEntry.Value.Text) { return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, selectorPath, "glob must be a valid nonempty path pattern string", selectorEntry.Value)} } segments, ok := validGlobPattern(selectorEntry.Value.Text) if !ok { return importDeclarationSpec{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, selectorPath, "glob pattern is invalid", selectorEntry.Value)} } spec.glob, spec.globSegments = selectorEntry.Value.Text, segments } return spec, nil } func selectorForField(field string) importSelector { switch field { case "path": return importSelectorPath case "dir": return importSelectorDirectory case "glob": return importSelectorGlob default: return 0 } } func importFieldApplies(field string, selector importSelector) bool { switch field { case "required", "exclude", "destination", "placement": return true case "pattern", "recursive", "depth": return selector == importSelectorDirectory case "follow", "hidden": return selector == importSelectorDirectory || selector == importSelectorGlob default: return false } } func parseStringSet(node *document.Node) ([]string, []int, bool, int) { if node.Kind == document.String { return []string{node.Text}, []int{-1}, true, -1 } if node.Kind != document.Sequence { return nil, nil, false, -1 } values := make([]string, 0, len(node.Sequence)) origins := make([]int, 0, len(node.Sequence)) seen := make(map[string]struct{}, len(node.Sequence)) for index, item := range node.Sequence { if item.Kind != document.String { return nil, nil, false, index } if _, exists := seen[item.Text]; exists { continue } seen[item.Text] = struct{}{} values = append(values, item.Text) origins = append(origins, index) } return values, origins, true, -1 } func stringSetDiagnostic(node *document.Node, fieldPath Path, field string, badIndex int) Diagnostic { if badIndex >= 0 { badPath, item := listMemberSite(node, fieldPath, badIndex) return nodeImportDiagnostic(CodeImportInvalid, badPath, field+" members must be strings", item) } return nodeImportDiagnostic(CodeImportInvalid, fieldPath, field+" must be a string or a flat list of strings", node) } func invalidWildcard(values []string) (int, bool) { for index, value := range values { if _, err := path.Match(value, ""); err != nil { return index, true } } return -1, false } func listMemberSite(node *document.Node, fieldPath Path, index int) (Path, *document.Node) { if node.Kind != document.Sequence || index < 0 { return fieldPath, node } return fieldPath.Index(index), node.Sequence[index] } func parseImportDepth(node *document.Node) (int, bool) { if node.Kind != document.Number || node.Text == "" { return 0, false } if node.Text == "-0" { return 0, true } var value uint64 maxInt := uint64(^uint(0) >> 1) for index := 0; index < len(node.Text); index++ { character := node.Text[index] if character < '0' || character > '9' { return 0, false } digit := uint64(character - '0') if value > (maxInt-digit)/10 { value = maxInt continue // Saturate the bound, but validate every remaining character. } value = value*10 + digit } return int(value), true } func parseImportDestination(node *document.Node, fieldPath Path) (Path, *Diagnostic) { if node.Kind != document.Sequence { diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must be a sequence of strings", node) return Path{}, &diagnostic } relative := Path{segments: make([]pathSegment, 0, len(node.Sequence))} for index, item := range node.Sequence { memberPath := fieldPath.Index(index) if item.Kind != document.String { diagnostic := nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be strings", item) return Path{}, &diagnostic } member := Path{segments: []pathSegment{{kind: pathSegmentKey, key: item.Text}}} if _, err := normalizeDestination(member); err != nil { diagnostic := nodeImportDiagnostic(CodeImportInvalid, memberPath, "destination members must be non-reserved mapping keys", item) return Path{}, &diagnostic } relative.segments = append(relative.segments, pathSegment{kind: pathSegmentKey, key: item.Text}) } normalized, err := normalizeDestination(relative) if err != nil { diagnostic := nodeImportDiagnostic(CodeImportInvalid, fieldPath, "destination must contain only non-reserved mapping keys", node) return Path{}, &diagnostic } return normalized, nil }