@@ -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,47 @@ fn get_deletions(
170178 Ok ( ( ) )
171179}
172180
181+ fn check_if_mergable (
182+ new : & Directory < CustomMetadata > ,
183+ current_inode : & Inode < CustomMetadata > ,
184+ current_path : & PathBuf ,
185+ diff : & mut Diff ,
186+ ) {
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+ if matches ! ( new_dir, Err ( ImageError :: NotADirectory ( ..) ) ) {
194+ diff. unmergable_paths . push ( UnmergablePaths {
195+ path : current_path. clone ( ) ,
196+ reason : format ! (
197+ "Directory '{}' now defaults to a file in new etc" ,
198+ current_path. display( )
199+ ) ,
200+ } ) ;
201+ }
202+ }
203+
204+ // If currently 'file' is not a directory, make sure it's not a directory in the
205+ // new_etc either, else we can't merge
206+ Inode :: Leaf ( ..) => {
207+ let new_dir = new. get_directory ( & current_path. as_os_str ( ) ) ;
208+
209+ if matches ! ( new_dir, Ok ( ..) ) {
210+ diff. unmergable_paths . push ( UnmergablePaths {
211+ path : current_path. clone ( ) ,
212+ reason : format ! (
213+ "File '{}' now defaults to a directory in new etc" ,
214+ current_path. display( )
215+ ) ,
216+ } ) ;
217+ }
218+ }
219+ }
220+ }
221+
173222// 1. Files in the currently booted deployment’s /etc which were modified from the default /usr/etc (of the same deployment) are retained.
174223//
175224// 2. Files in the currently booted deployment’s /etc which were not modified from the default /usr/etc (of the same deployment)
@@ -190,6 +239,7 @@ fn get_modifications(
190239 current : & Directory < CustomMetadata > ,
191240 pristine_leaves : & [ Leaf < CustomMetadata > ] ,
192241 current_leaves : & [ Leaf < CustomMetadata > ] ,
242+ new_leaves : & [ Leaf < CustomMetadata > ] ,
193243 new : & Directory < CustomMetadata > ,
194244 mut current_path : PathBuf ,
195245 diff : & mut Diff ,
@@ -216,26 +266,45 @@ fn get_modifications(
216266 & curr_dir,
217267 pristine_leaves,
218268 current_leaves,
269+ new_leaves,
219270 new,
220271 current_path. clone ( ) ,
221272 diff,
222273 ) ?;
223274
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 ( ) ) ;
275+ match new. get_directory ( & current_path. as_os_str ( ) ) {
276+ Ok ( ..) => {
277+ // Directory exists in both current and new etc.
278+ // Modifications/additions within this directory are handled recursively.
279+ // No additional action needed here.
232280 }
281+
282+ Err ( ImageError :: NotFound ( ..) ) | Err ( ImageError :: NotADirectory ( ..) ) => {
283+ // This directory was deleted in new_etc
284+ // If it was modified in the current etc, we want this back
285+ //
286+ // Was a directory in current etc, but is now
287+ // a file/symlink in the new etc
288+ if diff. added . len ( ) != total_added {
289+ diff. added . insert ( total_added, current_path. clone ( ) ) ;
290+ } else if diff. modified . len ( ) != total_modified {
291+ diff. modified . insert ( total_added, current_path. clone ( ) ) ;
292+ }
293+ }
294+
295+ Err ( e) => Err ( e) ?,
296+ }
297+
298+ if diff. modified . len ( ) != total_modified || diff. added . len ( ) != total_added
299+ {
300+ check_if_mergable ( new, inode, & current_path, diff) ;
233301 }
234302 }
235303
236304 Err ( ImageError :: NotFound ( ..) ) => {
237305 // Dir not found in original /etc, dir was added
238306 diff. added . push ( current_path. clone ( ) ) ;
307+ check_if_mergable ( new, inode, & current_path, diff) ;
239308
240309 // Also add every file inside that dir
241310 collect_all_files ( & curr_dir, current_path. clone ( ) , & mut diff. added ) ;
@@ -245,18 +314,21 @@ fn get_modifications(
245314 // Some directory was changed to a file/symlink
246315 // This should be counted in the diff, but we don't really merge this
247316 diff. modified . push ( current_path. clone ( ) ) ;
317+ check_if_mergable ( new, inode, & current_path, diff) ;
248318 }
249319
250- Err ( e) => Err ( e) ?,
320+ Err ( e) => Err ( e) . with_context ( || format ! ( "Opening pristine {path:?}" ) ) ?,
251321 }
252322 }
253323
254324 Inode :: Leaf ( leaf_id, _) => match pristine. leaf_id ( path) {
255325 Ok ( old_leaf_id) => {
256326 let leaf = & current_leaves[ leaf_id. 0 ] ;
257327 let old_leaf = & pristine_leaves[ old_leaf_id. 0 ] ;
328+
258329 if !stat_eq_ignore_mtime ( & old_leaf. stat , & leaf. stat ) {
259330 diff. modified . push ( current_path. clone ( ) ) ;
331+ check_if_mergable ( new, inode, & current_path, diff) ;
260332 current_path. pop ( ) ;
261333 continue ;
262334 }
@@ -266,19 +338,22 @@ fn get_modifications(
266338 if old_meta. content_hash != current_meta. content_hash {
267339 // File modified in some way
268340 diff. modified . push ( current_path. clone ( ) ) ;
341+ check_if_mergable ( new, inode, & current_path, diff) ;
269342 }
270343 }
271344
272345 ( Symlink ( old_link) , Symlink ( current_link) ) => {
273346 if old_link != current_link {
274347 // Symlink modified in some way
275348 diff. modified . push ( current_path. clone ( ) ) ;
349+ check_if_mergable ( new, inode, & current_path, diff) ;
276350 }
277351 }
278352
279353 ( Symlink ( ..) , Regular ( ..) ) | ( Regular ( ..) , Symlink ( ..) ) => {
280354 // File changed to symlink or vice-versa
281355 diff. modified . push ( current_path. clone ( ) ) ;
356+ check_if_mergable ( new, inode, & current_path, diff) ;
282357 }
283358
284359 ( a, b) => {
@@ -290,14 +365,16 @@ fn get_modifications(
290365 Err ( ImageError :: IsADirectory ( ..) ) => {
291366 // A directory was changed to a file
292367 diff. modified . push ( current_path. clone ( ) ) ;
368+ check_if_mergable ( new, inode, & current_path, diff) ;
293369 }
294370
295371 Err ( ImageError :: NotFound ( ..) ) => {
296372 // File not found in original /etc, file was added
297373 diff. added . push ( current_path. clone ( ) ) ;
374+ check_if_mergable ( new, inode, & current_path, diff) ;
298375 }
299376
300- Err ( e) => Err ( e) . context ( format ! ( "{path:?}" ) ) ?,
377+ Err ( e) => Err ( e) . with_context ( || format ! ( "Opening pristine {path:?}" ) ) ?,
301378 } ,
302379 }
303380
@@ -385,13 +462,15 @@ pub fn compute_diff(
385462 added : vec ! [ ] ,
386463 modified : vec ! [ ] ,
387464 removed : vec ! [ ] ,
465+ unmergable_paths : vec ! [ ] ,
388466 } ;
389467
390468 get_modifications (
391469 & pristine_etc_files. root ,
392470 & current_etc_files. root ,
393471 & pristine_etc_files. leaves ,
394472 & current_etc_files. leaves ,
473+ & new_etc_files. leaves ,
395474 & new_etc_files. root ,
396475 PathBuf :: new ( ) ,
397476 & mut diff,
@@ -422,6 +501,15 @@ pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
422501 for removed in & diff. removed {
423502 let _ = writeln ! ( writer, "{} {removed:?}" , ModificationType :: Removed . red( ) ) ;
424503 }
504+
505+ for unmergable in & diff. unmergable_paths {
506+ let _ = writeln ! (
507+ writer,
508+ "{} {}" ,
509+ ModificationType :: Unmergable . magenta( ) ,
510+ unmergable. reason
511+ ) ;
512+ }
425513}
426514
427515#[ context( "Collecting xattrs" ) ]
@@ -589,6 +677,7 @@ enum ModificationType {
589677 Added ,
590678 Modified ,
591679 Removed ,
680+ Unmergable ,
592681}
593682
594683impl std:: fmt:: Display for ModificationType {
@@ -603,6 +692,7 @@ impl ModificationType {
603692 ModificationType :: Added => "+" ,
604693 ModificationType :: Modified => "~" ,
605694 ModificationType :: Removed => "-" ,
695+ ModificationType :: Unmergable => "*" ,
606696 }
607697 }
608698}
@@ -613,23 +703,35 @@ fn create_dir_with_perms(
613703 stat : & Stat ,
614704 new_inode : Option < & Inode < CustomMetadata > > ,
615705) -> 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:?}" ) ) ?;
706+ match new_inode {
707+ Some ( inode) => match inode {
708+ Inode :: Directory ( ..) => { /* no-op */ }
709+
710+ Inode :: Leaf ( ..) => {
711+ anyhow:: bail!(
712+ "Modified config directory {dir_name:?} newly defaults to file. Cannot merge"
713+ )
714+ }
715+ } ,
716+
717+ // The new directory is not present in the new_etc, so we create it, else we only copy the
718+ // metadata
719+ None => {
720+ // Here we use `create_dir_all` to create every parent as we will set the permissions later
721+ // on. Due to the fact that we have an ordered (sorted) list of directories and directory
722+ // entries and we have a DFS traversal, we will always have directory creation starting from
723+ // the parent anyway.
724+ //
725+ // The exception being, if a directory is modified in the current_etc, and a new directory
726+ // is added inside the modified directory, say `dir/prems` has its permissions modified and
727+ // `dir/prems/new` is the new directory created. Since we handle added files/directories first,
728+ // we will create the directories `perms/new` with directory `new` also getting its
729+ // permissions set, but `perms` will not. `perms` will have its permissions set up when we
730+ // handle the modified directories.
731+ new_etc_fd
732+ . create_dir_all ( & dir_name)
733+ . context ( format ! ( "Failed to create dir {dir_name:?}" ) ) ?;
734+ }
633735 }
634736
635737 new_etc_fd
@@ -730,12 +832,14 @@ fn merge_modified_files(
730832 file,
731833 current_inode. stat ( current_leaves) ,
732834 new_inode,
733- ) ?;
835+ )
836+ . context ( "Merging directory" ) ?;
734837 }
735838
736839 Inode :: Leaf ( leaf_id, _) => {
737840 let leaf = & current_leaves[ leaf_id. 0 ] ;
738- merge_leaf ( current_etc_fd, new_etc_fd, leaf, new_inode, file) ?
841+ merge_leaf ( current_etc_fd, new_etc_fd, leaf, new_inode, file)
842+ . context ( "Merging leaf" ) ?
739843 }
740844 } ;
741845 }
@@ -755,7 +859,7 @@ fn merge_modified_files(
755859 }
756860 } ,
757861
758- Err ( e) => Err ( e) ?,
862+ Err ( e) => Err ( e) . with_context ( || format ! ( "Opening {file:?} in new etc" ) ) ?,
759863 } ;
760864 }
761865
0 commit comments