Resolve scan operands to their real paths (closes #144)
check / check (push) Canceled after 0s

scan now resolves each operand to its real path, every symlink followed, before the walk, as sneak ruled. Files are recorded and warnings printed under that path. With /srv/link a symlink to a directory outside /srv, scan /srv /srv/link/sub now walks sub on its own and keeps its records, where before it dropped sub and deleted them. A symlink operand is scanned at its target. A dangling symlink is a fatal error, exit 1. A .. after a symlink counts from the symlink's target, as realpath does. The .zfs rule applies to the real path. The walk still follows no symlink it finds.

Judgement call: records an earlier scan stored under a path through a symlink are not migrated; there is no installed base.

Model: opus-5-5
This commit was merged in pull request #155.
This commit is contained in:
clawbot committed 2026-11-06 19:56:33 +01:00
1 parent f52bbe5d95
commit 24b0fc486d
7 files changed
+396 -164

No files matched your search

+27 -30
View File
@@ -209,29 +209,30 @@ func interrupted(walked int) error {
return errInterrupted
}
// resolveRoots converts each PATH operand to an absolute, lexically
// cleaned path (symlinks are not resolved) and verifies that it
// exists. Database records are keyed by absolute path, so scan results
// must not depend on the working directory.
// resolveRoots converts each PATH operand to its real path: absolute,
// with every symlink in it followed. Database records are keyed by
// that path, so scan results depend neither on the working directory
// nor on the symlinks an operand was given through. An operand that
// cannot be resolved, because it or a symlink's target does not exist
// or cannot be read, is a fatal error before any scanning.
func resolveRoots(roots []string) ([]string, error) {
abs := make([]string, 0, len(roots))
resolved := make([]string, 0, len(roots))
for _, root := range roots {
a, err := filepath.Abs(root)
a, err := absolutePath(root)
if err != nil {
return nil, fmt.Errorf("resolve %s: %w", root, err)
}
// A nonexistent operand is a fatal error before any scanning.
_, err = os.Lstat(a)
r, err := filepath.EvalSymlinks(a)
if err != nil {
return nil, err
return nil, fmt.Errorf("resolve %s: %w", root, err)
}
abs = append(abs, a)
resolved = append(resolved, r)
}
return abs, nil
return resolved, nil
}
// pruneRoots drops operands already covered by another operand:
@@ -417,14 +418,16 @@ func (s *scanState) runPhases(ctx context.Context, roots []string,
// walkableRoots returns the operands the walk can start from: regular
// files, and directories not named .zfs, none of them inside a
// directory named .zfs. Every other operand is warned about, counted as
// skipped, and dropped. A dropped operand is no longer a root, so the
// records stored beneath it are treated as if it had not been given:
// those under no other root count as outside the roots and are not
// deleted as unverified. An operand on a filesystem in rules that is not
// persistent is dropped the same way, with a line saying so that is not
// a warning, and is not counted as skipped. An operand that fails lstat
// here is kept, and the walk warns about it unless it no longer exists.
// directory named .zfs. The operands are real paths from resolveRoots,
// so these checks apply to what a symlink operand points at. Every
// other operand is warned about, counted as skipped, and dropped. A
// dropped operand is no longer a root, so the records stored beneath it
// are treated as if it had not been given: those under no other root
// count as outside the roots and are not deleted as unverified. An
// operand on a filesystem in rules that is not persistent is dropped
// the same way, with a line saying so that is not a warning, and is not
// counted as skipped. An operand that fails lstat here is kept, and the
// walk warns about it unless it no longer exists.
func (s *scanState) walkableRoots(roots []string, rules walkRules) []string {
kept := make([]string, 0, len(roots))
@@ -894,19 +897,13 @@ func sendEvent(ctx context.Context, events chan<- walkEvent,
// operandWarning returns the one-line warning for an operand the walk
// does not start from, naming the path and what it is, or "" for one it
// does: a regular file, or a directory not named .zfs, that is not
// inside a directory named .zfs, neither as given nor with its symlinks
// resolved. Symlinks are never followed, including as operands.
// inside a directory named .zfs. resolveRoots has already followed the
// operand's symlinks, so a symlink here is an operand replaced by one
// after it was resolved, and is not followed.
func operandWarning(root string, fi fs.FileInfo) string {
// The walk never enters a .zfs directory, so it starts from nothing
// inside one either, not even through a symlink. An operand whose
// symlinks cannot be resolved is checked as given.
resolved, err := filepath.EvalSymlinks(root)
if err != nil {
resolved = root
}
if slices.Contains(strings.Split(filepath.Dir(root), "/"), ".zfs") ||
slices.Contains(strings.Split(filepath.Dir(resolved), "/"), ".zfs") {
// inside one either.
if slices.Contains(strings.Split(filepath.Dir(root), "/"), ".zfs") {
return fmt.Sprintf("walk %s: skipping operand inside a .zfs directory",
root)
}