Resolve native imports beneath nested roots

This commit is contained in:
2026-10-20 00:23:55 +00:00
parent 38fb315415
commit 75da54150f
3 changed files with 63 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
# Import Discovery and Scheduling Review Findings # 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 Reviewed implementation: `aa93aed..15943e2`, ending at
`15943e233ca3cd1286c03c0e7085871db344cd4b`, against the accepted `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 full everyday check passed, including existing default/enabled source-version
and assigned-index ownership regressions. 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), Owner: [resolveNativeImportPath](../../internal/engine/filesystem_links.go),
especially lines 170–181. 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 with ordinary entries and supported symlinks. Do not require consumers to flatten
their roots or remove optional link-inspection capability. 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 ## 4. [ ] P2 — Distinguish finite symlink reuse from actual cycles
Owners: [resolveHostImportPath and resolveNativeImportPath](../../internal/engine/filesystem_links.go), Owners: [resolveHostImportPath and resolveNativeImportPath](../../internal/engine/filesystem_links.go),

View File

@@ -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)
}
}
}

View File

@@ -177,7 +177,9 @@ func resolveNativeImportPath(access *filesystemAccess, filesystem fs.ReadLinkFS,
if candidate == "" { if candidate == "" {
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 return "", errImportPathOutsideRoot
} }
info, err := filesystem.Lstat(candidate) info, err := filesystem.Lstat(candidate)