From 5f33e1e90c5b99f483d2ec9fea7133510cb5db59 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 19 Oct 2026 20:07:54 +0000 Subject: [PATCH] Select named imports before source admission --- internal/engine/import_selection.go | 130 ++++++++++++ internal/engine/import_selection_test.go | 257 +++++++++++++++++++++++ internal/engine/import_spec.go | 3 + 3 files changed, 390 insertions(+) create mode 100644 internal/engine/import_selection.go create mode 100644 internal/engine/import_selection_test.go diff --git a/internal/engine/import_selection.go b/internal/engine/import_selection.go new file mode 100644 index 0000000..1dc44f6 --- /dev/null +++ b/internal/engine/import_selection.go @@ -0,0 +1,130 @@ +package engine + +import ( + "errors" + "io/fs" + "path" + "path/filepath" + "sort" +) + +type importSelectionOutcome uint8 + +const ( + importSelectionNone importSelectionOutcome = iota + importSelectionHasCandidates +) + +type selectedImportCandidate struct { + path string + accessPath string + site importFieldSite +} + +// importSelection contains lexical candidates only; admission assigns source identity later. +type importSelection struct { + candidates []selectedImportCandidate + outcome importSelectionOutcome +} + +func selectNamedImportCandidates(access *filesystemAccess, basePath string, spec importDeclarationSpec, policy resolvedImportPolicy) (importSelection, []Diagnostic) { + normalized := make([]selectedImportCandidate, 0, len(spec.paths)) + seen := make(map[string]struct{}, len(spec.paths)) + for index, relative := range spec.paths { + accessPath := access.join(basePath, relative) + candidatePath := accessPath + if access.native == nil { + candidatePath = filepath.ToSlash(filepath.Clean(accessPath)) + } + if _, exists := seen[candidatePath]; exists { + continue + } + seen[candidatePath] = struct{}{} + var site importFieldSite + if index < len(spec.pathSites) { + site = spec.pathSites[index] + } else { + site = spec.fieldSites["path"] + } + normalized = append(normalized, selectedImportCandidate{path: candidatePath, accessPath: accessPath, site: site}) + } + sort.Slice(normalized, func(left, right int) bool { + return normalized[left].path < normalized[right].path + }) + candidates := make([]selectedImportCandidate, 0, len(normalized)) + for _, candidate := range normalized { + missing, diagnostic := validateNamedImportTarget(access, candidate, policy.required) + if diagnostic != nil { + return importSelection{}, []Diagnostic{*diagnostic} + } + if missing { + continue + } + if excludedBasename(path.Base(candidate.path), policy.exclude) { + continue + } + candidates = append(candidates, candidate) + } + selection := importSelection{candidates: candidates} + if len(candidates) != 0 { + selection.outcome = importSelectionHasCandidates + } + return selection, nil +} + +func validateNamedImportTarget(access *filesystemAccess, candidate selectedImportCandidate, required bool) (bool, *Diagnostic) { + if !access.contains(candidate.accessPath) { + return false, importSelectionDiagnostic(candidate.site, CodeImportOutsideRoot, "import target is outside the configuration root") + } + canonical, err := access.canonical(candidate.accessPath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + if required { + return false, importSelectionDiagnostic(candidate.site, CodeImportMissing, "required import target does not exist") + } + return true, nil + } + if errors.Is(err, fs.ErrPermission) { + return false, importSelectionDiagnostic(candidate.site, CodeImportInvalid, "import target cannot be accessed") + } + return false, importSelectionDiagnostic(candidate.site, CodeImportInvalid, "import target could not be canonicalized") + } + if !access.contains(canonical) { + return false, importSelectionDiagnostic(candidate.site, CodeImportOutsideRoot, "import target resolves outside the configuration root") + } + info, err := access.stat(canonical) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + if required { + return false, importSelectionDiagnostic(candidate.site, CodeImportMissing, "required import target does not exist") + } + return true, nil + } + if errors.Is(err, fs.ErrPermission) { + return false, importSelectionDiagnostic(candidate.site, CodeImportInvalid, "import target cannot be accessed") + } + return false, importSelectionDiagnostic(candidate.site, CodeImportInvalid, "import target could not be inspected") + } + if !info.Mode().IsRegular() { + return false, importSelectionDiagnostic(candidate.site, CodeSourceNotRegular, "source is not a regular file") + } + return false, nil +} + +func excludedBasename(name string, patterns []string) bool { + for _, pattern := range patterns { + matched, err := path.Match(pattern, name) + if err == nil && matched { + return true + } + } + return false +} + +func importSelectionDiagnostic(site importFieldSite, code Code, message string) *Diagnostic { + location := site.location + return &Diagnostic{ + Code: code, Severity: SeverityError, Path: site.path.clone(), + Message: message, Primary: &location, + } +} diff --git a/internal/engine/import_selection_test.go b/internal/engine/import_selection_test.go new file mode 100644 index 0000000..8ef8a46 --- /dev/null +++ b/internal/engine/import_selection_test.go @@ -0,0 +1,257 @@ +package engine + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestSelectNamedImportsHandlesPartialAndEmptySelections(t *testing.T) { + t.Parallel() + + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "present.yaml"), []byte("value: present\n"), 0o600); err != nil { + t.Fatal(err) + } + access, err := openHostAccess(root) + if err != nil { + t.Fatal(err) + } + defer access.close() + + specs, diagnostics := parseImportSpecsYAML(t, " - path: [missing.yaml, present.yaml]\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + selection, diagnostics := selectNamedImportCandidates(access, root, specs[0], resolvedImportPolicy{}) + if len(diagnostics) != 0 { + t.Fatalf("optional partial selection diagnostics = %#v", diagnostics) + } + if selection.outcome != importSelectionHasCandidates || len(selection.candidates) != 1 || filepath.Base(selection.candidates[0].path) != "present.yaml" { + t.Fatalf("partial selection = %#v", selection) + } + + requiredSpecs, diagnostics := parseImportSpecsYAML(t, " - path: [missing.yaml, present.yaml]\n required: true\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + _, diagnostics = selectNamedImportCandidates(access, root, requiredSpecs[0], resolvedImportPolicy{required: true}) + if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportMissing { + t.Fatalf("required partial selection diagnostics = %#v", diagnostics) + } + + emptySpecs, diagnostics := parseImportSpecsYAML(t, " - path: []\n required: true\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + empty, diagnostics := selectNamedImportCandidates(access, root, emptySpecs[0], resolvedImportPolicy{required: true}) + if len(diagnostics) != 0 || empty.outcome != importSelectionNone || len(empty.candidates) != 0 { + t.Fatalf("required empty path selection, diagnostics = %#v, %#v", empty, diagnostics) + } +} + +func TestSelectNamedImportsChecksRequiredTargetsBeforeExclusions(t *testing.T) { + t.Parallel() + + root := t.TempDir() + access, err := openHostAccess(root) + if err != nil { + t.Fatal(err) + } + defer access.close() + + specs, diagnostics := parseImportSpecsYAML(t, " - path: missing.yaml\n required: true\n exclude: '*.yaml'\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + policy, diagnostics := resolveImportPolicy(specs[0], importDefaults{}) + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + _, diagnostics = selectNamedImportCandidates(access, root, specs[0], policy) + if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportMissing || diagnostics[0].Path.String() != "/configkit/imports/0/path" { + t.Fatalf("required excluded missing target diagnostics = %#v", diagnostics) + } +} + +func TestSelectNamedImportsDeduplicatesLexicalPathsButRetainsAliases(t *testing.T) { + t.Parallel() + + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "real.yaml"), []byte("value: real\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Symlink("real.yaml", filepath.Join(root, "alias.yaml")); err != nil { + t.Skipf("symlinks are unavailable: %v", err) + } + access, err := openHostAccess(root) + if err != nil { + t.Fatal(err) + } + defer access.close() + + specs, diagnostics := parseImportSpecsYAML(t, " - path: [real.yaml, nested/../real.yaml, alias.yaml]\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + selection, diagnostics := selectNamedImportCandidates(access, root, specs[0], resolvedImportPolicy{}) + if len(diagnostics) != 0 { + t.Fatalf("selection diagnostics = %#v", diagnostics) + } + if len(selection.candidates) != 2 { + t.Fatalf("candidates = %#v, want two alias spellings", selection.candidates) + } + if filepath.Base(selection.candidates[0].path) != "alias.yaml" || filepath.Base(selection.candidates[1].path) != "real.yaml" { + t.Fatalf("candidate order = %#v", selection.candidates) + } +} + +func TestSelectNamedImportsDoesNotOpenExcludedContentOrApplySearchControls(t *testing.T) { + t.Parallel() + + filesystem := &statOnlyImportFilesystem{files: map[string]bool{"secret.yaml": true, ".private.yaml": true}} + access := newNativeAccess(filesystem, ".", &namespaceToken{}) + specs, diagnostics := parseImportSpecsYAML(t, " - path: secret.yaml\n required: true\n exclude: '*.yaml'\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + policy, diagnostics := resolveImportPolicy(specs[0], importDefaults{}) + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + selection, diagnostics := selectNamedImportCandidates(access, ".", specs[0], policy) + if len(diagnostics) != 0 || selection.outcome != importSelectionNone { + t.Fatalf("excluded selection, diagnostics = %#v, %#v", selection, diagnostics) + } + if filesystem.openCalls != 0 || filesystem.statCalls == 0 { + t.Fatalf("excluded target access = %d opens, %d stats", filesystem.openCalls, filesystem.statCalls) + } + + private, diagnostics := parseImportSpecsYAML(t, " - path: .private.yaml\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + hidden := false + privatePolicy, policyDiagnostics := resolveImportPolicy(private[0], importDefaults{hidden: &hidden}) + if len(policyDiagnostics) != 0 || !privatePolicy.hidden { + t.Fatalf("path selector accepted search-only defaults: %#v, %#v", privatePolicy, policyDiagnostics) + } + selection, diagnostics = selectNamedImportCandidates(access, ".", private[0], privatePolicy) + if len(diagnostics) != 0 || len(selection.candidates) != 1 || selection.candidates[0].path != ".private.yaml" { + t.Fatalf("explicit hidden path was filtered: %#v, %#v", selection, diagnostics) + } + if filesystem.openCalls != 0 { + t.Fatalf("named selection opened content %d times", filesystem.openCalls) + } +} + +func TestSelectNamedImportsChecksNormalizedPathsLexically(t *testing.T) { + t.Parallel() + + filesystem := &statOnlyImportFilesystem{files: map[string]bool{"a.yaml": true, "z.yaml": true}} + access := newNativeAccess(filesystem, ".", &namespaceToken{}) + specs, diagnostics := parseImportSpecsYAML(t, " - path: [z.yaml, a.yaml]\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + selection, diagnostics := selectNamedImportCandidates(access, ".", specs[0], resolvedImportPolicy{}) + if len(diagnostics) != 0 || len(selection.candidates) != 2 { + t.Fatalf("selection, diagnostics = %#v, %#v", selection, diagnostics) + } + if strings.Join(filesystem.statPaths, ",") != "a.yaml,a.yaml,z.yaml,z.yaml" { + t.Fatalf("target stat order = %#v", filesystem.statPaths) + } +} + +func TestSelectNamedImportsDoesNotTreatInvalidTargetsAsAbsence(t *testing.T) { + t.Parallel() + + root := t.TempDir() + if err := os.Mkdir(filepath.Join(root, "directory"), 0o700); err != nil { + t.Fatal(err) + } + outside := filepath.Join(t.TempDir(), "outside.yaml") + if err := os.WriteFile(outside, []byte("value: outside\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Symlink(outside, filepath.Join(root, "escape.yaml")); err != nil { + t.Skipf("symlinks are unavailable: %v", err) + } + access, err := openHostAccess(root) + if err != nil { + t.Fatal(err) + } + defer access.close() + + for _, test := range []struct { + name, value string + code Code + }{ + {name: "nonregular target", value: "directory", code: CodeSourceNotRegular}, + {name: "outside symlink", value: "escape.yaml", code: CodeImportOutsideRoot}, + } { + t.Run(test.name, func(t *testing.T) { + specs, diagnostics := parseImportSpecsYAML(t, " - path: "+test.value+"\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + _, diagnostics = selectNamedImportCandidates(access, root, specs[0], resolvedImportPolicy{}) + if len(diagnostics) != 1 || diagnostics[0].Code != test.code { + t.Fatalf("diagnostics = %#v, want %s", diagnostics, test.code) + } + }) + } +} + +func TestSelectNamedImportsDoesNotTreatPermissionErrorsAsAbsence(t *testing.T) { + t.Parallel() + + filesystem := &statOnlyImportFilesystem{statErrors: map[string]error{"denied.yaml": fs.ErrPermission}} + access := newNativeAccess(filesystem, ".", &namespaceToken{}) + specs, diagnostics := parseImportSpecsYAML(t, " - path: denied.yaml\n") + if len(diagnostics) != 0 { + t.Fatal(diagnostics) + } + _, diagnostics = selectNamedImportCandidates(access, ".", specs[0], resolvedImportPolicy{}) + if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportInvalid { + t.Fatalf("optional permission failure diagnostics = %#v", diagnostics) + } +} + +type statOnlyImportFilesystem struct { + files map[string]bool + statErrors map[string]error + openCalls int + statCalls int + statPaths []string +} + +func (filesystem *statOnlyImportFilesystem) Open(name string) (fs.File, error) { + filesystem.openCalls++ + return nil, fs.ErrPermission +} + +func (filesystem *statOnlyImportFilesystem) Stat(name string) (fs.FileInfo, error) { + filesystem.statCalls++ + filesystem.statPaths = append(filesystem.statPaths, name) + if err := filesystem.statErrors[name]; err != nil { + return nil, err + } + if !filesystem.files[name] { + return nil, fs.ErrNotExist + } + return importSelectionFileInfo{name: pathBaseForTest(name)}, nil +} + +type importSelectionFileInfo struct{ name string } + +func (info importSelectionFileInfo) Name() string { return info.name } +func (importSelectionFileInfo) Size() int64 { return 1 } +func (importSelectionFileInfo) Mode() fs.FileMode { return 0o600 } +func (importSelectionFileInfo) ModTime() time.Time { return time.Time{} } +func (importSelectionFileInfo) IsDir() bool { return false } +func (importSelectionFileInfo) Sys() any { return nil } +func pathBaseForTest(name string) string { return strings.TrimPrefix(name, "./") } diff --git a/internal/engine/import_spec.go b/internal/engine/import_spec.go index e776987..cfc28a7 100644 --- a/internal/engine/import_spec.go +++ b/internal/engine/import_spec.go @@ -25,6 +25,7 @@ type importDeclarationSpec struct { location Location selector importSelector paths []string + pathSites []importFieldSite glob string globSegments []string required *bool @@ -285,6 +286,8 @@ func parseImportDeclarationSpec(node *document.Node, declarationPath Path) (impo 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.pathSites = append(spec.pathSites, importFieldSite{path: badPath.clone(), location: publicLocation(badNode.Location)}) } spec.paths = values case importSelectorGlob: