package engine import ( "io/fs" "os" "path" "path/filepath" "strings" "testing" "testing/fstest" "time" ) func TestSelectDirectoriesApplyHiddenAndFollowControls(t *testing.T) { t.Parallel() root := t.TempDir() for name, content := range map[string]string{ "direct.yaml": "value: direct\n", ".private.yaml": "value: hidden\n", ".private/nested.yaml": "value: hidden directory\n", "target/linked.yaml": "value: linked\n", "target/source.txt": "value: source\n", } { filename := filepath.Join(root, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filename, []byte(content), 0o600); err != nil { t.Fatal(err) } } if err := os.Symlink("target", filepath.Join(root, "visible-alias")); err != nil { t.Skipf("symlinks are unavailable: %v", err) } if err := os.Symlink("target/linked.yaml", filepath.Join(root, "linked-alias.txt")); err != nil { t.Skipf("symlinks are unavailable: %v", err) } if err := os.Symlink("target/source.txt", filepath.Join(root, "selected.yaml")); err != nil { t.Skipf("symlinks are unavailable: %v", err) } if err := os.Symlink("target", filepath.Join(root, ".hidden-alias")); err != nil { t.Skipf("symlinks are unavailable: %v", err) } access, err := openHostAccess(root) if err != nil { t.Fatal(err) } defer access.close() selection := selectDirectoryWithOptions(t, access, root, ` - dir: . recursive: true follow: false hidden: false pattern: '*.yaml' `) if got, want := selectedPathsRelativeTo(selection, root), []string{"direct.yaml", "target/linked.yaml"}; !equalStrings(got, want) { t.Fatalf("filtered paths = %#v, want %#v", got, want) } selection = selectDirectoryWithOptions(t, access, root, ` - dir: visible-alias follow: false pattern: '*.yaml' `) if got, want := selectedPathsRelativeTo(selection, root), []string{"visible-alias/linked.yaml"}; !equalStrings(got, want) { t.Fatalf("explicit symlink root paths = %#v, want %#v", got, want) } selection = selectDirectoryWithOptions(t, access, root, ` - dir: . recursive: true follow: true hidden: false pattern: '*.yaml' `) if got, want := selectedPathsRelativeTo(selection, root), []string{"direct.yaml", "selected.yaml", "target/linked.yaml", "visible-alias/linked.yaml"}; !equalStrings(got, want) { t.Fatalf("followed paths = %#v, want %#v", got, want) } selection = selectDirectoryWithOptions(t, access, root, ` - dir: . recursive: true follow: true hidden: true pattern: '*.yaml' `) if got, want := selectedPathsRelativeTo(selection, root), []string{ ".hidden-alias/linked.yaml", ".private.yaml", ".private/nested.yaml", "direct.yaml", "selected.yaml", "target/linked.yaml", "visible-alias/linked.yaml", }; !equalStrings(got, want) { t.Fatalf("hidden-enabled paths = %#v, want %#v", got, want) } } func TestSelectDirectoriesRejectEscapingAndCyclicLinks(t *testing.T) { t.Parallel() t.Run("escape", func(t *testing.T) { root := t.TempDir() outside := t.TempDir() if err := os.WriteFile(filepath.Join(outside, "outside.yaml"), []byte("value: outside\n"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink(outside, filepath.Join(root, "escape")); err != nil { t.Skipf("symlinks are unavailable: %v", err) } access, err := openHostAccess(root) if err != nil { t.Fatal(err) } defer access.close() _, diagnostics := selectDirectoryWithDiagnostics(t, access, root, ` - dir: . recursive: true follow: true pattern: '*.yaml' `) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportOutsideRoot { t.Fatalf("escape diagnostics = %#v, want import.outside_root", diagnostics) } selection := selectDirectoryWithOptions(t, access, root, ` - dir: . recursive: true follow: false pattern: '*.yaml' `) if len(selection.candidates) != 0 { t.Fatalf("follow:false selected an escaping symlink: %#v", selection.candidates) } }) for _, test := range []struct { name string }{ {name: "self"}, {name: "ancestor"}, } { t.Run(test.name, func(t *testing.T) { root := t.TempDir() if test.name == "self" { if err := os.Symlink(".", filepath.Join(root, "loop")); err != nil { t.Skipf("symlinks are unavailable: %v", err) } } else { if err := os.Mkdir(filepath.Join(root, "nested"), 0o700); err != nil { t.Fatal(err) } if err := os.Symlink("..", filepath.Join(root, "nested", "up")); err != nil { t.Skipf("symlinks are unavailable: %v", err) } } access, err := openHostAccess(root) if err != nil { t.Fatal(err) } defer access.close() _, diagnostics := selectDirectoryWithDiagnostics(t, access, root, ` - dir: . recursive: true follow: true `) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportCycle { t.Fatalf("cycle diagnostics = %#v, want import.cycle", diagnostics) } if diagnostics[0].Message != "directory traversal cycle detected" { t.Fatalf("cycle diagnostic exposes unexpected detail: %q", diagnostics[0].Message) } }) } } func TestSelectDirectoriesRetainSeparateNoncyclicAliases(t *testing.T) { t.Parallel() root := t.TempDir() if err := os.Mkdir(filepath.Join(root, "target"), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "target", "item.yaml"), []byte("value: item\n"), 0o600); err != nil { t.Fatal(err) } for _, alias := range []string{"first", "second"} { if err := os.Symlink("target", filepath.Join(root, alias)); err != nil { t.Skipf("symlinks are unavailable: %v", err) } } access, err := openHostAccess(root) if err != nil { t.Fatal(err) } defer access.close() selection := selectDirectoryWithOptions(t, access, root, ` - dir: . recursive: true follow: true pattern: '*.yaml' `) want := []string{"first/item.yaml", "second/item.yaml", "target/item.yaml"} if got := selectedPathsRelativeTo(selection, root); !equalStrings(got, want) { t.Fatalf("alias candidates = %#v, want %#v", got, want) } } func TestSelectDirectoriesUseNativeReadLinkCapability(t *testing.T) { t.Parallel() filesystem := &linkAwareMapFS{ files: fstest.MapFS{ "real": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, "real/item.yaml": &fstest.MapFile{Data: []byte("value: item\n")}, "alias": &fstest.MapFile{Mode: fs.ModeSymlink, Data: []byte("real")}, ".hidden": &fstest.MapFile{Mode: fs.ModeDir | 0o700}, ".hidden/secret.yaml": &fstest.MapFile{Data: []byte("value: secret\n")}, }, links: map[string]string{"alias": "real"}, } access := newNativeAccess(filesystem, ".", &namespaceToken{}) selection := selectDirectoryWithOptions(t, access, ".", ` - dir: . recursive: true follow: true hidden: false pattern: '*.yaml' `) if got, want := selectedPaths(selection), []string{"alias/item.yaml", "real/item.yaml"}; !equalStrings(got, want) { t.Fatalf("native link candidates = %#v, want %#v", got, want) } cyclic := &linkAwareMapFS{ files: fstest.MapFS{ "loop": &fstest.MapFile{Mode: fs.ModeSymlink, Data: []byte(".")}, }, links: map[string]string{"loop": "."}, } _, diagnostics := selectDirectoryWithDiagnostics(t, newNativeAccess(cyclic, ".", &namespaceToken{}), ".", ` - dir: . recursive: true follow: true `) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportCycle { t.Fatalf("native loop diagnostics = %#v, want import.cycle", diagnostics) } plain := newNativeAccess(fstest.MapFS{"plain.yaml": &fstest.MapFile{Data: []byte("value: plain\n")}}, ".", &namespaceToken{}) selection = selectDirectoryWithOptions(t, plain, ".", " - dir: .\n") if got, want := selectedPaths(selection), []string{"plain.yaml"}; !equalStrings(got, want) { t.Fatalf("plain native filesystem selection = %#v, want %#v", got, want) } } func selectDirectoryWithOptions(t *testing.T, access *filesystemAccess, base, declaration string) importSelection { t.Helper() selection, diagnostics := selectDirectoryWithDiagnostics(t, access, base, declaration) if len(diagnostics) != 0 { t.Fatalf("directory selection diagnostics = %#v", diagnostics) } return selection } func selectDirectoryWithDiagnostics(t *testing.T, access *filesystemAccess, base, declaration string) (importSelection, []Diagnostic) { t.Helper() specs, diagnostics := parseImportSpecsYAML(t, declaration) if len(diagnostics) != 0 { t.Fatalf("parse diagnostics = %#v", diagnostics) } policy, diagnostics := resolveImportPolicy(specs[0], ImportDefaults{}) if len(diagnostics) != 0 { t.Fatalf("policy diagnostics = %#v", diagnostics) } return selectDirectoryImportCandidates(access, base, specs[0], policy, defaultImportLimits(), &importAccounting{}) } func selectedPaths(selection importSelection) []string { paths := make([]string, 0, len(selection.candidates)) for _, candidate := range selection.candidates { paths = append(paths, candidate.path) } return paths } func selectedPathsRelativeTo(selection importSelection, root string) []string { paths := make([]string, 0, len(selection.candidates)) for _, candidate := range selection.candidates { relative, err := filepath.Rel(root, candidate.path) if err != nil { paths = append(paths, candidate.path) continue } paths = append(paths, filepath.ToSlash(relative)) } return paths } func equalStrings(left, right []string) bool { if len(left) != len(right) { return false } for index := range left { if left[index] != right[index] { return false } } return true } type linkAwareMapFS struct { files fstest.MapFS links map[string]string } func (f *linkAwareMapFS) Open(name string) (fs.File, error) { resolved, err := f.resolve(name) if err != nil { return nil, err } return f.files.Open(resolved) } func (f *linkAwareMapFS) ReadLink(name string) (string, error) { target, ok := f.links[name] if !ok { return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrInvalid} } return target, nil } func (f *linkAwareMapFS) Lstat(name string) (fs.FileInfo, error) { if _, ok := f.links[name]; ok { return linkMapFileInfo{name: path.Base(name)}, nil } return fs.Stat(f.files, name) } func (f *linkAwareMapFS) resolve(name string) (string, error) { if !fs.ValidPath(name) { return "", fs.ErrInvalid } resolved := "" seen := make(map[string]struct{}) for _, segment := range strings.Split(name, "/") { candidate := path.Join(resolved, segment) if target, ok := f.links[candidate]; ok { if _, exists := seen[candidate]; exists { return "", fs.ErrInvalid } seen[candidate] = struct{}{} candidate = path.Clean(path.Join(path.Dir(candidate), target)) } resolved = candidate } return resolved, nil } type linkMapFileInfo struct{ name string } func (i linkMapFileInfo) Name() string { return i.name } func (linkMapFileInfo) Size() int64 { return 0 } func (linkMapFileInfo) Mode() fs.FileMode { return fs.ModeSymlink | 0o777 } func (linkMapFileInfo) ModTime() time.Time { return time.Time{} } func (linkMapFileInfo) IsDir() bool { return false } func (linkMapFileInfo) Sys() any { return nil }