diff --git a/TODO.md b/TODO.md index e0fef95..da10f09 100644 --- a/TODO.md +++ b/TODO.md @@ -24,6 +24,9 @@ today) with lint, fmt, fmt-check, check, and hooks targets. # Completed Steps +* 2026-10-06: a path that `--exclude` or `--exclude-dotfiles` excludes + is skipped even when it cannot be read, so an excluded directory + that cannot be listed no longer fails the run * 2026-10-06: `check --continue` keeps going past a file or directory it cannot read: it counts it as failed, prints the error and the path on stderr, and checks the rest of the tree diff --git a/attrsum.go b/attrsum.go index 5cfd6d9..7ec7499 100644 --- a/attrsum.go +++ b/attrsum.go @@ -613,6 +613,12 @@ func countFiles(opts *options, root string, cont bool) (int64, error) { root = filepath.Clean(root) err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error { + // An excluded path is skipped whatever went wrong reading it. + rel, _ := filepath.Rel(root, p) + if err != nil && shouldExclude(opts, rel) { + return nil + } + if err != nil && cont { return nil } @@ -627,7 +633,6 @@ func countFiles(opts *options, root string, cont bool) (int64, error) { return nil } - rel, _ := filepath.Rel(root, p) if shouldExclude(opts, rel) { if info.IsDir() { return filepath.SkipDir @@ -699,6 +704,12 @@ func walkAndProcess( root = filepath.Clean(root) return filepath.Walk(root, func(p string, info os.FileInfo, err error) error { + // An excluded path is skipped whatever went wrong reading it. + rel, _ := filepath.Rel(root, p) + if err != nil && shouldExclude(opts, rel) { + return nil + } + if err != nil { return unreadable(cont, stats, err) } diff --git a/attrsum_test.go b/attrsum_test.go index 803acb7..c1e5db0 100644 --- a/attrsum_test.go +++ b/attrsum_test.go @@ -214,6 +214,54 @@ func TestExcludeDotfilesAndPatterns(t *testing.T) { } } +func TestExcludeUnreadableDir(t *testing.T) { + t.Parallel() + + opts := &options{excludePatterns: []string{"locked"}} + dir := t.TempDir() + skipIfNoXattr(t, dir) + + keep := writeFile(t, dir, "keep.txt", "keep") + hidden := writeFile(t, dir, "locked/a.txt", "hidden") + + // The excluded directory cannot be listed. + sub := filepath.Join(dir, "locked") + + err := os.Chmod(sub, noPerm) + if err != nil { + t.Fatalf("chmod dir: %v", err) + } + + defer func() { _ = os.Chmod(sub, dirPerm) }() + + err = processSumAdd(opts, dir, newTestStats(), nil) + if err != nil { + t.Fatalf("add: %v", err) + } + + _, err = xattr.Get(keep, checksumKey) + if err != nil { + t.Fatalf("expected xattr on keep.txt: %v", err) + } + + // Without --quiet, runCheck counts the files for the progress bar + // before it checks any, so this also covers the count. + err = runCheck(opts, []string{dir}, false) + if err != nil { + t.Fatalf("check: %v", err) + } + + err = os.Chmod(sub, dirPerm) + if err != nil { + t.Fatalf("chmod dir back: %v", err) + } + + _, err = xattr.Get(hidden, checksumKey) + if err == nil { + t.Fatalf("locked/a.txt should have been excluded") + } +} + func TestSkipBrokenSymlink(t *testing.T) { t.Parallel()