@@ -3,21 +3,22 @@ use crate::size::Bytes;
33use pipe_trait:: Pipe ;
44use pretty_assertions:: { assert_eq, assert_ne} ;
55
6- const TABLE : & [ ( u64 , u64 , u64 , & str ) ] = & [
7- ( 241 , 3652 , 1 , "a" ) ,
8- ( 569 , 2210 , 1 , "b" ) ,
9- ( 110 , 2350 , 3 , "c" ) ,
10- ( 110 , 2350 , 3 , "c1" ) ,
11- ( 778 , 1110 , 1 , "d" ) ,
12- ( 274 , 6060 , 2 , "e" ) ,
13- ( 274 , 6060 , 2 , "e1" ) ,
14- ( 883 , 4530 , 1 , "f" ) ,
6+ const TABLE : & [ ( u64 , u64 , u64 , u64 , & str ) ] = & [
7+ // dev, ino, size, links, path
8+ ( 0 , 241 , 3652 , 1 , "a" ) ,
9+ ( 0 , 569 , 2210 , 1 , "b" ) ,
10+ ( 0 , 110 , 2350 , 3 , "c" ) ,
11+ ( 0 , 110 , 2350 , 3 , "c1" ) ,
12+ ( 0 , 778 , 1110 , 1 , "d" ) ,
13+ ( 0 , 274 , 6060 , 2 , "e" ) ,
14+ ( 0 , 274 , 6060 , 2 , "e1" ) ,
15+ ( 0 , 883 , 4530 , 1 , "f" ) ,
1516] ;
1617
1718fn add < const ROW : usize > ( list : HardlinkList < Bytes > ) -> HardlinkList < Bytes > {
1819 let values = TABLE [ ROW ] ;
19- let ( ino, size, links, path) = values;
20- if let Err ( error) = list. add ( ino. into ( ) , size. into ( ) , links, path. as_ref ( ) ) {
20+ let ( dev , ino, size, links, path) = values;
21+ if let Err ( error) = list. add ( ino. into ( ) , dev , size. into ( ) , links, path. as_ref ( ) ) {
2122 panic ! ( "Failed to add {values:?} (index: {ROW}) to the list: {error}" ) ;
2223 }
2324 list
@@ -119,10 +120,10 @@ fn insertion_difference_cause_inequality() {
119120#[ test]
120121fn detect_size_change ( ) {
121122 let list = HardlinkList :: < Bytes > :: new ( ) ;
122- list. add ( 123 . into ( ) , 100 . into ( ) , 1 , "a" . as_ref ( ) )
123+ list. add ( 123 . into ( ) , 0 , 100 . into ( ) , 1 , "a" . as_ref ( ) )
123124 . expect ( "add the first path" ) ;
124125 let actual = list
125- . add ( 123 . into ( ) , 110 . into ( ) , 1 , "b" . as_ref ( ) )
126+ . add ( 123 . into ( ) , 0 , 110 . into ( ) , 1 , "b" . as_ref ( ) )
126127 . expect_err ( "add the second path" ) ;
127128 let expected = AddError :: SizeConflict ( SizeConflictError {
128129 ino : 123 . into ( ) ,
@@ -135,10 +136,10 @@ fn detect_size_change() {
135136#[ test]
136137fn detect_number_of_links_change ( ) {
137138 let list = HardlinkList :: < Bytes > :: new ( ) ;
138- list. add ( 123 . into ( ) , 100 . into ( ) , 1 , "a" . as_ref ( ) )
139+ list. add ( 123 . into ( ) , 0 , 100 . into ( ) , 1 , "a" . as_ref ( ) )
139140 . expect ( "add the first path" ) ;
140141 let actual = list
141- . add ( 123 . into ( ) , 100 . into ( ) , 2 , "b" . as_ref ( ) )
142+ . add ( 123 . into ( ) , 0 , 100 . into ( ) , 2 , "b" . as_ref ( ) )
142143 . expect_err ( "add the second path" ) ;
143144 let expected = AddError :: NumberOfLinksConflict ( NumberOfLinksConflictError {
144145 ino : 123 . into ( ) ,
@@ -147,3 +148,38 @@ fn detect_number_of_links_change() {
147148 } ) ;
148149 assert_eq ! ( actual, expected) ;
149150}
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