@@ -12,11 +12,14 @@ use serde::{Deserialize, Serialize};
1212/// internal content.
1313///
1414/// **Guarantees:**
15- /// * Every inode number is unique within the scope of a single scan (files are keyed by
16- /// `(device, inode)` in the underlying [`HardlinkList`], but the reflection stores
17- /// only the inode number; entries from different filesystems with the same inode number
18- /// are an unsupported edge case in JSON round-trips).
19- /// * The internal list is always sorted by inode numbers.
15+ /// * Every `(device, inode)` pair is unique within the scope of a single scan, but inode
16+ /// numbers alone are **not** guaranteed to be unique: when scanning multiple filesystems,
17+ /// two unrelated files on different devices can share the same inode number and will each
18+ /// produce a separate entry. The reflection stores only the inode number (the JSON format
19+ /// does not carry device information), so round-tripping a multi-filesystem scan through
20+ /// JSON is an unsupported edge case.
21+ /// * The internal list is always sorted by inode numbers (and by device number as a
22+ /// tie-breaker when two entries share the same inode number).
2023///
2124/// **Equality:** `Reflection` implements `PartialEq` and `Eq` traits.
2225///
@@ -98,10 +101,17 @@ impl<Size> From<Vec<ReflectionEntry<Size>>> for Reflection<Size> {
98101
99102impl < Size > From < HardlinkList < Size > > for Reflection < Size > {
100103 fn from ( HardlinkList ( list) : HardlinkList < Size > ) -> Self {
101- list. into_iter ( )
104+ // Collect to a vec, sort by (ino, dev) for a stable, deterministic order, then
105+ // strip dev before wrapping. Sorting here (with dev still available) avoids the
106+ // nondeterminism that would arise from an unstable sort on ino alone when two
107+ // entries from different filesystems share the same inode number.
108+ let mut pairs: Vec < ( InodeKey , Value < Size > ) > = list. into_iter ( ) . collect ( ) ;
109+ pairs. sort_unstable_by_key ( |( key, _) | ( u64:: from ( key. ino ) , key. dev ) ) ;
110+ pairs
111+ . into_iter ( )
102112 . map ( |( key, value) | ReflectionEntry :: new ( key. ino , value) )
103113 . collect :: < Vec < _ > > ( )
104- . pipe ( Reflection :: from )
114+ . pipe ( Reflection )
105115 }
106116}
107117
@@ -125,8 +135,10 @@ impl<Size> TryFrom<Reflection<Size>> for HardlinkList<Size> {
125135 // Device number is unknown when loading from a reflection (e.g. JSON input);
126136 // use dev=0 as a placeholder. This means that when reloading JSON output that
127137 // was produced by scanning multiple filesystems, files from different devices
128- // sharing the same inode number will be incorrectly merged into a single entry.
129- // This is an unsupported edge case: the JSON format does not carry device info.
138+ // sharing the same inode number cannot be distinguished and therefore cannot
139+ // all be represented. Such duplicates cause a ConversionError::DuplicatedInode
140+ // and are treated as an unsupported edge case, since the JSON format does not
141+ // carry device information.
130142 let key = InodeKey { dev : 0 , ino } ;
131143 if map. insert ( key, value) . is_some ( ) {
132144 return ino. pipe ( ConversionError :: DuplicatedInode ) . pipe ( Err ) ;
0 commit comments