@@ -7,6 +7,7 @@ use std::cell::RefCell;
77use std:: collections:: BTreeMap ;
88use std:: ffi:: { OsStr , OsString } ;
99use std:: io:: BufReader ;
10+ use std:: io:: Write ;
1011use std:: os:: fd:: { AsFd , AsRawFd } ;
1112use std:: os:: unix:: ffi:: OsStrExt ;
1213use std:: path:: { Path , PathBuf } ;
@@ -19,11 +20,14 @@ use cap_std_ext::dirext::CapStdExtDirExt;
1920use composefs:: fsverity:: { FsVerityHashValue , Sha256HashValue , Sha512HashValue } ;
2021use composefs:: generic_tree:: { Directory , Inode , Leaf , LeafContent , Stat } ;
2122use composefs:: tree:: ImageError ;
22- use rustix:: fs:: { AtFlags , Gid , Uid , XattrFlags , getxattr , listxattr , lsetxattr , readlinkat } ;
23+ use rustix:: fs:: { lgetxattr , llistxattr , lsetxattr , readlinkat , AtFlags , Gid , Uid , XattrFlags } ;
2324
25+ /// Metadata associated with a file, directory, or symlink entry.
2426#[ derive( Debug ) ]
25- struct CustomMetadata {
27+ pub struct CustomMetadata {
28+ /// A SHA256 sum representing the file contents.
2629 content_hash : String ,
30+ /// Optional verity for the file
2731 verity : Option < String > ,
2832}
2933
@@ -72,14 +76,18 @@ fn stat_eq_ignore_mtime(this: &Stat, other: &Stat) -> bool {
7276 return true ;
7377}
7478
79+ /// Represents the differences between two directory trees.
7580#[ derive( Debug ) ]
76- struct Diff {
81+ pub struct Diff {
82+ /// Paths that exist in the current /etc but not in the pristine
7783 added : Vec < PathBuf > ,
84+ /// Paths that exist in both pristine and current /etc but differ in metadata
85+ /// (e.g., file contents, permissions, symlink targets)
7886 modified : Vec < PathBuf > ,
87+ /// Paths that exist in the pristine /etc but not in the current one
7988 removed : Vec < PathBuf > ,
8089}
8190
82- // if /outer/inner.txt is removed, then we only add /outer iff it's empty
8391fn collect_all_files ( root : & Directory < CustomMetadata > , current_path : PathBuf ) -> Vec < PathBuf > {
8492 fn collect (
8593 root : & Directory < CustomMetadata > ,
@@ -132,7 +140,7 @@ fn get_deletions(
132140
133141 Inode :: Leaf ( ..) => match current. ref_leaf ( file_name) {
134142 Ok ( ..) => {
135- // Empty as all additions/modifications are tracked above
143+ // Empty as all additions/modifications are tracked earlier in `get_modifications`
136144 }
137145
138146 Err ( ImageError :: NotFound ( ..) ) => {
@@ -170,6 +178,8 @@ fn get_modifications(
170178 mut current_path : PathBuf ,
171179 diff : & mut Diff ,
172180) -> anyhow:: Result < ( ) > {
181+ use composefs:: generic_tree:: LeafContent :: * ;
182+
173183 for ( path, inode) in current. sorted_entries ( ) {
174184 current_path. push ( path) ;
175185
@@ -200,19 +210,33 @@ fn get_modifications(
200210
201211 Inode :: Leaf ( leaf) => match pristine. ref_leaf ( path) {
202212 Ok ( old_leaf) => {
203- let LeafContent :: Regular ( current_meta) = & leaf. content else {
204- unreachable ! ( "File types do not match" ) ;
205- } ;
206-
207- let LeafContent :: Regular ( old_meta) = & old_leaf. content else {
208- unreachable ! ( "File types do not match" ) ;
209- } ;
210-
211- if old_meta. content_hash != current_meta. content_hash
212- || !stat_eq_ignore_mtime ( & old_leaf. stat , & leaf. stat )
213- {
214- // File modified in some way
215- diff. modified . push ( current_path. clone ( ) ) ;
213+ match ( & old_leaf. content , & leaf. content ) {
214+ ( Regular ( old_meta) , Regular ( current_meta) ) => {
215+ if old_meta. content_hash != current_meta. content_hash
216+ || !stat_eq_ignore_mtime ( & old_leaf. stat , & leaf. stat )
217+ {
218+ // File modified in some way
219+ diff. modified . push ( current_path. clone ( ) ) ;
220+ }
221+ }
222+
223+ ( Symlink ( old_link) , Symlink ( current_link) ) => {
224+ if old_link != current_link
225+ || !stat_eq_ignore_mtime ( & old_leaf. stat , & leaf. stat )
226+ {
227+ // Symlink modified in some way
228+ diff. modified . push ( current_path. clone ( ) ) ;
229+ }
230+ }
231+
232+ ( Symlink ( ..) , Regular ( ..) ) | ( Regular ( ..) , Symlink ( ..) ) => {
233+ // File changed to symlink or vice-versa
234+ diff. modified . push ( current_path. clone ( ) ) ;
235+ }
236+
237+ ( a, b) => {
238+ unreachable ! ( "{a:?} modified to {b:?}" )
239+ }
216240 }
217241 }
218242
@@ -236,8 +260,34 @@ fn get_modifications(
236260 Ok ( ( ) )
237261}
238262
239- /// (Pristine, Current, New)
240- fn traverse_etc (
263+ /// Traverses and collects directory trees for three etc states.
264+ ///
265+ /// Recursively walks through the given *pristine*, *current*, and *new* etc directories,
266+ /// building filesystem trees that capture files, directories, and symlinks.
267+ /// Device files, sockets, pipes etc are ignored
268+ ///
269+ /// It is primarily used to prepare inputs for later diff computations and
270+ /// comparisons between different etc states.
271+ ///
272+ /// # Arguments
273+ ///
274+ /// * `pristine_etc` - The reference directory representing the unmodified version or current /etc.
275+ /// Usually this will be obtained by remounting the EROFS image to a temporary location
276+ ///
277+ /// * `current_etc` - The current `/etc` directory
278+ ///
279+ /// * `new_etc` - The directory representing the `/etc` directory for a new deployment. This will
280+ /// again be usually obtained by mounting the new EROFS image to a temporary location. If merging
281+ /// it will be necessary to make the `/etc` for the deployment writeable
282+ ///
283+ /// # Returns
284+ ///
285+ /// [`anyhow::Result`] containing a tuple of directory trees in the order:
286+ ///
287+ /// 1. `pristine_etc_files` – Dirtree of the pristine etc state
288+ /// 2. `current_etc_files` – Dirtree of the current etc state
289+ /// 3. `new_etc_files` – Dirtree of the new etc state
290+ pub fn traverse_etc (
241291 pristine_etc : & CapStdDir ,
242292 current_etc : & CapStdDir ,
243293 new_etc : & CapStdDir ,
@@ -260,7 +310,8 @@ fn traverse_etc(
260310 return Ok ( ( pristine_etc_files, current_etc_files, new_etc_files) ) ;
261311}
262312
263- fn compute_diff (
313+ /// Computes the differences between two directory snapshots.
314+ pub fn compute_diff (
264315 pristine_etc_files : & Directory < CustomMetadata > ,
265316 current_etc_files : & Directory < CustomMetadata > ,
266317) -> anyhow:: Result < Diff > {
@@ -287,6 +338,25 @@ fn compute_diff(
287338 Ok ( diff)
288339}
289340
341+ /// Prints a colorized summary of differences to standard output.
342+ pub fn print_diff ( diff : & Diff ) {
343+ use owo_colors:: OwoColorize ;
344+
345+ let mut stdout = anstream:: stdout ( ) ;
346+
347+ for added in & diff. added {
348+ let _ = writeln ! ( stdout, "{} {added:?}" , ModificationType :: Added . green( ) ) ;
349+ }
350+
351+ for modified in & diff. modified {
352+ let _ = writeln ! ( stdout, "{} {modified:?}" , ModificationType :: Modified . cyan( ) ) ;
353+ }
354+
355+ for removed in & diff. removed {
356+ let _ = writeln ! ( stdout, "{} {removed:?}" , ModificationType :: Removed . red( ) ) ;
357+ }
358+ }
359+
290360#[ context( "Collecting xattrs" ) ]
291361fn collect_xattrs ( etc_fd : & CapStdDir , rel_path : & OsString ) -> anyhow:: Result < Xattrs > {
292362 let link = format ! ( "/proc/self/fd/{}" , etc_fd. as_fd( ) . as_raw_fd( ) ) ;
@@ -296,27 +366,24 @@ fn collect_xattrs(etc_fd: &CapStdDir, rel_path: &OsString) -> anyhow::Result<Xat
296366
297367 // Start with a guess for size
298368 let mut buf: Vec < u8 > = vec ! [ 0 ; DEFAULT_SIZE ] ;
299- let size = listxattr ( & path, & mut buf) . context ( "listxattr " ) ?;
369+ let size = llistxattr ( & path, & mut buf) . context ( "llistxattr " ) ?;
300370
301371 if size > DEFAULT_SIZE {
302372 buf = vec ! [ 0 ; size] ;
303- listxattr ( & path, & mut buf) . context ( "listxattr " ) ?;
373+ llistxattr ( & path, & mut buf) . context ( "llistxattr " ) ?;
304374 }
305375
306376 let xattrs: Xattrs = RefCell :: new ( BTreeMap :: new ( ) ) ;
307377
308- for name_buf in buf[ ..size]
309- . split_inclusive ( |& b| b == 0 )
310- . filter ( |x| !x. is_empty ( ) )
311- {
378+ for name_buf in buf[ ..size] . split ( |& b| b == 0 ) . filter ( |x| !x. is_empty ( ) ) {
312379 let name = OsStr :: from_bytes ( name_buf) ;
313380
314381 let mut buf = vec ! [ 0 ; DEFAULT_SIZE ] ;
315- let size = getxattr ( & path, name_buf, & mut buf) . context ( "getxattr " ) ?;
382+ let size = lgetxattr ( & path, name_buf, & mut buf) . context ( "lgetxattr " ) ?;
316383
317384 if size > DEFAULT_SIZE {
318385 buf = vec ! [ 0 ; size] ;
319- getxattr ( & path, name_buf, & mut buf) . context ( "getxattr " ) ?;
386+ lgetxattr ( & path, name_buf, & mut buf) . context ( "lgetxattr " ) ?;
320387 }
321388
322389 xattrs
@@ -345,12 +412,31 @@ fn recurse_dir(dir: &CapStdDir, root: &mut Directory<CustomMetadata>) -> anyhow:
345412 let entry_name = entry. file_name ( ) ;
346413
347414 let entry_type = entry. file_type ( ) ?;
415+
348416 let entry_meta = entry
349417 . metadata ( )
350418 . context ( format ! ( "Getting metadata for {entry_name:?}" ) ) ?;
351419
352420 let xattrs = collect_xattrs ( & dir, & entry_name) ?;
353421
422+ // Do symlinks first as we don't want to follow back up any symlinks
423+ if entry_type. is_symlink ( ) {
424+ let readlinkat_result = readlinkat ( & dir, & entry_name, vec ! [ ] )
425+ . context ( format ! ( "readlinkat {entry_name:?}" ) ) ?;
426+
427+ let os_str = OsStr :: from_bytes ( readlinkat_result. as_bytes ( ) ) ;
428+
429+ root. insert (
430+ & entry_name,
431+ Inode :: Leaf ( Rc :: new ( Leaf {
432+ stat : MyStat :: from ( ( & entry_meta, xattrs) ) . 0 ,
433+ content : LeafContent :: Symlink ( Box :: from ( os_str) ) ,
434+ } ) ) ,
435+ ) ;
436+
437+ continue ;
438+ }
439+
354440 if entry_type. is_dir ( ) {
355441 let dir = dir
356442 . open_dir ( & entry_name)
@@ -372,23 +458,6 @@ fn recurse_dir(dir: &CapStdDir, root: &mut Directory<CustomMetadata>) -> anyhow:
372458 continue ;
373459 }
374460
375- if entry_type. is_symlink ( ) {
376- let readlinkat_result = readlinkat ( & dir, & entry_name, vec ! [ ] )
377- . context ( format ! ( "readlinkat {entry_name:?}" ) ) ?;
378-
379- let os_str = OsStr :: from_bytes ( readlinkat_result. as_bytes ( ) ) ;
380-
381- root. insert (
382- & entry_name,
383- Inode :: Leaf ( Rc :: new ( Leaf {
384- stat : MyStat :: from ( ( & entry_meta, xattrs) ) . 0 ,
385- content : LeafContent :: Symlink ( Box :: from ( os_str) ) ,
386- } ) ) ,
387- ) ;
388-
389- continue ;
390- }
391-
392461 // TODO: Another generic here but constrained to Sha256HashValue
393462 // Regarding this, we'll definitely get DigestMismatch error if SHA512 is being used
394463 // So we query the verity again if we get a DigestMismatch error
0 commit comments