1 Commits
Author SHA1 Message Date
sneak c6066afa6e Resolve scan operands to their real paths (closes #144)
check / check (push) Canceled after 0s
scan now resolves every operand with its symlinks followed before the
walk, and uses that real path for pruning, walking, records and
warnings. A relative operand is joined to the working directory as the
kernel reports it, so a ".." after a symlink counts from its target, as
with realpath. A symlink operand is scanned at its target instead of
skipped. An operand given through a symlink below another operand, to a
directory outside it, is walked on its own instead of dropped with its
records deleted. An operand that cannot be resolved is a fatal error
naming the operand as given. The walk itself still follows no symlink.

Model: opus-5-5
2026-11-06 18:27:42 +00:00
4 changed files with 33 additions and 49 deletions

No files matched your search

+4 -4
View File
@@ -364,10 +364,8 @@ default. An operand may be a directory or a regular file.
- An operand that repeats another is dropped before the walk. Two operands
repeat each other when their real paths are the same.
- An operand under another operand is dropped the same way, unless one of the
cases below applies. No path is then walked twice. Whether one operand is
under another is judged on their real paths. With `/srv/link` a symlink to a
directory outside `/srv`, `scan /srv /srv/link/sub` walks `sub` on its own,
because the real path of `sub` is not under `/srv`.
cases below applies. This too is judged on real paths. No path is then walked
twice.
- An operand under another is kept, and walked on its own, when the inner
operand or a directory between the two is one of these:
- a default exclude (see "Rules for the walk");
@@ -379,6 +377,8 @@ default. An operand may be a directory or a regular file.
such a directory, so the inner operand is walked on its own.
- No other case keeps an operand under another.
- With `/srv/link` a symlink to a directory outside `/srv`,
`scan /srv /srv/link/sub` walks `sub` on its own, at its real path.
- An operand that is neither a regular file nor a directory is not scanned. Such
an operand is, for example, a symlink to a device node. `scan` prints a
warning naming the real path and what it is. It counts the operand as skipped
+1 -1
View File
@@ -286,7 +286,7 @@ func TestRunScanTempDirOperand(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)
dir := realTempDir(t)
dir := t.TempDir()
if !underAnyRoot(dir, defaultExcludes(runtime.GOOS)) {
t.Skip(dir + " is not inside a default exclude")
}
+27 -43
View File
@@ -799,7 +799,7 @@ func TestRunVersionWriteFailureIsFatal(t *testing.T) {
func scanFixture(t *testing.T) []string {
t.Helper()
dir := realTempDir(t)
dir := t.TempDir()
dupes := []string{
writeFile(t, dir, "one/a.bin", pattern(1, 300)),
@@ -869,7 +869,7 @@ func TestRunScanSucceedsDespiteWarnings(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)
dir := realTempDir(t)
dir := t.TempDir()
writeFile(t, dir, "a.bin", pattern(1, 300))
// Same size as a.bin, so the scan reads it, and the read fails.
@@ -921,6 +921,15 @@ func symlink(t *testing.T, target, link string) {
}
}
// assertSummary checks that stderr holds the scan summary line want.
func assertSummary(t *testing.T, stderr, want string) {
t.Helper()
if !strings.Contains(stderr, want) {
t.Errorf("stderr = %q, want %q", stderr, want)
}
}
// TestRunScanFollowsSymlinkOperand scans link, a symlink to target.
// The operand is scanned at its real path, and its file is recorded
// there.
@@ -934,13 +943,8 @@ func TestRunScanFollowsSymlinkOperand(t *testing.T) {
link := filepath.Join(dir, "link")
symlink(t, filepath.Join(dir, "target"), link)
stderr := scanOK(t, link)
summary := "scan: 1 files seen (1 added, 0 updated, 0 removed, " +
"0 unchanged), 0 skipped\n"
if !strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q", stderr, summary)
}
assertSummary(t, scanOK(t, link), "scan: 1 files seen (1 added, "+
"0 updated, 0 removed, 0 unchanged), 0 skipped\n")
if got := storedPaths(t, path); !slices.Equal(got, []string{f}) {
t.Errorf("records = %q, want %q", got, []string{f})
@@ -960,13 +964,9 @@ func TestRunScanWalksOperandUnderSymlinkOperand(t *testing.T) {
link := filepath.Join(dir, "link")
symlink(t, filepath.Join(dir, "target"), link)
stderr := scanOK(t, link, filepath.Join(link, "sub"))
summary := "scan: 1 files seen (1 added, 0 updated, 0 removed, " +
"0 unchanged), 0 skipped\n"
if !strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q", stderr, summary)
}
assertSummary(t, scanOK(t, link, filepath.Join(link, "sub")),
"scan: 1 files seen (1 added, 0 updated, 0 removed, "+
"0 unchanged), 0 skipped\n")
if got := storedPaths(t, path); !slices.Equal(got, []string{f}) {
t.Errorf("records = %q, want %q", got, []string{f})
@@ -993,12 +993,8 @@ func TestRunScanWalksSymlinkedOperandBelowOperand(t *testing.T) {
scanOK(t, filepath.Join(outside, "sub"))
stderr := scanOK(t, dir, filepath.Join(dir, "link", "sub"))
summary := "scan: 2 files seen (1 added, 0 updated, 0 removed, " +
"1 unchanged), 0 skipped\n"
if !strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q", stderr, summary)
}
assertSummary(t, stderr, "scan: 2 files seen (1 added, 0 updated, "+
"0 removed, 1 unchanged), 0 skipped\n")
want := []string{a, f}
slices.Sort(want)
@@ -1021,13 +1017,8 @@ func TestRunScanDropsSymlinkOperandInsideOperand(t *testing.T) {
link := filepath.Join(realTempDir(t), "link")
symlink(t, filepath.Join(dir, "real"), link)
stderr := scanOK(t, dir, link)
summary := "scan: 1 files seen (1 added, 0 updated, 0 removed, " +
"0 unchanged), 0 skipped\n"
if !strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q", stderr, summary)
}
assertSummary(t, scanOK(t, dir, link), "scan: 1 files seen (1 added, "+
"0 updated, 0 removed, 0 unchanged), 0 skipped\n")
if got := storedPaths(t, path); !slices.Equal(got, []string{f}) {
t.Errorf("records = %q, want %q", got, []string{f})
@@ -1160,15 +1151,12 @@ func TestRunScanSkipsSymlinkToFIFOOperand(t *testing.T) {
t.Errorf("stderr = %q, want %q", stderr, warning)
}
summary := "scan: 0 files seen (0 added, 0 updated, 0 removed, " +
"0 unchanged), 1 skipped\n"
if !strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q", stderr, summary)
}
assertSummary(t, stderr, "scan: 0 files seen (0 added, 0 updated, "+
"0 removed, 0 unchanged), 1 skipped\n")
}
func TestRunScanSkipsZFSOperand(t *testing.T) {
zfs := filepath.Join(realTempDir(t), ".zfs")
zfs := filepath.Join(t.TempDir(), ".zfs")
f := writeFile(t, zfs, "snapshot/hourly/f", pattern(1, 10))
// No scan records a file inside a .zfs directory, so the record
@@ -1219,7 +1207,7 @@ func TestRunScanSkipsOperandInsideZFS(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)
dir := realTempDir(t)
dir := t.TempDir()
hourly := filepath.Join(dir, ".zfs", "snapshot", "hourly")
writeFile(t, hourly, "f", pattern(1, 10))
f := writeFile(t, dir, "f", pattern(2, 10))
@@ -1299,13 +1287,9 @@ func TestRunScanWalksOperandThroughSymlink(t *testing.T) {
data := filepath.Join(dir, "data")
symlink(t, filepath.Join(dir, "pool", "data"), data)
stderr := scanOK(t, filepath.Join(data, "daily"))
summary := "scan: 1 files seen (1 added, 0 updated, 0 removed, " +
"0 unchanged), 0 skipped\n"
if !strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q", stderr, summary)
}
assertSummary(t, scanOK(t, filepath.Join(data, "daily")),
"scan: 1 files seen (1 added, 0 updated, 0 removed, "+
"0 unchanged), 0 skipped\n")
if got := storedPaths(t, path); !slices.Equal(got, []string{f}) {
t.Errorf("records = %q, want %q", got, []string{f})
+1 -1
View File
@@ -1094,7 +1094,7 @@ func TestScanOneFilesystemWalksNestedFileOperand(t *testing.T) {
func buildSmokeTree(t *testing.T) string {
t.Helper()
dir := realTempDir(t)
dir := t.TempDir()
one := pattern(10, 2000)
f1 := pattern(30, 3000)
f2 := pattern(40, 100)