From 75da54150fb08dac4390d0211243d6047fb90f5e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 20 Oct 2026 00:23:55 +0000 Subject: [PATCH] Resolve native imports beneath nested roots --- docs/roadmap/remediation.md | 10 ++++-- facade_import_root_test.go | 52 +++++++++++++++++++++++++++++ internal/engine/filesystem_links.go | 4 ++- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 facade_import_root_test.go diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index aac17b2..e8ca5aa 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -1,6 +1,6 @@ # Import Discovery and Scheduling Review Findings -Status: in progress; issues 1–2 complete, four findings remain. +Status: in progress; issues 1–3 complete, three findings remain. Reviewed implementation: `aa93aed..15943e2`, ending at `15943e233ca3cd1286c03c0e7085871db344cd4b`, against the accepted @@ -92,7 +92,7 @@ items, underlays, and selected main items, and failed before the change. The full everyday check passed, including existing default/enabled source-version and assigned-index ownership regressions. -## 3. [ ] P2 — Accept valid multi-component native filesystem roots +## 3. [x] P2 — Accept valid multi-component native filesystem roots Owner: [resolveNativeImportPath](../../internal/engine/filesystem_links.go), especially lines 170–181. @@ -114,6 +114,12 @@ resolved targets. Test directory and glob discovery under multi-component roots, with ordinary entries and supported symlinks. Do not require consumers to flatten their roots or remove optional link-inspection capability. +Completed: native link resolution allows ancestor prefixes while resolving a +contained multi-component path; selected paths and resolved link targets still +undergo their existing containment checks. Public directory/glob regressions +cover ordinary and aliased roots plus escaping links. All four valid cases +failed before the fix. The full everyday check passed. + ## 4. [ ] P2 — Distinguish finite symlink reuse from actual cycles Owners: [resolveHostImportPath and resolveNativeImportPath](../../internal/engine/filesystem_links.go), diff --git a/facade_import_root_test.go b/facade_import_root_test.go new file mode 100644 index 0000000..c9113a0 --- /dev/null +++ b/facade_import_root_test.go @@ -0,0 +1,52 @@ +package configkit_test + +import ( + "errors" + "os" + "path/filepath" + "testing" + + configkit "gitea.maximumdirect.net/eric/ConfigKit" +) + +func TestFacadeNativeDiscoveryWithNestedRoot(t *testing.T) { + root := t.TempDir() + parts := filepath.Join(root, "nested/root/parts") + if err := os.MkdirAll(parts, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(parts, "value.yml"), []byte("value: 1\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Symlink("parts", filepath.Join(root, "nested/root/linked")); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + filesystem := configkit.NewFilesystem("nested", os.DirFS(root)) + context := &configkit.FilesystemContext{FS: &filesystem, Root: "nested/root", BaseDir: "nested/root"} + for _, selector := range []string{ + "dir: parts", "glob: 'parts/*.yml'", "dir: linked", "glob: 'lin*/*.yml'", + } { + t.Run(selector, func(t *testing.T) { + result, err := configkit.Load(configkit.Bytes("main", []byte("configkit:\n imports:\n - "+selector+"\n")), configkit.Options[map[string]any]{Imports: true, Filesystem: context}) + if err != nil { + t.Fatal(err) + } + if result.Config["value"] != int64(1) { + t.Fatalf("configuration = %#v", result.Config) + } + }) + } + if err := os.WriteFile(filepath.Join(root, "nested/outside.yml"), []byte("value: outside\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Symlink("../../outside.yml", filepath.Join(parts, "escape.yml")); err != nil { + t.Fatal(err) + } + for _, selector := range []string{"dir: parts", "glob: 'parts/*.yml'"} { + _, err := configkit.Load(configkit.Bytes("main", []byte("configkit:\n imports:\n - "+selector+"\n")), configkit.Options[map[string]any]{Imports: true, Filesystem: context}) + var loadError *configkit.LoadError + if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeImportOutsideRoot { + t.Fatalf("escaping %s: %v", selector, err) + } + } +} diff --git a/internal/engine/filesystem_links.go b/internal/engine/filesystem_links.go index 5b4af6e..25ff005 100644 --- a/internal/engine/filesystem_links.go +++ b/internal/engine/filesystem_links.go @@ -177,7 +177,9 @@ func resolveNativeImportPath(access *filesystemAccess, filesystem fs.ReadLinkFS, if candidate == "" { candidate = "." } - if !access.contains(candidate) { + // Ancestors of a multi-component root are resolution prefixes, not + // selected targets. Complete paths and link targets remain contained. + if !access.contains(candidate) && !strings.HasPrefix(access.rootPath, candidate+"/") { return "", errImportPathOutsideRoot } info, err := filesystem.Lstat(candidate)