From eb31377e43fb26a6726b741978db83453fcd7bdd Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 15 Sep 2026 00:33:04 +0200 Subject: a rule's duplicate check failure shortens the path too --- internal/engine/facts.go | 38 +++++++++++++++++++++++++++++-------- internal/engine/match_test.go | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 8 deletions(-) (limited to 'internal') diff --git a/internal/engine/facts.go b/internal/engine/facts.go index 89bd71b..641f1ef 100644 --- a/internal/engine/facts.go +++ b/internal/engine/facts.go @@ -5,6 +5,7 @@ package engine import ( "context" "errors" + "fmt" "io/fs" "path/filepath" "sort" @@ -83,18 +84,39 @@ func (run *matchRun) drainDupErrors() { // PATH: cause", the path abbreviated like every other one shown, and an OS // error's cause without the raw path it would repeat. func dupWarning(err error) string { + return "duplicate: " + dupCause(err).Error() +} + +// dupCause is a duplicate check's error as "PATH: cause", the path +// shortened with xdg.Abbrev and not repeated inside the cause - a candidate +// that could not be hashed, or an OS error on the file itself (plan 11 +// review L7). It still unwraps to err. +func dupCause(err error) error { + path, cause := "", err var ce dup.CandidateError - if !errors.As(err, &ce) { - return "duplicate: " + err.Error() - } - cause := ce.Err.Error() var pe *fs.PathError - if errors.As(ce.Err, &pe) { - cause = pe.Op + ": " + pe.Err.Error() + switch { + case errors.As(err, &ce): + path, cause = ce.Path, ce.Err + if errors.As(ce.Err, &pe) { + cause = fmt.Errorf("%s: %w", pe.Op, pe.Err) + } + case errors.As(err, &pe): + path, cause = pe.Path, fmt.Errorf("%s: %w", pe.Op, pe.Err) + default: + return err } - return "duplicate: " + xdg.Abbrev(ce.Path) + ": " + cause + return shortenedErr{msg: xdg.Abbrev(path) + ": " + cause.Error(), err: err} +} + +type shortenedErr struct { + msg string + err error } +func (e shortenedErr) Error() string { return e.msg } +func (e shortenedErr) Unwrap() error { return e.err } + // dupIndex returns the shared *dup.Index for the resolved, sorted extra // directories named by key, building it exactly once across every // concurrent caller that asks for the same key. @@ -263,7 +285,7 @@ func (f *facts) Duplicate(dirs []string) (string, bool, error) { idx := f.run.dupIndex(key, sorted) orig, isDup, err := idx.Lookup(f.file.Path) if err != nil { - return "", false, err + return "", false, dupCause(err) } if !isDup { return "", false, nil diff --git a/internal/engine/match_test.go b/internal/engine/match_test.go index 93311df..63bb54a 100644 --- a/internal/engine/match_test.go +++ b/internal/engine/match_test.go @@ -416,3 +416,47 @@ func TestDuplicateWarningsShareOneFormat(t *testing.T) { } } } + +// TestRuleDuplicateWarningShortensThePath: a duplicate check that fails on +// the file itself is reported on the rule with the path shortened and not +// repeated raw inside the cause, like the directory-level warnings (plan 11 +// review L7). +func TestRuleDuplicateWarningShortensThePath(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("root reads a chmod 000 file") + } + h := sandbox(t) + dl := filepath.Join(h, "dl") + os.MkdirAll(dl, 0o755) + old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + for _, n := range []string{"a.txt", "b.txt"} { + p := filepath.Join(dl, n) + os.WriteFile(p, []byte("same size"), 0o644) + os.Chtimes(p, old, old) + } + os.Chmod(filepath.Join(dl, "a.txt"), 0) + t.Cleanup(func() { os.Chmod(filepath.Join(dl, "a.txt"), 0o644) }) + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": "(path \"~/dl\")\n(rule \"dups\" (when (duplicate)) (move \"Dupes\"))\n"}) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + r, err := e.Match(context.Background(), e.Dirs[0]) + if err != nil { + t.Fatal(err) + } + found := false + for _, fm := range append(append([]FileMatch{}, r.Matched...), r.Unmatched...) { + for _, w := range fm.Warnings { + if strings.Contains(w, h) { + t.Errorf("%s: warning repeats the raw path: %q", fm.File.Rel, w) + } + if w == "dups: duplicate check failed: ~/dl/a.txt: open: permission denied" { + found = true + } + } + } + if !found { + t.Errorf("no shortened rule warning for a.txt: %+v %+v", r.Matched, r.Unmatched) + } +} -- cgit v1.3