From 24a84671ace373ae331fa83a1ff484990f4dff0e Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 12 Sep 2026 12:58:14 +0200 Subject: krino: planning — chains, placeholders, conflicts, JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/dup/dup.go | 41 +++++++++++++++++++++++++++++++++++++++++ internal/dup/dup_test.go | 23 +++++++++++++++++++++++ 2 files changed, 64 insertions(+) (limited to 'internal/dup') diff --git a/internal/dup/dup.go b/internal/dup/dup.go index 502ef31..e3c7424 100644 --- a/internal/dup/dup.go +++ b/internal/dup/dup.go @@ -381,6 +381,47 @@ func readAt(f *os.File, off int64) ([]byte, error) { return buf[:n], nil } +// SameContent reports whether a and b hold identical content: a stat and +// size check first, then the same partial/full hash comparison Lookup uses +// for scanned candidates. Neither file needs to have been scanned or +// indexed; this is the one place content identity is decided, so callers +// outside this package must not hash a second way. +func SameContent(a, b string) (bool, error) { + ai, err := os.Stat(a) + if err != nil { + return false, err + } + bi, err := os.Stat(b) + if err != nil { + return false, err + } + if ai.Size() != bi.Size() { + return false, nil + } + + aPartial, err := computePartialHash(a) + if err != nil { + return false, err + } + bPartial, err := computePartialHash(b) + if err != nil { + return false, err + } + if aPartial != bPartial { + return false, nil + } + + aFull, err := computeFullHash(a) + if err != nil { + return false, err + } + bFull, err := computeFullHash(b) + if err != nil { + return false, err + } + return aFull == bFull, nil +} + // computeFullHash hashes the whole file. func computeFullHash(path string) ([sha256.Size]byte, error) { f, err := os.Open(path) diff --git a/internal/dup/dup_test.go b/internal/dup/dup_test.go index fb4e64d..5d2621c 100644 --- a/internal/dup/dup_test.go +++ b/internal/dup/dup_test.go @@ -262,6 +262,29 @@ func TestLookupFailsWhenSubjectUnreadable(t *testing.T) { } } +// TestSameContentEqualSizeDifferentContent: two files of identical size but +// different bytes must not be reported as the same content — the partial +// hash (not just the size check) has to separate them. +func TestSameContentEqualSizeDifferentContent(t *testing.T) { + d := t.TempDir() + a := filepath.Join(d, "a.bin") + b := filepath.Join(d, "b.bin") + one := []byte("acme-invoice-01") + two := []byte("acme-invoice-02") + if len(one) != len(two) { + t.Fatal("fixture bug: files must be the same size") + } + if err := os.WriteFile(a, one, 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(b, two, 0o644); err != nil { + t.Fatal(err) + } + if ok, err := SameContent(a, b); err != nil || ok { + t.Errorf("SameContent(a, b) = %v, %v; want false, nil", ok, err) + } +} + // fakeDirEntry is an fs.DirEntry whose Info() returns a canned result, for // exercising addEntry's Info()-failure handling directly (A3) — a real // filepath.WalkDir gives no hook to inject a stat failure deterministically -- cgit v1.3