diff options
Diffstat (limited to 'internal/dup/dup.go')
| -rw-r--r-- | internal/dup/dup.go | 50 |
1 files changed, 40 insertions, 10 deletions
diff --git a/internal/dup/dup.go b/internal/dup/dup.go index e3c7424..32bcfb0 100644 --- a/internal/dup/dup.go +++ b/internal/dup/dup.go @@ -174,18 +174,47 @@ func (x *Index) Lookup(path string) (original string, dup bool, err error) { return path, false, nil } orig := x.candidates[x.original(identical)].path - return orig, orig != path, nil + if orig == path { + return orig, false, nil + } + // Spec ยง5.5: two names for one file are never duplicates of each other. + // The other name may be a hardlink, or path itself indexed a second time + // under a DIR that overlaps the scanned tree. identicalTo gives every + // member of the content class the same set, so every lookup elects the + // same original, and no name for that original's file is reported as a + // duplicate: its content always keeps at least one name. Portable: + // os.SameFile, never a Stat_t.Dev/Ino read (that field's type differs + // across freebsd/openbsd, which `make ci` vets). + origInfo, err := os.Lstat(orig) + if err != nil { + return "", false, err + } + pathInfo, err := os.Lstat(path) + if err != nil { + return "", false, err + } + if os.SameFile(origInfo, pathInfo) { + return orig, false, nil + } + return orig, true, nil } // identicalTo returns the indexes in group (which all share idx's size, // idx included) whose content matches candidates[idx]: same partial hash, -// then, only for those that collide, the same full hash. idx is the file -// Lookup was asked about; a failure hashing it propagates, since Lookup can -// answer nothing without it. A failure hashing any other candidate in group -// only removes that candidate from consideration: a vanished candidate -// (errors.Is fs.ErrNotExist) is dropped silently, any other failure is -// recorded on the Index (see recordCandidateError) so the caller can warn -// about it once matching is done. +// then, only for those that collide, the same full hash. Every candidate +// with identical bytes is included, whatever its path or inode: a hardlink +// of idx, and idx's own path indexed a second time under an overlapping +// extra directory, are both members. That keeps the set the same whichever +// member Lookup was asked about, so every member elects the same original; +// Lookup, not this function, decides that a name for the elected original's +// own file is not a duplicate of it. +// +// idx is the file Lookup was asked about; a failure hashing it propagates, +// since Lookup can answer nothing without it. A failure hashing any other +// candidate in group only removes that candidate from consideration: a +// vanished candidate (errors.Is fs.ErrNotExist) is dropped silently, any +// other failure is recorded on the Index (see recordCandidateError) so the +// caller can warn about it once matching is done. func (x *Index) identicalTo(idx int, group []int) ([]int, error) { idxPartial, err := x.partialHash(x.candidates[idx].path) if err != nil { @@ -219,9 +248,10 @@ func (x *Index) identicalTo(idx int, group []int) ([]int, error) { x.recordCandidateError(x.candidates[j].path, err) continue } - if jFull == idxFull { - same = append(same, j) + if jFull != idxFull { + continue } + same = append(same, j) } return same, nil } |
