@@ -148,3 +148,38 @@ fn detect_number_of_links_change() {
148148 } ) ;
149149 assert_eq ! ( actual, expected) ;
150150}
151+
152+ /// Files on different devices may share the same inode number, but they are
153+ /// unrelated — hardlinks cannot span filesystem boundaries. Verify that two
154+ /// files with the same inode number but different device numbers produce
155+ /// separate entries in the list (i.e. the device number is actually used in
156+ /// the deduplication key).
157+ #[ test]
158+ fn same_ino_on_different_devices_are_treated_separately ( ) {
159+ let list = HardlinkList :: < Bytes > :: new ( ) ;
160+
161+ // dev=1, ino=100 — first filesystem
162+ list. add ( 100 . into ( ) , 1 , 50 . into ( ) , 2 , "dev1/file_a" . as_ref ( ) )
163+ . expect ( "add dev1/file_a" ) ;
164+ list. add ( 100 . into ( ) , 1 , 50 . into ( ) , 2 , "dev1/file_b" . as_ref ( ) )
165+ . expect ( "add dev1/file_b (same dev+ino → same inode group)" ) ;
166+
167+ // dev=2, ino=100 — second filesystem, coincidentally same inode number
168+ list. add ( 100 . into ( ) , 2 , 80 . into ( ) , 2 , "dev2/file_c" . as_ref ( ) )
169+ . expect ( "add dev2/file_c (different dev → separate inode group)" ) ;
170+ list. add ( 100 . into ( ) , 2 , 80 . into ( ) , 2 , "dev2/file_d" . as_ref ( ) )
171+ . expect ( "add dev2/file_d (same dev+ino → same inode group as file_c)" ) ;
172+
173+ // Each device should produce its own entry, so the list should have 2 entries.
174+ assert_eq ! ( list. len( ) , 2 , "expected one entry per (dev, ino) pair" ) ;
175+
176+ let reflection = list. into_reflection ( ) ;
177+ // Both entries expose ino=100 in the reflection (device is not part of the
178+ // public JSON format), so there are still 2 entries in the vector.
179+ assert_eq ! ( reflection. len( ) , 2 ) ;
180+
181+ // Paths are grouped per (dev, ino): each group has exactly 2 paths.
182+ for entry in reflection. iter ( ) {
183+ assert_eq ! ( entry. paths. len( ) , 2 ) ;
184+ }
185+ }
0 commit comments