Resolve contained symlinks in import directory prefixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Import Integration Correctness Audit
|
# Import Integration Correctness Audit
|
||||||
|
|
||||||
Status: open; three findings require correction.
|
Status: in progress; finding 1 complete, findings 2 and 3 remain.
|
||||||
|
|
||||||
Reviewed tree: `91db99b` (including the six completed fixes through `5747ec4`).
|
Reviewed tree: `91db99b` (including the six completed fixes through `5747ec4`).
|
||||||
Scope: import discovery/defaults and shared dependency-ready scheduling, including
|
Scope: import discovery/defaults and shared dependency-ready scheduling, including
|
||||||
@@ -10,11 +10,11 @@ resource contexts, and maintained consumer examples. The completed design and
|
|||||||
implementation plan remain available at `5747ec4:docs/roadmap/imports.md` and
|
implementation plan remain available at `5747ec4:docs/roadmap/imports.md` and
|
||||||
`5747ec4:docs/roadmap/implementation.md`.
|
`5747ec4:docs/roadmap/implementation.md`.
|
||||||
|
|
||||||
This review changes no production code or existing contracts. Preserve the
|
The audit made no production code or contract changes. Preserve the
|
||||||
accepted syntax, defaults, scheduling, containment, and namespace decisions when
|
accepted syntax, defaults, scheduling, containment, and namespace decisions when
|
||||||
addressing these findings.
|
addressing these findings.
|
||||||
|
|
||||||
## 1. [ ] P2 — Accept contained absolute symlinks in directory-root prefixes
|
## 1. [x] P2 — Accept contained absolute symlinks in directory-root prefixes
|
||||||
|
|
||||||
Owners: [`importEntryTarget`](../../internal/engine/filesystem_links.go),
|
Owners: [`importEntryTarget`](../../internal/engine/filesystem_links.go),
|
||||||
especially the initial host `a.root.Lstat(relative)` around line 30, and
|
especially the initial host `a.root.Lstat(relative)` around line 30, and
|
||||||
@@ -53,6 +53,16 @@ solve this by prohibiting absolute symlinks or relaxing root containment. Add a
|
|||||||
focused public regression for a directory root below an absolute contained link;
|
focused public regression for a directory root below an absolute contained link;
|
||||||
reuse existing escaping-link and cycle coverage.
|
reuse existing escaping-link and cycle coverage.
|
||||||
|
|
||||||
|
Completed: host entry inspection resolves intermediate parent components through
|
||||||
|
the existing containment-aware resolver before inspecting the final entry. Native
|
||||||
|
handling and the final entry's `follow` policy are unchanged. Added a public
|
||||||
|
regression for directory roots below an absolute contained link with both
|
||||||
|
`follow` settings, including canonical source identity. Confirmed that the test
|
||||||
|
fails before the fix and passes afterward. Focused facade and engine directory,
|
||||||
|
glob, symlink, and cycle tests passed, as did `./scripts/check.sh`,
|
||||||
|
`go test -race -count=1 ./...`, and `git diff --check`. The original public
|
||||||
|
reproduction now succeeds for all selectors.
|
||||||
|
|
||||||
## 2. [ ] P2 — Describe scheduled provenance and history order consistently
|
## 2. [ ] P2 — Describe scheduled provenance and history order consistently
|
||||||
|
|
||||||
Owner: [configuration contract](../config.md), especially layered metadata
|
Owner: [configuration contract](../config.md), especially layered metadata
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package configkit_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -9,6 +10,43 @@ import (
|
|||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestFacadeDirectoryRootBelowContainedAbsoluteSymlink(t *testing.T) {
|
||||||
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parts := filepath.Join(root, "real", "parts")
|
||||||
|
if err := os.MkdirAll(parts, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
partPath := filepath.Join(parts, "part.yml")
|
||||||
|
if err := os.WriteFile(partPath, []byte("value: included\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(filepath.Join(root, "real"), filepath.Join(root, "absolute")); err != nil {
|
||||||
|
t.Skipf("symlinks unavailable: %v", err)
|
||||||
|
}
|
||||||
|
for _, follow := range []bool{true, false} {
|
||||||
|
t.Run(fmt.Sprintf("follow=%t", follow), func(t *testing.T) {
|
||||||
|
mainPath := filepath.Join(root, "main.yml")
|
||||||
|
contents := fmt.Sprintf("configkit:\n imports:\n - dir: absolute/parts\n pattern: '*.yml'\n follow: %t\n", follow)
|
||||||
|
if err := os.WriteFile(mainPath, []byte(contents), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result, err := configkit.Load[map[string]any](configkit.File(mainPath), configkit.Options[map[string]any]{Imports: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config["value"] != "included" {
|
||||||
|
t.Fatalf("configuration = %#v", result.Config)
|
||||||
|
}
|
||||||
|
if sources := result.Sources(); len(sources) != 2 || sources[1].Path != partPath {
|
||||||
|
t.Fatalf("canonical imported source = %#v", sources)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFacadeNativeDiscoveryWithNestedRoot(t *testing.T) {
|
func TestFacadeNativeDiscoveryWithNestedRoot(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
parts := filepath.Join(root, "nested/root/parts")
|
parts := filepath.Join(root, "nested/root/parts")
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ func (a *filesystemAccess) importEntryTarget(name string, entry fs.DirEntry, fol
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return importEntryTarget{}, err
|
return importEntryTarget{}, err
|
||||||
}
|
}
|
||||||
|
// Root.Lstat cannot follow absolute links in intermediate components,
|
||||||
|
// even when contained. Resolve parents first, leaving the final entry
|
||||||
|
// unexpanded so its discovered-link follow policy still applies.
|
||||||
|
if parent := filepath.Dir(relative); parent != "." {
|
||||||
|
resolvedParent, err := resolveHostImportPath(a, parent)
|
||||||
|
if err != nil {
|
||||||
|
return importEntryTarget{}, err
|
||||||
|
}
|
||||||
|
relative = filepath.Join(filepath.FromSlash(resolvedParent), filepath.Base(relative))
|
||||||
|
}
|
||||||
info, err := a.root.Lstat(relative)
|
info, err := a.root.Lstat(relative)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return importEntryTarget{}, err
|
return importEntryTarget{}, err
|
||||||
|
|||||||
Reference in New Issue
Block a user