@@ -7,6 +7,7 @@ use std::cell::RefCell;
7
7
use std:: collections:: BTreeMap ;
8
8
use std:: ffi:: { OsStr , OsString } ;
9
9
use std:: io:: BufReader ;
10
+ use std:: io:: Write ;
10
11
use std:: os:: fd:: { AsFd , AsRawFd } ;
11
12
use std:: os:: unix:: ffi:: OsStrExt ;
12
13
use std:: path:: { Path , PathBuf } ;
@@ -19,11 +20,14 @@ use cap_std_ext::dirext::CapStdExtDirExt;
19
20
use composefs:: fsverity:: { FsVerityHashValue , Sha256HashValue , Sha512HashValue } ;
20
21
use composefs:: generic_tree:: { Directory , Inode , Leaf , LeafContent , Stat } ;
21
22
use 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 } ;
23
24
25
+ /// Metadata associated with a file, directory, or symlink entry.
24
26
#[ derive( Debug ) ]
25
- struct CustomMetadata {
27
+ pub struct CustomMetadata {
28
+ /// A SHA256 sum representing the file contents.
26
29
content_hash : String ,
30
+ /// Optional verity for the file
27
31
verity : Option < String > ,
28
32
}
29
33
@@ -72,14 +76,18 @@ fn stat_eq_ignore_mtime(this: &Stat, other: &Stat) -> bool {
72
76
return true ;
73
77
}
74
78
79
+ /// Represents the differences between two directory trees.
75
80
#[ derive( Debug ) ]
76
- struct Diff {
81
+ pub struct Diff {
82
+ /// Paths that exist in the current /etc but not in the pristine
77
83
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)
78
86
modified : Vec < PathBuf > ,
87
+ /// Paths that exist in the pristine /etc but not in the current one
79
88
removed : Vec < PathBuf > ,
80
89
}
81
90
82
- // if /outer/inner.txt is removed, then we only add /outer iff it's empty
83
91
fn collect_all_files ( root : & Directory < CustomMetadata > , current_path : PathBuf ) -> Vec < PathBuf > {
84
92
fn collect (
85
93
root : & Directory < CustomMetadata > ,
@@ -132,7 +140,7 @@ fn get_deletions(
132
140
133
141
Inode :: Leaf ( ..) => match current. ref_leaf ( file_name) {
134
142
Ok ( ..) => {
135
- // Empty as all additions/modifications are tracked above
143
+ // Empty as all additions/modifications are tracked earlier in `get_modifications`
136
144
}
137
145
138
146
Err ( ImageError :: NotFound ( ..) ) => {
@@ -170,6 +178,8 @@ fn get_modifications(
170
178
mut current_path : PathBuf ,
171
179
diff : & mut Diff ,
172
180
) -> anyhow:: Result < ( ) > {
181
+ use composefs:: generic_tree:: LeafContent :: * ;
182
+
173
183
for ( path, inode) in current. sorted_entries ( ) {
174
184
current_path. push ( path) ;
175
185
@@ -200,19 +210,33 @@ fn get_modifications(
200
210
201
211
Inode :: Leaf ( leaf) => match pristine. ref_leaf ( path) {
202
212
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
+ }
216
240
}
217
241
}
218
242
@@ -236,8 +260,34 @@ fn get_modifications(
236
260
Ok ( ( ) )
237
261
}
238
262
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 (
241
291
pristine_etc : & CapStdDir ,
242
292
current_etc : & CapStdDir ,
243
293
new_etc : & CapStdDir ,
@@ -260,7 +310,8 @@ fn traverse_etc(
260
310
return Ok ( ( pristine_etc_files, current_etc_files, new_etc_files) ) ;
261
311
}
262
312
263
- fn compute_diff (
313
+ /// Computes the differences between two directory snapshots.
314
+ pub fn compute_diff (
264
315
pristine_etc_files : & Directory < CustomMetadata > ,
265
316
current_etc_files : & Directory < CustomMetadata > ,
266
317
) -> anyhow:: Result < Diff > {
@@ -287,6 +338,25 @@ fn compute_diff(
287
338
Ok ( diff)
288
339
}
289
340
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
+
290
360
#[ context( "Collecting xattrs" ) ]
291
361
fn collect_xattrs ( etc_fd : & CapStdDir , rel_path : & OsString ) -> anyhow:: Result < Xattrs > {
292
362
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
296
366
297
367
// Start with a guess for size
298
368
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 " ) ?;
300
370
301
371
if size > DEFAULT_SIZE {
302
372
buf = vec ! [ 0 ; size] ;
303
- listxattr ( & path, & mut buf) . context ( "listxattr " ) ?;
373
+ llistxattr ( & path, & mut buf) . context ( "llistxattr " ) ?;
304
374
}
305
375
306
376
let xattrs: Xattrs = RefCell :: new ( BTreeMap :: new ( ) ) ;
307
377
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 ( ) ) {
312
379
let name = OsStr :: from_bytes ( name_buf) ;
313
380
314
381
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 " ) ?;
316
383
317
384
if size > DEFAULT_SIZE {
318
385
buf = vec ! [ 0 ; size] ;
319
- getxattr ( & path, name_buf, & mut buf) . context ( "getxattr " ) ?;
386
+ lgetxattr ( & path, name_buf, & mut buf) . context ( "lgetxattr " ) ?;
320
387
}
321
388
322
389
xattrs
@@ -345,12 +412,31 @@ fn recurse_dir(dir: &CapStdDir, root: &mut Directory<CustomMetadata>) -> anyhow:
345
412
let entry_name = entry. file_name ( ) ;
346
413
347
414
let entry_type = entry. file_type ( ) ?;
415
+
348
416
let entry_meta = entry
349
417
. metadata ( )
350
418
. context ( format ! ( "Getting metadata for {entry_name:?}" ) ) ?;
351
419
352
420
let xattrs = collect_xattrs ( & dir, & entry_name) ?;
353
421
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
+
354
440
if entry_type. is_dir ( ) {
355
441
let dir = dir
356
442
. open_dir ( & entry_name)
@@ -372,23 +458,6 @@ fn recurse_dir(dir: &CapStdDir, root: &mut Directory<CustomMetadata>) -> anyhow:
372
458
continue ;
373
459
}
374
460
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
-
392
461
// TODO: Another generic here but constrained to Sha256HashValue
393
462
// Regarding this, we'll definitely get DigestMismatch error if SHA512 is being used
394
463
// So we query the verity again if we get a DigestMismatch error
0 commit comments