@@ -78,6 +78,12 @@ fn stat_eq_ignore_mtime(this: &Stat, other: &Stat) -> bool {
7878 return true ;
7979}
8080
81+ #[ derive( Debug ) ]
82+ pub struct UnmergablePaths {
83+ path : PathBuf ,
84+ reason : String ,
85+ }
86+
8187/// Represents the differences between two directory trees.
8288#[ derive( Debug ) ]
8389pub struct Diff {
@@ -88,6 +94,8 @@ pub struct Diff {
8894 modified : Vec < PathBuf > ,
8995 /// Paths that exist in the pristine /etc but not in the current one
9096 removed : Vec < PathBuf > ,
97+ /// Paths that are unmergable
98+ unmergable_paths : Vec < UnmergablePaths > ,
9199}
92100
93101fn collect_all_files (
@@ -170,6 +178,61 @@ fn get_deletions(
170178 Ok ( ( ) )
171179}
172180
181+ fn check_if_mergable (
182+ new : & Directory < CustomMetadata > ,
183+ current_inode : & Inode < CustomMetadata > ,
184+ current_path : & Path ,
185+ diff : & mut Diff ,
186+ ) -> anyhow:: Result < ( ) > {
187+ match current_inode {
188+ // If currently 'file' is a directory, make sure it's not a regular file
189+ // new_etc as well, else we can't merge
190+ Inode :: Directory ( ..) => {
191+ let new_dir = new. get_directory ( & current_path. as_os_str ( ) ) ;
192+
193+ match new_dir {
194+ Ok ( _) => { }
195+ Err ( e) => match e {
196+ ImageError :: NotADirectory ( ..) => {
197+ diff. unmergable_paths . push ( UnmergablePaths {
198+ path : current_path. to_path_buf ( ) ,
199+ reason : format ! (
200+ "Directory '{}' now defaults to a file in new etc" ,
201+ current_path. display( )
202+ ) ,
203+ } ) ;
204+ }
205+ ImageError :: NotFound ( ..) => { }
206+
207+ _ => Err ( e) ?,
208+ } ,
209+ }
210+ }
211+
212+ // If currently 'file' is not a directory, make sure it's not a directory in the
213+ // new_etc either, else we can't merge
214+ Inode :: Leaf ( ..) => {
215+ let new_dir = new. get_directory ( & current_path. as_os_str ( ) ) ;
216+
217+ match new_dir {
218+ Ok ( ..) => {
219+ diff. unmergable_paths . push ( UnmergablePaths {
220+ path : current_path. to_path_buf ( ) ,
221+ reason : format ! (
222+ "File '{}' now defaults to a directory in new etc" ,
223+ current_path. display( )
224+ ) ,
225+ } ) ;
226+ }
227+ Err ( ImageError :: NotFound ( ..) ) => { } ,
228+ Err ( e) => Err ( e) ?,
229+ }
230+ }
231+ }
232+
233+ Ok ( ( ) )
234+ }
235+
173236// 1. Files in the currently booted deployment’s /etc which were modified from the default /usr/etc (of the same deployment) are retained.
174237//
175238// 2. Files in the currently booted deployment’s /etc which were not modified from the default /usr/etc (of the same deployment)
@@ -221,21 +284,39 @@ fn get_modifications(
221284 diff,
222285 ) ?;
223286
224- // This directory or its contents were modified/added
225- // Check if the new directory was deleted from new_etc
226- // If it was, we want to add the directory back
227- if new. get_directory_opt ( & current_path. as_os_str ( ) ) ?. is_none ( ) {
228- if diff. added . len ( ) != total_added {
229- diff. added . insert ( total_added, current_path. clone ( ) ) ;
230- } else if diff. modified . len ( ) != total_modified {
231- diff. modified . insert ( total_modified, current_path. clone ( ) ) ;
287+ match new. get_directory ( & current_path. as_os_str ( ) ) {
288+ Ok ( ..) => {
289+ // Directory exists in both current and new etc.
290+ // Modifications/additions within this directory are handled recursively.
291+ // No additional action needed here.
292+ }
293+
294+ Err ( ImageError :: NotFound ( ..) ) | Err ( ImageError :: NotADirectory ( ..) ) => {
295+ // This directory was deleted in new_etc
296+ // If it was modified in the current etc, we want this back
297+ //
298+ // Was a directory in current etc, but is now
299+ // a file/symlink in the new etc
300+ if diff. added . len ( ) != total_added {
301+ diff. added . insert ( total_added, current_path. clone ( ) ) ;
302+ } else if diff. modified . len ( ) != total_modified {
303+ diff. modified . insert ( total_modified, current_path. clone ( ) ) ;
304+ }
232305 }
306+
307+ Err ( e) => Err ( e) ?,
308+ }
309+
310+ if diff. modified . len ( ) != total_modified || diff. added . len ( ) != total_added
311+ {
312+ check_if_mergable ( new, inode, & current_path, diff) ?;
233313 }
234314 }
235315
236316 Err ( ImageError :: NotFound ( ..) ) => {
237317 // Dir not found in original /etc, dir was added
238318 diff. added . push ( current_path. clone ( ) ) ;
319+ check_if_mergable ( new, inode, & current_path, diff) ?;
239320
240321 // Also add every file inside that dir
241322 collect_all_files ( & curr_dir, current_path. clone ( ) , & mut diff. added ) ;
@@ -245,18 +326,21 @@ fn get_modifications(
245326 // Some directory was changed to a file/symlink
246327 // This should be counted in the diff, but we don't really merge this
247328 diff. modified . push ( current_path. clone ( ) ) ;
329+ check_if_mergable ( new, inode, & current_path, diff) ?;
248330 }
249331
250- Err ( e) => Err ( e) ?,
332+ Err ( e) => Err ( e) . with_context ( || format ! ( "Opening pristine {path:?}" ) ) ?,
251333 }
252334 }
253335
254336 Inode :: Leaf ( leaf_id, _) => match pristine. leaf_id ( path) {
255337 Ok ( old_leaf_id) => {
256338 let leaf = & current_leaves[ leaf_id. 0 ] ;
257339 let old_leaf = & pristine_leaves[ old_leaf_id. 0 ] ;
340+
258341 if !stat_eq_ignore_mtime ( & old_leaf. stat , & leaf. stat ) {
259342 diff. modified . push ( current_path. clone ( ) ) ;
343+ check_if_mergable ( new, inode, & current_path, diff) ?;
260344 current_path. pop ( ) ;
261345 continue ;
262346 }
@@ -266,19 +350,22 @@ fn get_modifications(
266350 if old_meta. content_hash != current_meta. content_hash {
267351 // File modified in some way
268352 diff. modified . push ( current_path. clone ( ) ) ;
353+ check_if_mergable ( new, inode, & current_path, diff) ?;
269354 }
270355 }
271356
272357 ( Symlink ( old_link) , Symlink ( current_link) ) => {
273358 if old_link != current_link {
274359 // Symlink modified in some way
275360 diff. modified . push ( current_path. clone ( ) ) ;
361+ check_if_mergable ( new, inode, & current_path, diff) ?;
276362 }
277363 }
278364
279365 ( Symlink ( ..) , Regular ( ..) ) | ( Regular ( ..) , Symlink ( ..) ) => {
280366 // File changed to symlink or vice-versa
281367 diff. modified . push ( current_path. clone ( ) ) ;
368+ check_if_mergable ( new, inode, & current_path, diff) ?;
282369 }
283370
284371 ( a, b) => {
@@ -290,14 +377,16 @@ fn get_modifications(
290377 Err ( ImageError :: IsADirectory ( ..) ) => {
291378 // A directory was changed to a file
292379 diff. modified . push ( current_path. clone ( ) ) ;
380+ check_if_mergable ( new, inode, & current_path, diff) ?;
293381 }
294382
295383 Err ( ImageError :: NotFound ( ..) ) => {
296384 // File not found in original /etc, file was added
297385 diff. added . push ( current_path. clone ( ) ) ;
386+ check_if_mergable ( new, inode, & current_path, diff) ?;
298387 }
299388
300- Err ( e) => Err ( e) . context ( format ! ( "{path:?}" ) ) ?,
389+ Err ( e) => Err ( e) . with_context ( || format ! ( "Opening pristine {path:?}" ) ) ?,
301390 } ,
302391 }
303392
@@ -385,6 +474,7 @@ pub fn compute_diff(
385474 added : vec ! [ ] ,
386475 modified : vec ! [ ] ,
387476 removed : vec ! [ ] ,
477+ unmergable_paths : vec ! [ ] ,
388478 } ;
389479
390480 get_modifications (
@@ -422,6 +512,15 @@ pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
422512 for removed in & diff. removed {
423513 let _ = writeln ! ( writer, "{} {removed:?}" , ModificationType :: Removed . red( ) ) ;
424514 }
515+
516+ for unmergable in & diff. unmergable_paths {
517+ let _ = writeln ! (
518+ writer,
519+ "{} {}" ,
520+ ModificationType :: Unmergable . magenta( ) ,
521+ unmergable. reason
522+ ) ;
523+ }
425524}
426525
427526#[ context( "Collecting xattrs" ) ]
@@ -589,6 +688,7 @@ enum ModificationType {
589688 Added ,
590689 Modified ,
591690 Removed ,
691+ Unmergable ,
592692}
593693
594694impl std:: fmt:: Display for ModificationType {
@@ -603,6 +703,7 @@ impl ModificationType {
603703 ModificationType :: Added => "+" ,
604704 ModificationType :: Modified => "~" ,
605705 ModificationType :: Removed => "-" ,
706+ ModificationType :: Unmergable => "*" ,
606707 }
607708 }
608709}
@@ -613,23 +714,35 @@ fn create_dir_with_perms(
613714 stat : & Stat ,
614715 new_inode : Option < & Inode < CustomMetadata > > ,
615716) -> anyhow:: Result < ( ) > {
616- // The new directory is not present in the new_etc, so we create it, else we only copy the
617- // metadata
618- if new_inode. is_none ( ) {
619- // Here we use `create_dir_all` to create every parent as we will set the permissions later
620- // on. Due to the fact that we have an ordered (sorted) list of directories and directory
621- // entries and we have a DFS traversal, we will always have directory creation starting from
622- // the parent anyway.
623- //
624- // The exception being, if a directory is modified in the current_etc, and a new directory
625- // is added inside the modified directory, say `dir/prems` has its permissions modified and
626- // `dir/prems/new` is the new directory created. Since we handle added files/directories first,
627- // we will create the directories `perms/new` with directory `new` also getting its
628- // permissions set, but `perms` will not. `perms` will have its permissions set up when we
629- // handle the modified directories.
630- new_etc_fd
631- . create_dir_all ( & dir_name)
632- . context ( format ! ( "Failed to create dir {dir_name:?}" ) ) ?;
717+ match new_inode {
718+ Some ( inode) => match inode {
719+ Inode :: Directory ( ..) => { /* no-op */ }
720+
721+ Inode :: Leaf ( ..) => {
722+ anyhow:: bail!(
723+ "Modified config directory {dir_name:?} newly defaults to file. Cannot merge"
724+ )
725+ }
726+ } ,
727+
728+ // The new directory is not present in the new_etc, so we create it, else we only copy the
729+ // metadata
730+ None => {
731+ // Here we use `create_dir_all` to create every parent as we will set the permissions later
732+ // on. Due to the fact that we have an ordered (sorted) list of directories and directory
733+ // entries and we have a DFS traversal, we will always have directory creation starting from
734+ // the parent anyway.
735+ //
736+ // The exception being, if a directory is modified in the current_etc, and a new directory
737+ // is added inside the modified directory, say `dir/prems` has its permissions modified and
738+ // `dir/prems/new` is the new directory created. Since we handle added files/directories first,
739+ // we will create the directories `perms/new` with directory `new` also getting its
740+ // permissions set, but `perms` will not. `perms` will have its permissions set up when we
741+ // handle the modified directories.
742+ new_etc_fd
743+ . create_dir_all ( & dir_name)
744+ . context ( format ! ( "Failed to create dir {dir_name:?}" ) ) ?;
745+ }
633746 }
634747
635748 new_etc_fd
@@ -730,12 +843,14 @@ fn merge_modified_files(
730843 file,
731844 current_inode. stat ( current_leaves) ,
732845 new_inode,
733- ) ?;
846+ )
847+ . context ( "Merging directory" ) ?;
734848 }
735849
736850 Inode :: Leaf ( leaf_id, _) => {
737851 let leaf = & current_leaves[ leaf_id. 0 ] ;
738- merge_leaf ( current_etc_fd, new_etc_fd, leaf, new_inode, file) ?
852+ merge_leaf ( current_etc_fd, new_etc_fd, leaf, new_inode, file)
853+ . context ( "Merging leaf" ) ?
739854 }
740855 } ;
741856 }
@@ -755,7 +870,7 @@ fn merge_modified_files(
755870 }
756871 } ,
757872
758- Err ( e) => Err ( e) ?,
873+ Err ( e) => Err ( e) . with_context ( || format ! ( "Opening {file:?} in new etc" ) ) ?,
759874 } ;
760875 }
761876
0 commit comments