aboutsummaryrefslogtreecommitdiff
path: root/internal/dup/dup.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/dup/dup.go')
-rw-r--r--internal/dup/dup.go50
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
}