1#![allow(dead_code)]
4
5use fn_error_context::context;
6use std::collections::BTreeMap;
7use std::ffi::OsStr;
8use std::io::BufReader;
9use std::io::Write;
10use std::os::fd::{AsFd, AsRawFd};
11use std::os::unix::ffi::OsStrExt;
12use std::path::{Path, PathBuf};
13
14use anyhow::Context;
15use cap_std_ext::cap_std;
16use cap_std_ext::cap_std::fs::{Dir as CapStdDir, MetadataExt, Permissions, PermissionsExt};
17use cap_std_ext::dirext::CapStdExtDirExt;
18use composefs::fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
19use composefs::generic_tree::{Directory, FileSystem, Inode, Leaf, LeafContent, LeafId, Stat};
20use composefs::tree::ImageError;
21use composefs_ctl::composefs;
22use rustix::fs::{
23 AtFlags, Gid, Uid, XattrFlags, lgetxattr, llistxattr, lsetxattr, readlinkat, symlinkat,
24};
25
26#[derive(Debug)]
28pub struct CustomMetadata {
29 content_hash: String,
31 verity: Option<String>,
33}
34
35impl CustomMetadata {
36 fn new(content_hash: String, verity: Option<String>) -> Self {
37 Self {
38 content_hash,
39 verity,
40 }
41 }
42}
43
44type Xattrs = BTreeMap<Box<OsStr>, Box<[u8]>>;
45
46struct MyStat(Stat);
47
48impl From<(&cap_std::fs::Metadata, Xattrs)> for MyStat {
49 fn from(value: (&cap_std::fs::Metadata, Xattrs)) -> Self {
50 Self(Stat {
51 st_mode: value.0.mode(),
52 st_uid: value.0.uid(),
53 st_gid: value.0.gid(),
54 st_mtim_sec: value.0.mtime(),
55 st_mtim_nsec: value.0.mtime_nsec() as u32,
56 xattrs: value.1,
57 })
58 }
59}
60
61fn stat_eq_ignore_mtime(this: &Stat, other: &Stat) -> bool {
62 if this.st_uid != other.st_uid {
63 return false;
64 }
65
66 if this.st_gid != other.st_gid {
67 return false;
68 }
69
70 if this.st_mode != other.st_mode {
71 return false;
72 }
73
74 if this.xattrs != other.xattrs {
75 return false;
76 }
77
78 return true;
79}
80
81#[derive(Debug)]
82pub struct UnmergablePaths {
83 pub path: PathBuf,
84 pub reason: String,
85}
86
87#[derive(Debug)]
89pub struct Diff {
90 added: Vec<PathBuf>,
92 modified: Vec<PathBuf>,
95 removed: Vec<PathBuf>,
97 pub unmergable_paths: Vec<UnmergablePaths>,
99}
100
101fn collect_all_files(
102 root: &Directory<CustomMetadata>,
103 current_path: PathBuf,
104 files: &mut Vec<PathBuf>,
105) {
106 fn collect(
107 root: &Directory<CustomMetadata>,
108 mut current_path: PathBuf,
109 files: &mut Vec<PathBuf>,
110 ) {
111 for (path, inode) in root.sorted_entries() {
112 current_path.push(path);
113
114 files.push(current_path.clone());
115
116 if let Inode::Directory(dir) = inode {
117 collect(dir, current_path.clone(), files);
118 }
119
120 current_path.pop();
121 }
122 }
123
124 collect(root, current_path, files);
125}
126
127#[context("Getting deletions")]
128fn get_deletions(
129 pristine: &Directory<CustomMetadata>,
130 current: &Directory<CustomMetadata>,
131 mut current_path: PathBuf,
132 diff: &mut Diff,
133) -> anyhow::Result<()> {
134 for (file_name, inode) in pristine.sorted_entries() {
135 current_path.push(file_name);
136
137 match inode {
138 Inode::Directory(pristine_dir) => {
139 match current.get_directory(file_name) {
140 Ok(curr_dir) => {
141 get_deletions(pristine_dir, curr_dir, current_path.clone(), diff)?
142 }
143
144 Err(ImageError::NotFound(..)) => {
145 diff.removed.push(current_path.clone());
147 }
148
149 Err(ImageError::NotADirectory(..)) => {
150 }
152
153 Err(e) => Err(e)?,
154 }
155 }
156
157 Inode::Leaf(..) => match current.leaf_id(file_name) {
158 Ok(..) => {
159 }
161
162 Err(ImageError::NotFound(..)) => {
163 diff.removed.push(current_path.clone());
165 }
166
167 Err(ImageError::IsADirectory(..)) => {
168 }
170
171 Err(e) => Err(e).context(format!("{file_name:?}"))?,
172 },
173 }
174
175 current_path.pop();
176 }
177
178 Ok(())
179}
180
181#[context("Checking mergability: {current_path:?}")]
182fn check_if_mergable(
183 new: &Directory<CustomMetadata>,
184 current_inode: &Inode<CustomMetadata>,
185 current_path: &Path,
186 diff: &mut Diff,
187) -> anyhow::Result<()> {
188 match current_inode {
189 Inode::Directory(..) => {
192 let new_dir = new.get_directory(¤t_path.as_os_str());
193
194 match new_dir {
195 Ok(_) => {}
196 Err(e) => match e {
197 ImageError::NotADirectory(..) => {
198 diff.unmergable_paths.push(UnmergablePaths {
199 path: current_path.to_path_buf(),
200 reason: format!(
201 "Directory '{}' now defaults to a file in new etc",
202 current_path.display()
203 ),
204 });
205 }
206 ImageError::NotFound(..) => {}
207
208 _ => Err(e)?,
209 },
210 }
211 }
212
213 Inode::Leaf(..) => {
216 let new_dir = new.get_directory(¤t_path.as_os_str());
217
218 match new_dir {
219 Ok(..) => {
220 diff.unmergable_paths.push(UnmergablePaths {
221 path: current_path.to_path_buf(),
222 reason: format!(
223 "File '{}' now defaults to a directory in new etc",
224 current_path.display()
225 ),
226 });
227 }
228 Err(ImageError::NotFound(..)) | Err(ImageError::NotADirectory(..)) => {}
229 Err(e) => Err(e)?,
230 }
231 }
232 }
233
234 Ok(())
235}
236
237#[context("Getting modifications")]
252fn get_modifications(
253 pristine: &Directory<CustomMetadata>,
254 current: &Directory<CustomMetadata>,
255 pristine_leaves: &[Leaf<CustomMetadata>],
256 current_leaves: &[Leaf<CustomMetadata>],
257 new: &Directory<CustomMetadata>,
258 mut current_path: PathBuf,
259 diff: &mut Diff,
260) -> anyhow::Result<()> {
261 use composefs::generic_tree::LeafContent::*;
262
263 for (path, inode) in current.sorted_entries() {
264 current_path.push(path);
265
266 match inode {
267 Inode::Directory(curr_dir) => {
268 match pristine.get_directory(path) {
269 Ok(old_dir) => {
270 if !stat_eq_ignore_mtime(&curr_dir.stat, &old_dir.stat) {
271 diff.modified.push(current_path.clone());
273 }
274
275 let total_added = diff.added.len();
276 let total_modified = diff.modified.len();
277
278 get_modifications(
279 old_dir,
280 &curr_dir,
281 pristine_leaves,
282 current_leaves,
283 new,
284 current_path.clone(),
285 diff,
286 )?;
287
288 match new.get_directory(¤t_path.as_os_str()) {
289 Ok(..) => {
290 }
294
295 Err(ImageError::NotFound(..)) | Err(ImageError::NotADirectory(..)) => {
296 if diff.added.len() != total_added {
302 diff.added.insert(total_added, current_path.clone());
303 } else if diff.modified.len() != total_modified {
304 diff.modified.insert(total_modified, current_path.clone());
305 }
306 }
307
308 Err(e) => Err(e)?,
309 }
310
311 if diff.modified.len() != total_modified || diff.added.len() != total_added
312 {
313 check_if_mergable(new, inode, ¤t_path, diff)?;
314 }
315 }
316
317 Err(ImageError::NotFound(..)) => {
318 diff.added.push(current_path.clone());
320 check_if_mergable(new, inode, ¤t_path, diff)?;
321
322 collect_all_files(&curr_dir, current_path.clone(), &mut diff.added);
324 }
325
326 Err(ImageError::NotADirectory(..)) => {
327 diff.modified.push(current_path.clone());
330 check_if_mergable(new, inode, ¤t_path, diff)?;
331 }
332
333 Err(e) => Err(e).with_context(|| format!("Opening pristine {path:?}"))?,
334 }
335 }
336
337 Inode::Leaf(leaf_id, _) => match pristine.leaf_id(path) {
338 Ok(old_leaf_id) => {
339 let leaf = ¤t_leaves[leaf_id.0];
340 let old_leaf = &pristine_leaves[old_leaf_id.0];
341
342 if !stat_eq_ignore_mtime(&old_leaf.stat, &leaf.stat) {
343 diff.modified.push(current_path.clone());
344 check_if_mergable(new, inode, ¤t_path, diff)?;
345 current_path.pop();
346 continue;
347 }
348
349 match (&old_leaf.content, &leaf.content) {
350 (Regular(old_meta), Regular(current_meta)) => {
351 if old_meta.content_hash != current_meta.content_hash {
352 diff.modified.push(current_path.clone());
354 check_if_mergable(new, inode, ¤t_path, diff)?;
355 }
356 }
357
358 (Symlink(old_link), Symlink(current_link)) => {
359 if old_link != current_link {
360 diff.modified.push(current_path.clone());
362 check_if_mergable(new, inode, ¤t_path, diff)?;
363 }
364 }
365
366 (Symlink(..), Regular(..)) | (Regular(..), Symlink(..)) => {
367 diff.modified.push(current_path.clone());
369 check_if_mergable(new, inode, ¤t_path, diff)?;
370 }
371
372 (a, b) => {
373 unreachable!("{a:?} modified to {b:?}")
374 }
375 }
376 }
377
378 Err(ImageError::IsADirectory(..)) => {
379 diff.modified.push(current_path.clone());
381 check_if_mergable(new, inode, ¤t_path, diff)?;
382 }
383
384 Err(ImageError::NotFound(..)) => {
385 diff.added.push(current_path.clone());
387 check_if_mergable(new, inode, ¤t_path, diff)?;
388 }
389
390 Err(e) => Err(e).with_context(|| format!("Opening pristine {path:?}"))?,
391 },
392 }
393
394 current_path.pop();
395 }
396
397 Ok(())
398}
399
400pub fn traverse_etc(
428 pristine_etc: &CapStdDir,
429 current_etc: &CapStdDir,
430 new_etc: Option<&CapStdDir>,
431) -> anyhow::Result<(
432 FileSystem<CustomMetadata>,
433 FileSystem<CustomMetadata>,
434 Option<FileSystem<CustomMetadata>>,
435)> {
436 let mut pristine_etc_files = FileSystem::new(Stat::uninitialized());
437 recurse_dir(
438 pristine_etc,
439 &mut pristine_etc_files.root,
440 &mut pristine_etc_files.leaves,
441 )
442 .context(format!("Recursing {pristine_etc:?}"))?;
443
444 let mut current_etc_files = FileSystem::new(Stat::uninitialized());
445 recurse_dir(
446 current_etc,
447 &mut current_etc_files.root,
448 &mut current_etc_files.leaves,
449 )
450 .context(format!("Recursing {current_etc:?}"))?;
451
452 let new_etc_files = match new_etc {
453 Some(new_etc) => {
454 let mut new_etc_files = FileSystem::new(Stat::uninitialized());
455 recurse_dir(new_etc, &mut new_etc_files.root, &mut new_etc_files.leaves)
456 .context(format!("Recursing {new_etc:?}"))?;
457
458 Some(new_etc_files)
459 }
460
461 None => None,
462 };
463
464 return Ok((pristine_etc_files, current_etc_files, new_etc_files));
465}
466
467#[context("Computing diff")]
469pub fn compute_diff(
470 pristine_etc_files: &FileSystem<CustomMetadata>,
471 current_etc_files: &FileSystem<CustomMetadata>,
472 new_etc_files: &FileSystem<CustomMetadata>,
473) -> anyhow::Result<Diff> {
474 let mut diff = Diff {
475 added: vec![],
476 modified: vec![],
477 removed: vec![],
478 unmergable_paths: vec![],
479 };
480
481 get_modifications(
482 &pristine_etc_files.root,
483 ¤t_etc_files.root,
484 &pristine_etc_files.leaves,
485 ¤t_etc_files.leaves,
486 &new_etc_files.root,
487 PathBuf::new(),
488 &mut diff,
489 )?;
490
491 get_deletions(
492 &pristine_etc_files.root,
493 ¤t_etc_files.root,
494 PathBuf::new(),
495 &mut diff,
496 )?;
497
498 Ok(diff)
499}
500
501pub fn print_unmergable_paths(diff: &Diff, writer: &mut impl Write) {
502 use owo_colors::OwoColorize;
503
504 for unmergable in &diff.unmergable_paths {
505 let _ = writeln!(
506 writer,
507 "{} {}",
508 ModificationType::Unmergable.magenta(),
509 unmergable.reason
510 );
511 }
512}
513
514pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
516 use owo_colors::OwoColorize;
517
518 for added in &diff.added {
519 let _ = writeln!(writer, "{} {added:?}", ModificationType::Added.green());
520 }
521
522 for modified in &diff.modified {
523 let _ = writeln!(writer, "{} {modified:?}", ModificationType::Modified.cyan());
524 }
525
526 for removed in &diff.removed {
527 let _ = writeln!(writer, "{} {removed:?}", ModificationType::Removed.red());
528 }
529
530 print_unmergable_paths(diff, writer);
531}
532
533#[context("Collecting xattrs")]
534fn collect_xattrs(etc_fd: &CapStdDir, rel_path: impl AsRef<Path>) -> anyhow::Result<Xattrs> {
535 let link = format!("/proc/self/fd/{}", etc_fd.as_fd().as_raw_fd());
536 let path = Path::new(&link).join(rel_path);
537
538 const DEFAULT_SIZE: usize = 128;
539
540 let mut xattrs_name_buf: Vec<u8> = vec![0; DEFAULT_SIZE];
542 let mut size = llistxattr(&path, &mut xattrs_name_buf).context("llistxattr")?;
543
544 if size > xattrs_name_buf.capacity() {
545 xattrs_name_buf.resize(size, 0);
546 size = llistxattr(&path, &mut xattrs_name_buf).context("llistxattr")?;
547 }
548
549 let mut xattrs: Xattrs = BTreeMap::new();
550
551 for name_buf in xattrs_name_buf[..size]
552 .split(|&b| b == 0)
553 .filter(|x| !x.is_empty())
554 {
555 let name = OsStr::from_bytes(name_buf);
556
557 let mut xattrs_value_buf = vec![0; DEFAULT_SIZE];
558 let mut size = lgetxattr(&path, name_buf, &mut xattrs_value_buf).context("lgetxattr")?;
559
560 if size > xattrs_value_buf.capacity() {
561 xattrs_value_buf.resize(size, 0);
562 size = lgetxattr(&path, name_buf, &mut xattrs_value_buf).context("lgetxattr")?;
563 }
564
565 xattrs.insert(
566 Box::<OsStr>::from(name),
567 Box::<[u8]>::from(&xattrs_value_buf[..size]),
568 );
569 }
570
571 Ok(xattrs)
572}
573
574#[context("Copying xattrs")]
575fn copy_xattrs(xattrs: &Xattrs, new_etc_fd: &CapStdDir, path: &Path) -> anyhow::Result<()> {
576 for (attr, value) in xattrs.iter() {
577 let fdpath = &Path::new(&format!("/proc/self/fd/{}", new_etc_fd.as_raw_fd())).join(path);
578 lsetxattr(fdpath, attr.as_ref(), value, XattrFlags::empty())
579 .with_context(|| format!("setxattr {attr:?} for {fdpath:?}"))?;
580 }
581
582 Ok(())
583}
584
585fn recurse_dir(
586 dir: &CapStdDir,
587 root: &mut Directory<CustomMetadata>,
588 leaves: &mut Vec<Leaf<CustomMetadata>>,
589) -> anyhow::Result<()> {
590 for entry in dir.entries()? {
591 let entry = entry.context(format!("Getting entry"))?;
592 let entry_name = entry.file_name();
593
594 let entry_type = entry.file_type()?;
595
596 let entry_meta = entry
597 .metadata()
598 .context(format!("Getting metadata for {entry_name:?}"))?;
599
600 let xattrs = collect_xattrs(&dir, &entry_name)?;
601
602 if entry_type.is_symlink() {
604 let readlinkat_result = readlinkat(&dir, &entry_name, vec![])
605 .context(format!("readlinkat {entry_name:?}"))?;
606
607 let os_str = OsStr::from_bytes(readlinkat_result.as_bytes());
608
609 let id = LeafId(leaves.len());
610 leaves.push(Leaf {
611 stat: MyStat::from((&entry_meta, xattrs)).0,
612 content: LeafContent::Symlink(Box::from(os_str)),
613 });
614 root.insert(&entry_name, Inode::leaf(id));
615
616 continue;
617 }
618
619 if entry_type.is_dir() {
620 let dir = dir
621 .open_dir(&entry_name)
622 .with_context(|| format!("Opening dir {entry_name:?} inside {dir:?}"))?;
623
624 let mut directory = Directory::new(MyStat::from((&entry_meta, xattrs)).0);
625
626 recurse_dir(&dir, &mut directory, leaves)?;
627
628 root.insert(&entry_name, Inode::Directory(Box::new(directory)));
629
630 continue;
631 }
632
633 if !(entry_type.is_symlink() || entry_type.is_file()) {
634 tracing::debug!("Ignoring non-regular/non-symlink file: {:?}", entry_name);
637 continue;
638 }
639
640 let measured_verity =
644 composefs::fsverity::measure_verity_opt::<Sha256HashValue>(entry.open()?);
645
646 let measured_verity = match measured_verity {
647 Ok(mv) => mv.map(|verity| verity.to_hex()),
648
649 Err(composefs::fsverity::MeasureVerityError::InvalidDigestAlgorithm { .. }) => {
650 composefs::fsverity::measure_verity_opt::<Sha512HashValue>(entry.open()?)?
651 .map(|verity| verity.to_hex())
652 }
653
654 Err(e) => Err(e)?,
655 };
656
657 if let Some(measured_verity) = measured_verity {
658 let id = LeafId(leaves.len());
659 leaves.push(Leaf {
660 stat: MyStat::from((&entry_meta, xattrs)).0,
661 content: LeafContent::Regular(CustomMetadata::new(
662 "".into(),
663 Some(measured_verity),
664 )),
665 });
666 root.insert(&entry_name, Inode::leaf(id));
667
668 continue;
669 }
670
671 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
672
673 let file = entry
674 .open()
675 .context(format!("Opening entry {entry_name:?}"))?;
676
677 let mut reader = BufReader::new(file);
678 std::io::copy(&mut reader, &mut hasher)?;
679
680 let content_digest = hex::encode(hasher.finish()?);
681
682 let id = LeafId(leaves.len());
683 leaves.push(Leaf {
684 stat: MyStat::from((&entry_meta, xattrs)).0,
685 content: LeafContent::Regular(CustomMetadata::new(content_digest, None)),
686 });
687 root.insert(&entry_name, Inode::leaf(id));
688 }
689
690 Ok(())
691}
692
693#[derive(Debug)]
694enum ModificationType {
695 Added,
696 Modified,
697 Removed,
698 Unmergable,
699}
700
701impl std::fmt::Display for ModificationType {
702 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
703 write!(f, "{:?}", self)
704 }
705}
706
707impl ModificationType {
708 fn symbol(&self) -> &'static str {
709 match self {
710 ModificationType::Added => "+",
711 ModificationType::Modified => "~",
712 ModificationType::Removed => "-",
713 ModificationType::Unmergable => "*",
714 }
715 }
716}
717
718fn create_dir_with_perms(
719 new_etc_fd: &CapStdDir,
720 dir_name: &PathBuf,
721 stat: &Stat,
722 new_inode: Option<&Inode<CustomMetadata>>,
723) -> anyhow::Result<()> {
724 match new_inode {
725 Some(inode) => match inode {
726 Inode::Directory(..) => { }
727
728 Inode::Leaf(..) => {
729 anyhow::bail!(
730 "Modified config directory {dir_name:?} newly defaults to file. Cannot merge"
731 )
732 }
733 },
734
735 None => {
738 new_etc_fd
750 .create_dir_all(&dir_name)
751 .context(format!("Failed to create dir {dir_name:?}"))?;
752 }
753 }
754
755 new_etc_fd
756 .set_permissions(&dir_name, Permissions::from_mode(stat.st_mode))
757 .context(format!("Changing permissions for dir {dir_name:?}"))?;
758
759 rustix::fs::chownat(
760 &new_etc_fd,
761 dir_name,
762 Some(Uid::from_raw(stat.st_uid)),
763 Some(Gid::from_raw(stat.st_gid)),
764 AtFlags::SYMLINK_NOFOLLOW,
765 )
766 .context(format!("chown {dir_name:?}"))?;
767
768 copy_xattrs(&stat.xattrs, new_etc_fd, dir_name)?;
769
770 Ok(())
771}
772
773fn merge_leaf(
774 current_etc_fd: &CapStdDir,
775 new_etc_fd: &CapStdDir,
776 leaf: &Leaf<CustomMetadata>,
777 new_inode: Option<&Inode<CustomMetadata>>,
778 file: &PathBuf,
779) -> anyhow::Result<()> {
780 let symlink = match &leaf.content {
781 LeafContent::Regular(..) => None,
782 LeafContent::Symlink(target) => Some(target),
783
784 _ => {
785 tracing::debug!("Found non file/symlink while merging. Ignoring");
786 return Ok(());
787 }
788 };
789
790 if matches!(new_inode, Some(Inode::Directory(..))) {
791 anyhow::bail!("Modified config file {file:?} newly defaults to directory. Cannot merge")
792 };
793
794 new_etc_fd
796 .remove_all_optional(&file)
797 .context(format!("Deleting {file:?}"))?;
798
799 if let Some(target) = symlink {
800 symlinkat(&**target, new_etc_fd, file).context(format!("Creating symlink {file:?}"))?;
802 } else {
803 current_etc_fd
804 .copy(&file, new_etc_fd, &file)
805 .with_context(|| format!("Copying file {file:?}"))?;
806 };
807
808 rustix::fs::chownat(
809 &new_etc_fd,
810 file,
811 Some(Uid::from_raw(leaf.stat.st_uid)),
812 Some(Gid::from_raw(leaf.stat.st_gid)),
813 AtFlags::SYMLINK_NOFOLLOW,
814 )
815 .context(format!("chown {file:?}"))?;
816
817 copy_xattrs(&leaf.stat.xattrs, new_etc_fd, file)?;
818
819 Ok(())
820}
821
822fn merge_modified_files(
823 files: &Vec<PathBuf>,
824 current_etc_fd: &CapStdDir,
825 current_etc_dirtree: &Directory<CustomMetadata>,
826 current_leaves: &[Leaf<CustomMetadata>],
827 new_etc_fd: &CapStdDir,
828 new_etc_dirtree: &Directory<CustomMetadata>,
829) -> anyhow::Result<()> {
830 for file in files {
831 let (dir, filename) = current_etc_dirtree
832 .split(OsStr::new(&file))
833 .context("Getting directory and file")?;
834
835 let current_inode = dir
836 .lookup(filename)
837 .ok_or_else(|| anyhow::anyhow!("{filename:?} not found"))?;
838
839 let res = new_etc_dirtree.split(OsStr::new(&file));
841
842 match res {
843 Ok((new_dir, filename)) => {
844 let new_inode = new_dir.lookup(filename);
845
846 match current_inode {
847 Inode::Directory(..) => {
848 create_dir_with_perms(
849 new_etc_fd,
850 file,
851 current_inode.stat(current_leaves),
852 new_inode,
853 )
854 .context("Merging directory")?;
855 }
856
857 Inode::Leaf(leaf_id, _) => {
858 let leaf = ¤t_leaves[leaf_id.0];
859 merge_leaf(current_etc_fd, new_etc_fd, leaf, new_inode, file)
860 .context("Merging leaf")?
861 }
862 };
863 }
864
865 Err(ImageError::NotFound(..)) => match current_inode {
867 Inode::Directory(..) => create_dir_with_perms(
868 new_etc_fd,
869 file,
870 current_inode.stat(current_leaves),
871 None,
872 )?,
873
874 Inode::Leaf(leaf_id, _) => {
875 let leaf = ¤t_leaves[leaf_id.0];
876 merge_leaf(current_etc_fd, new_etc_fd, leaf, None, file)?;
877 }
878 },
879
880 Err(e) => Err(e).with_context(|| format!("Opening {file:?} in new etc"))?,
881 };
882 }
883
884 Ok(())
885}
886
887#[context("Merging")]
891pub fn merge(
892 current_etc_fd: &CapStdDir,
893 current_etc_dirtree: &FileSystem<CustomMetadata>,
894 new_etc_fd: &CapStdDir,
895 new_etc_dirtree: &FileSystem<CustomMetadata>,
896 diff: &Diff,
897) -> anyhow::Result<()> {
898 merge_modified_files(
899 &diff.added,
900 current_etc_fd,
901 ¤t_etc_dirtree.root,
902 ¤t_etc_dirtree.leaves,
903 new_etc_fd,
904 &new_etc_dirtree.root,
905 )
906 .context("Merging added files")?;
907
908 merge_modified_files(
909 &diff.modified,
910 current_etc_fd,
911 ¤t_etc_dirtree.root,
912 ¤t_etc_dirtree.leaves,
913 new_etc_fd,
914 &new_etc_dirtree.root,
915 )
916 .context("Merging modified files")?;
917
918 for removed in &diff.removed {
919 let stat = new_etc_fd.symlink_metadata_optional(&removed)?;
922
923 let Some(stat) = stat else {
924 continue;
927 };
928
929 if stat.is_file() || stat.is_symlink() {
930 new_etc_fd.remove_file(&removed)?;
931 } else if stat.is_dir() {
932 new_etc_fd.remove_dir_all(&removed)?;
935 }
936 }
937
938 Ok(())
939}
940
941#[cfg(test)]
942mod tests {
943 use cap_std::fs::PermissionsExt;
944 use cap_std_ext::cap_std::fs::Metadata;
945
946 use super::*;
947
948 const FILES: &[(&str, &str)] = &[
949 ("a/file1", "a-file1"),
950 ("a/file2", "a-file2"),
951 ("a/b/file1", "ab-file1"),
952 ("a/b/file2", "ab-file2"),
953 ("a/b/c/fileabc", "abc-file1"),
954 ("a/b/c/modify-perms", "modify-perms"),
955 ("a/b/c/to-be-removed", "remove this"),
956 ("to-be-removed", "remove this 2"),
957 ];
958
959 #[test]
960 fn test_etc_diff_plus_merge() -> anyhow::Result<()> {
961 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
962
963 tempdir.create_dir("pristine_etc")?;
964 tempdir.create_dir("current_etc")?;
965 tempdir.create_dir("new_etc")?;
966
967 let p = tempdir.open_dir("pristine_etc")?;
968 let c = tempdir.open_dir("current_etc")?;
969 let n = tempdir.open_dir("new_etc")?;
970
971 p.create_dir_all("a/b/c")?;
972 c.create_dir_all("a/b/c")?;
973
974 for (file, content) in FILES {
975 p.write(file, content.as_bytes())?;
976 c.write(file, content.as_bytes())?;
977 }
978
979 let new_files = ["new_file", "a/new_file", "a/b/c/new_file"];
980
981 for file in new_files {
983 c.write(file, b"hello")?;
984 }
985
986 let overwritten_files = [FILES[1].0, FILES[4].0];
987 let perm_changed_files = [FILES[5].0];
988
989 c.write(overwritten_files[0], b"some new content")?;
991 c.write(overwritten_files[1], b"some newer content")?;
992
993 let file = c.open(perm_changed_files[0])?;
995 file.set_permissions(cap_std::fs::Permissions::from_mode(0o400))?;
997
998 let deleted_files = [FILES[6].0, FILES[7].0];
1000 c.remove_file(deleted_files[0])?;
1001 c.remove_file(deleted_files[1])?;
1002
1003 let (pristine_etc_files, current_etc_files, new_etc_files) =
1004 traverse_etc(&p, &c, Some(&n))?;
1005
1006 let res = compute_diff(
1007 &pristine_etc_files,
1008 ¤t_etc_files,
1009 new_etc_files.as_ref().unwrap(),
1010 )?;
1011
1012 merge(
1013 &c,
1014 ¤t_etc_files,
1015 &n,
1016 new_etc_files.as_ref().unwrap(),
1017 &res,
1018 )
1019 .expect("Merge failed");
1020
1021 let added_dirs = ["a", "a/b", "a/b/c"];
1022
1023 assert_eq!(res.added.len(), new_files.len() + added_dirs.len());
1025
1026 let all_modified_files = overwritten_files
1028 .iter()
1029 .chain(&perm_changed_files)
1030 .collect::<Vec<_>>();
1031
1032 assert_eq!(res.modified.len(), all_modified_files.len());
1033 assert!(res.modified.iter().all(|file| {
1034 all_modified_files
1035 .iter()
1036 .find(|x| PathBuf::from(*x) == *file)
1037 .is_some()
1038 }));
1039
1040 assert_eq!(res.removed.len(), deleted_files.len());
1042 assert!(res.removed.iter().all(|file| {
1043 deleted_files
1044 .iter()
1045 .find(|x| PathBuf::from(*x) == *file)
1046 .is_some()
1047 }));
1048
1049 Ok(())
1050 }
1051
1052 fn compare_meta(meta1: Metadata, meta2: Metadata) -> bool {
1053 return meta1.is_file() == meta2.is_file()
1054 && meta1.is_dir() == meta2.is_dir()
1055 && meta1.is_symlink() == meta2.is_symlink()
1056 && meta1.mode() == meta2.mode()
1057 && meta1.uid() == meta2.uid()
1058 && meta1.gid() == meta2.gid();
1059 }
1060
1061 fn files_eq(current_etc: &CapStdDir, new_etc: &CapStdDir, path: &str) -> anyhow::Result<bool> {
1062 return Ok(
1063 compare_meta(current_etc.metadata(path)?, new_etc.metadata(path)?)
1064 && current_etc.read(path)? == new_etc.read(path)?,
1065 );
1066 }
1067
1068 #[test]
1069 fn test_merge() -> anyhow::Result<()> {
1070 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1071
1072 tempdir.create_dir("pristine_etc")?;
1073 tempdir.create_dir("current_etc")?;
1074 tempdir.create_dir("new_etc")?;
1075
1076 let p = tempdir.open_dir("pristine_etc")?;
1077 let c = tempdir.open_dir("current_etc")?;
1078 let n = tempdir.open_dir("new_etc")?;
1079
1080 p.create_dir_all("a/b")?;
1081 c.create_dir_all("a/b")?;
1082 n.create_dir_all("a/b")?;
1083
1084 c.write("new_file.txt", "text1")?;
1087 c.write("a/new_file.txt", "text2")?;
1088 c.write("a/b/new_file.txt", "text3")?;
1089
1090 c.write("present_file.txt", "new-present-text1")?;
1092 c.write("a/present_file.txt", "new-present-text2")?;
1093 c.write("a/b/present_file.txt", "new-present-text3")?;
1094
1095 n.write("present_file.txt", "present-text1")?;
1096 n.write("a/present_file.txt", "present-text2")?;
1097 n.write("a/b/present_file.txt", "present-text3")?;
1098
1099 p.write("content-modify.txt", "old-content1")?;
1101 p.write("a/content-modify.txt", "old-content2")?;
1102 p.write("a/b/content-modify.txt", "old-content3")?;
1103
1104 c.write("content-modify.txt", "new-content1")?;
1105 c.write("a/content-modify.txt", "new-content2")?;
1106 c.write("a/b/content-modify.txt", "new-content3")?;
1107
1108 p.write("content-modify-present.txt", "old-present-content1")?;
1110 p.write("a/content-modify-present.txt", "old-present-content2")?;
1111 p.write("a/b/content-modify-present.txt", "old-present-content3")?;
1112
1113 c.write("content-modify-present.txt", "current-present-content1")?;
1114 c.write("a/content-modify-present.txt", "current-present-content2")?;
1115 c.write("a/b/content-modify-present.txt", "current-present-content3")?;
1116
1117 n.write("content-modify-present.txt", "new-present-content1")?;
1118 n.write("a/content-modify-present.txt", "new-present-content2")?;
1119 n.write("a/b/content-modify-present.txt", "new-present-content3")?;
1120
1121 p.write("permission-modify.txt", "old-content1")?;
1123 p.write("a/permission-modify.txt", "old-content2")?;
1124 p.write("a/b/permission-modify.txt", "old-content3")?;
1125
1126 c.atomic_write_with_perms(
1127 "permission-modify.txt",
1128 "old-content1",
1129 Permissions::from_mode(0o755),
1130 )?;
1131 c.atomic_write_with_perms(
1132 "a/permission-modify.txt",
1133 "old-content2",
1134 Permissions::from_mode(0o766),
1135 )?;
1136 c.atomic_write_with_perms(
1137 "a/b/permission-modify.txt",
1138 "old-content3",
1139 Permissions::from_mode(0o744),
1140 )?;
1141
1142 p.write("permission-modify-present.txt", "old-present-content1")?;
1144 p.write("a/permission-modify-present.txt", "old-present-content2")?;
1145 p.write("a/b/permission-modify-present.txt", "old-present-content3")?;
1146
1147 c.atomic_write_with_perms(
1148 "permission-modify-present.txt",
1149 "old-present-content1",
1150 Permissions::from_mode(0o755),
1151 )?;
1152 c.atomic_write_with_perms(
1153 "a/permission-modify-present.txt",
1154 "old-present-content2",
1155 Permissions::from_mode(0o766),
1156 )?;
1157 c.atomic_write_with_perms(
1158 "a/b/permission-modify-present.txt",
1159 "old-present-content3",
1160 Permissions::from_mode(0o744),
1161 )?;
1162
1163 n.write("permission-modify-present.txt", "new-present-content1")?;
1164 n.write("a/permission-modify-present.txt", "old-present-content2")?;
1165 n.write("a/b/permission-modify-present.txt", "new-present-content3")?;
1166
1167 c.create_dir_all("new/dir/tree/here")?;
1169
1170 p.create_dir_all("existing/tree")?;
1172 c.create_dir_all("existing/tree/another/dir/tree")?;
1173 c.write(
1174 "existing/tree/another/dir/tree/file.txt",
1175 "dir-tree-contents",
1176 )?;
1177
1178 p.create_dir_all("dir/perms")?;
1180 p.create_dir_all("dir/perms/wo")?;
1181 p.create_dir_all("dir/perms/wo/ro")?;
1182
1183 c.create_dir_all("dir/perms")?;
1184 c.set_permissions("dir/perms", Permissions::from_mode(0o777))?;
1185
1186 c.create_dir_all("dir/perms/rwx")?;
1187 c.set_permissions("dir/perms/rwx", Permissions::from_mode(0o777))?;
1188
1189 c.create_dir_all("dir/perms/wo")?;
1190 c.set_permissions("dir/perms/wo", Permissions::from_mode(0o733))?;
1191
1192 c.create_dir_all("dir/perms/wo/ro")?;
1193 c.set_permissions("dir/perms/wo/ro", Permissions::from_mode(0o775))?;
1194
1195 n.create_dir_all("dir/perms")?;
1196 n.write("dir/perms/some-file", "Some-file")?;
1197
1198 p.write("ext-symlink", "pristine content")?;
1202 symlinkat("/usr/bin/bash", &n, "ext-symlink")?;
1203
1204 let (pristine_etc_files, current_etc_files, new_etc_files) =
1205 traverse_etc(&p, &c, Some(&n))?;
1206 let diff = compute_diff(
1207 &pristine_etc_files,
1208 ¤t_etc_files,
1209 &new_etc_files.as_ref().unwrap(),
1210 )?;
1211 merge(&c, ¤t_etc_files, &n, &new_etc_files.unwrap(), &diff)?;
1212
1213 assert!(files_eq(&c, &n, "new_file.txt")?);
1214 assert!(files_eq(&c, &n, "a/new_file.txt")?);
1215 assert!(files_eq(&c, &n, "a/b/new_file.txt")?);
1216
1217 assert!(files_eq(&c, &n, "present_file.txt")?);
1218 assert!(files_eq(&c, &n, "a/present_file.txt")?);
1219 assert!(files_eq(&c, &n, "a/b/present_file.txt")?);
1220
1221 assert!(files_eq(&c, &n, "content-modify.txt")?);
1222 assert!(files_eq(&c, &n, "a/content-modify.txt")?);
1223 assert!(files_eq(&c, &n, "a/b/content-modify.txt")?);
1224
1225 assert!(files_eq(&c, &n, "content-modify-present.txt")?);
1226 assert!(files_eq(&c, &n, "a/content-modify-present.txt")?);
1227 assert!(files_eq(&c, &n, "a/b/content-modify-present.txt")?);
1228
1229 assert!(files_eq(&c, &n, "permission-modify.txt")?);
1230 assert!(files_eq(&c, &n, "a/permission-modify.txt")?);
1231 assert!(files_eq(&c, &n, "a/b/permission-modify.txt")?);
1232
1233 assert!(files_eq(&c, &n, "permission-modify-present.txt")?);
1234 assert!(files_eq(&c, &n, "a/permission-modify-present.txt")?);
1235 assert!(files_eq(&c, &n, "a/b/permission-modify-present.txt")?);
1236
1237 assert!(n.exists("new/dir/tree/here"));
1238 assert!(n.exists("existing/tree/another/dir/tree"));
1239 assert!(files_eq(&c, &n, "existing/tree/another/dir/tree/file.txt")?);
1240
1241 assert!(compare_meta(
1242 c.metadata("dir/perms")?,
1243 n.metadata("dir/perms")?
1244 ));
1245
1246 assert!(n.exists("dir/perms/some-file"));
1248
1249 const DIR_BITS: u32 = 0o040000;
1250
1251 assert_eq!(
1252 n.metadata("dir/perms/rwx").unwrap().mode(),
1253 DIR_BITS | 0o777
1254 );
1255 assert_eq!(n.metadata("dir/perms/wo").unwrap().mode(), DIR_BITS | 0o733);
1256 assert_eq!(
1257 n.metadata("dir/perms/wo/ro").unwrap().mode(),
1258 DIR_BITS | 0o775
1259 );
1260
1261 assert!(!n.exists("ext-symlink"));
1263
1264 Ok(())
1265 }
1266
1267 #[test]
1268 fn file_to_dir() -> anyhow::Result<()> {
1269 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1270
1271 tempdir.create_dir("pristine_etc")?;
1272 tempdir.create_dir("current_etc")?;
1273 tempdir.create_dir("new_etc")?;
1274
1275 let p = tempdir.open_dir("pristine_etc")?;
1276 let c = tempdir.open_dir("current_etc")?;
1277 let n = tempdir.open_dir("new_etc")?;
1278
1279 p.write("file-to-dir", "some text")?;
1280 c.write("file-to-dir", "some text 1")?;
1281
1282 n.create_dir_all("file-to-dir")?;
1283
1284 let (pristine_etc_files, current_etc_files, new_etc_files) =
1285 traverse_etc(&p, &c, Some(&n))?;
1286 let diff = compute_diff(
1287 &pristine_etc_files,
1288 ¤t_etc_files,
1289 &new_etc_files.as_ref().unwrap(),
1290 )?;
1291
1292 let merge_res = merge(&c, ¤t_etc_files, &n, &new_etc_files.unwrap(), &diff);
1293
1294 assert!(merge_res.is_err());
1295 assert_eq!(
1296 merge_res.unwrap_err().root_cause().to_string(),
1297 "Modified config file \"file-to-dir\" newly defaults to directory. Cannot merge"
1298 );
1299
1300 Ok(())
1301 }
1302}