AI codebase review

fix: duplicate search no longer hangs on files swapped for FIFOs

Candidates were reopened by path with os.Open after the scan. A file
replaced by a FIFO (or a symlink to one) blocked open(2) forever, and
context cancellation could not interrupt it, wedging the duplicate view
and --duplicates. Content is now opened with O_NOFOLLOW|O_NONBLOCK on
Unix; the existing regular-file check then skips it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andy Dixon
2026-10-02 10:59:28 +01:00
co-authored by Claude Opus 5.5
parent 1f9429ea74
commit 5966fdd91e
4 changed files with 76 additions and 1 deletions
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"sync/atomic"
"github.com/andydixon/mapsize/internal/inventory"
"github.com/andydixon/mapsize/internal/platform"
)
// Group is a set of files verified to have identical content.
@@ -226,7 +227,7 @@ func (f *Finder) splitHash(ctx context.Context, g []cand, workers int) []fullGro
// open opens a candidate and checks it still has the inventoried size.
func open(c cand) (*os.File, bool) {
fh, err := os.Open(c.path)
fh, err := platform.OpenContent(c.path)
if err != nil {
return nil, false
}
+51
View File
@@ -0,0 +1,51 @@
//go:build unix
package duplicate
import (
"context"
"os"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/andydixon/mapsize/internal/scan"
)
// A candidate replaced by a FIFO (or a symlink to one) after the scan must
// be skipped, not block the search forever in open(2).
func TestFindSkipsSwappedFIFO(t *testing.T) {
root := t.TempDir()
for _, n := range []string{"a", "b", "c"} {
os.WriteFile(filepath.Join(root, n), []byte("same"), 0o644)
}
s, err := scan.Start(context.Background(), root, scan.Options{Workers: 1})
if err != nil {
t.Fatal(err)
}
<-s.Done()
os.Remove(filepath.Join(root, "b"))
if err := syscall.Mkfifo(filepath.Join(root, "b"), 0o644); err != nil {
t.Skip("mkfifo:", err)
}
fifo2 := filepath.Join(t.TempDir(), "p")
syscall.Mkfifo(fifo2, 0o644)
os.Remove(filepath.Join(root, "c"))
os.Symlink(fifo2, filepath.Join(root, "c"))
done := make(chan error, 1)
var f Finder
go func() { _, err := f.Find(context.Background(), s.Tree, Options{}); done <- err }()
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("duplicate search blocked on a FIFO")
}
if p := f.Progress(); p.Skipped != 2 {
t.Fatalf("skipped=%d want 2", p.Skipped)
}
}