1#![allow(dead_code)]
4
5use fn_error_context::context;
6use std::cell::RefCell;
7use std::collections::BTreeMap;
8use std::ffi::OsStr;
9use std::io::BufReader;
10use std::io::Write;
11use std::os::fd::{AsFd, AsRawFd};
12use std::os::unix::ffi::OsStrExt;
13use std::path::{Path, PathBuf};
14use std::rc::Rc;
15
16use anyhow::Context;
17use cap_std_ext::cap_std;
18use cap_std_ext::cap_std::fs::{Dir as CapStdDir, MetadataExt, Permissions, PermissionsExt};
19use cap_std_ext::dirext::CapStdExtDirExt;
20use composefs::fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
21use composefs::generic_tree::{Directory, Inode, Leaf, LeafContent, Stat};
22use composefs::tree::ImageError;
23use rustix::fs::{
24 AtFlags, Gid, Uid, XattrFlags, lgetxattr, llistxattr, lsetxattr, readlinkat, symlinkat,
25};
26
27#[derive(Debug)]
29pub struct CustomMetadata {
30 content_hash: String,
32 verity: Option<String>,
34}
35
36impl CustomMetadata {
37 fn new(content_hash: String, verity: Option<String>) -> Self {
38 Self {
39 content_hash,
40 verity,
41 }
42 }
43}
44
45type Xattrs = RefCell<BTreeMap<Box<OsStr>, Box<[u8]>>>;
46
47struct MyStat(Stat);
48
49impl From<(&cap_std::fs::Metadata, Xattrs)> for MyStat {
50 fn from(value: (&cap_std::fs::Metadata, Xattrs)) -> Self {
51 Self(Stat {
52 st_mode: value.0.mode(),
53 st_uid: value.0.uid(),
54 st_gid: value.0.gid(),
55 st_mtim_sec: value.0.mtime(),
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)]
83pub struct Diff {
84 added: Vec<PathBuf>,
86 modified: Vec<PathBuf>,
89 removed: Vec<PathBuf>,
91}
92
93fn collect_all_files(
94 root: &Directory<CustomMetadata>,
95 current_path: PathBuf,
96 files: &mut Vec<PathBuf>,
97) {
98 fn collect(
99 root: &Directory<CustomMetadata>,
100 mut current_path: PathBuf,
101 files: &mut Vec<PathBuf>,
102 ) {
103 for (path, inode) in root.sorted_entries() {
104 current_path.push(path);
105
106 files.push(current_path.clone());
107
108 if let Inode::Directory(dir) = inode {
109 collect(dir, current_path.clone(), files);
110 }
111
112 current_path.pop();
113 }
114 }
115
116 collect(root, current_path, files);
117}
118
119#[context("Getting deletions")]
120fn get_deletions(
121 pristine: &Directory<CustomMetadata>,
122 current: &Directory<CustomMetadata>,
123 mut current_path: PathBuf,
124 diff: &mut Diff,
125) -> anyhow::Result<()> {
126 for (file_name, inode) in pristine.sorted_entries() {
127 current_path.push(file_name);
128
129 match inode {
130 Inode::Directory(pristine_dir) => {
131 match current.get_directory(file_name) {
132 Ok(curr_dir) => {
133 get_deletions(pristine_dir, curr_dir, current_path.clone(), diff)?
134 }
135
136 Err(ImageError::NotFound(..)) => {
137 diff.removed.push(current_path.clone());
139 }
140
141 Err(ImageError::NotADirectory(..)) => {
142 }
144
145 Err(e) => Err(e)?,
146 }
147 }
148
149 Inode::Leaf(..) => match current.ref_leaf(file_name) {
150 Ok(..) => {
151 }
153
154 Err(ImageError::NotFound(..)) => {
155 diff.removed.push(current_path.clone());
157 }
158
159 Err(ImageError::IsADirectory(..)) => {
160 }
162
163 Err(e) => Err(e).context(format!("{file_name:?}"))?,
164 },
165 }
166
167 current_path.pop();
168 }
169
170 Ok(())
171}
172
173#[context("Getting modifications")]
188fn get_modifications(
189 pristine: &Directory<CustomMetadata>,
190 current: &Directory<CustomMetadata>,
191 new: &Directory<CustomMetadata>,
192 mut current_path: PathBuf,
193 diff: &mut Diff,
194) -> anyhow::Result<()> {
195 use composefs::generic_tree::LeafContent::*;
196
197 for (path, inode) in current.sorted_entries() {
198 current_path.push(path);
199
200 match inode {
201 Inode::Directory(curr_dir) => {
202 match pristine.get_directory(path) {
203 Ok(old_dir) => {
204 if !stat_eq_ignore_mtime(&curr_dir.stat, &old_dir.stat) {
205 diff.modified.push(current_path.clone());
207 }
208
209 let total_added = diff.added.len();
210 let total_modified = diff.modified.len();
211
212 get_modifications(old_dir, &curr_dir, new, current_path.clone(), diff)?;
213
214 if new.get_directory_opt(¤t_path.as_os_str())?.is_none() {
218 if diff.added.len() != total_added {
219 diff.added.insert(total_added, current_path.clone());
220 } else if diff.modified.len() != total_modified {
221 diff.modified.insert(total_modified, current_path.clone());
222 }
223 }
224 }
225
226 Err(ImageError::NotFound(..)) => {
227 diff.added.push(current_path.clone());
229
230 collect_all_files(&curr_dir, current_path.clone(), &mut diff.added);
232 }
233
234 Err(ImageError::NotADirectory(..)) => {
235 diff.modified.push(current_path.clone());
238 }
239
240 Err(e) => Err(e)?,
241 }
242 }
243
244 Inode::Leaf(leaf) => match pristine.ref_leaf(path) {
245 Ok(old_leaf) => {
246 if !stat_eq_ignore_mtime(&old_leaf.stat, &leaf.stat) {
247 diff.modified.push(current_path.clone());
248 current_path.pop();
249 continue;
250 }
251
252 match (&old_leaf.content, &leaf.content) {
253 (Regular(old_meta), Regular(current_meta)) => {
254 if old_meta.content_hash != current_meta.content_hash {
255 diff.modified.push(current_path.clone());
257 }
258 }
259
260 (Symlink(old_link), Symlink(current_link)) => {
261 if old_link != current_link {
262 diff.modified.push(current_path.clone());
264 }
265 }
266
267 (Symlink(..), Regular(..)) | (Regular(..), Symlink(..)) => {
268 diff.modified.push(current_path.clone());
270 }
271
272 (a, b) => {
273 unreachable!("{a:?} modified to {b:?}")
274 }
275 }
276 }
277
278 Err(ImageError::IsADirectory(..)) => {
279 diff.modified.push(current_path.clone());
281 }
282
283 Err(ImageError::NotFound(..)) => {
284 diff.added.push(current_path.clone());
286 }
287
288 Err(e) => Err(e).context(format!("{path:?}"))?,
289 },
290 }
291
292 current_path.pop();
293 }
294
295 Ok(())
296}
297
298pub fn traverse_etc(
326 pristine_etc: &CapStdDir,
327 current_etc: &CapStdDir,
328 new_etc: Option<&CapStdDir>,
329) -> anyhow::Result<(
330 Directory<CustomMetadata>,
331 Directory<CustomMetadata>,
332 Option<Directory<CustomMetadata>>,
333)> {
334 let mut pristine_etc_files = Directory::new(Stat::uninitialized());
335 recurse_dir(pristine_etc, &mut pristine_etc_files)
336 .context(format!("Recursing {pristine_etc:?}"))?;
337
338 let mut current_etc_files = Directory::new(Stat::uninitialized());
339 recurse_dir(current_etc, &mut current_etc_files)
340 .context(format!("Recursing {current_etc:?}"))?;
341
342 let new_etc_files = match new_etc {
343 Some(new_etc) => {
344 let mut new_etc_files = Directory::new(Stat::uninitialized());
345 recurse_dir(new_etc, &mut new_etc_files).context(format!("Recursing {new_etc:?}"))?;
346
347 Some(new_etc_files)
348 }
349
350 None => None,
351 };
352
353 return Ok((pristine_etc_files, current_etc_files, new_etc_files));
354}
355
356#[context("Computing diff")]
358pub fn compute_diff(
359 pristine_etc_files: &Directory<CustomMetadata>,
360 current_etc_files: &Directory<CustomMetadata>,
361 new_etc_files: &Directory<CustomMetadata>,
362) -> anyhow::Result<Diff> {
363 let mut diff = Diff {
364 added: vec![],
365 modified: vec![],
366 removed: vec![],
367 };
368
369 get_modifications(
370 &pristine_etc_files,
371 ¤t_etc_files,
372 &new_etc_files,
373 PathBuf::new(),
374 &mut diff,
375 )?;
376
377 get_deletions(
378 &pristine_etc_files,
379 ¤t_etc_files,
380 PathBuf::new(),
381 &mut diff,
382 )?;
383
384 Ok(diff)
385}
386
387pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
389 use owo_colors::OwoColorize;
390
391 for added in &diff.added {
392 let _ = writeln!(writer, "{} {added:?}", ModificationType::Added.green());
393 }
394
395 for modified in &diff.modified {
396 let _ = writeln!(writer, "{} {modified:?}", ModificationType::Modified.cyan());
397 }
398
399 for removed in &diff.removed {
400 let _ = writeln!(writer, "{} {removed:?}", ModificationType::Removed.red());
401 }
402}
403
404#[context("Collecting xattrs")]
405fn collect_xattrs(etc_fd: &CapStdDir, rel_path: impl AsRef<Path>) -> anyhow::Result<Xattrs> {
406 let link = format!("/proc/self/fd/{}", etc_fd.as_fd().as_raw_fd());
407 let path = Path::new(&link).join(rel_path);
408
409 const DEFAULT_SIZE: usize = 128;
410
411 let mut xattrs_name_buf: Vec<u8> = vec![0; DEFAULT_SIZE];
413 let mut size = llistxattr(&path, &mut xattrs_name_buf).context("llistxattr")?;
414
415 if size > xattrs_name_buf.capacity() {
416 xattrs_name_buf.resize(size, 0);
417 size = llistxattr(&path, &mut xattrs_name_buf).context("llistxattr")?;
418 }
419
420 let xattrs: Xattrs = RefCell::new(BTreeMap::new());
421
422 for name_buf in xattrs_name_buf[..size]
423 .split(|&b| b == 0)
424 .filter(|x| !x.is_empty())
425 {
426 let name = OsStr::from_bytes(name_buf);
427
428 let mut xattrs_value_buf = vec![0; DEFAULT_SIZE];
429 let mut size = lgetxattr(&path, name_buf, &mut xattrs_value_buf).context("lgetxattr")?;
430
431 if size > xattrs_value_buf.capacity() {
432 xattrs_value_buf.resize(size, 0);
433 size = lgetxattr(&path, name_buf, &mut xattrs_value_buf).context("lgetxattr")?;
434 }
435
436 xattrs.borrow_mut().insert(
437 Box::<OsStr>::from(name),
438 Box::<[u8]>::from(&xattrs_value_buf[..size]),
439 );
440 }
441
442 Ok(xattrs)
443}
444
445#[context("Copying xattrs")]
446fn copy_xattrs(xattrs: &Xattrs, new_etc_fd: &CapStdDir, path: &Path) -> anyhow::Result<()> {
447 for (attr, value) in xattrs.borrow().iter() {
448 let fdpath = &Path::new(&format!("/proc/self/fd/{}", new_etc_fd.as_raw_fd())).join(path);
449 lsetxattr(fdpath, attr.as_ref(), value, XattrFlags::empty())
450 .with_context(|| format!("setxattr {attr:?} for {fdpath:?}"))?;
451 }
452
453 Ok(())
454}
455
456fn recurse_dir(dir: &CapStdDir, root: &mut Directory<CustomMetadata>) -> anyhow::Result<()> {
457 for entry in dir.entries()? {
458 let entry = entry.context(format!("Getting entry"))?;
459 let entry_name = entry.file_name();
460
461 let entry_type = entry.file_type()?;
462
463 let entry_meta = entry
464 .metadata()
465 .context(format!("Getting metadata for {entry_name:?}"))?;
466
467 let xattrs = collect_xattrs(&dir, &entry_name)?;
468
469 if entry_type.is_symlink() {
471 let readlinkat_result = readlinkat(&dir, &entry_name, vec![])
472 .context(format!("readlinkat {entry_name:?}"))?;
473
474 let os_str = OsStr::from_bytes(readlinkat_result.as_bytes());
475
476 root.insert(
477 &entry_name,
478 Inode::Leaf(Rc::new(Leaf {
479 stat: MyStat::from((&entry_meta, xattrs)).0,
480 content: LeafContent::Symlink(Box::from(os_str)),
481 })),
482 );
483
484 continue;
485 }
486
487 if entry_type.is_dir() {
488 let dir = dir
489 .open_dir(&entry_name)
490 .with_context(|| format!("Opening dir {entry_name:?} inside {dir:?}"))?;
491
492 let mut directory = Directory::new(MyStat::from((&entry_meta, xattrs)).0);
493
494 recurse_dir(&dir, &mut directory)?;
495
496 root.insert(&entry_name, Inode::Directory(Box::new(directory)));
497
498 continue;
499 }
500
501 if !(entry_type.is_symlink() || entry_type.is_file()) {
502 tracing::debug!("Ignoring non-regular/non-symlink file: {:?}", entry_name);
505 continue;
506 }
507
508 let measured_verity =
512 composefs::fsverity::measure_verity_opt::<Sha256HashValue>(entry.open()?);
513
514 let measured_verity = match measured_verity {
515 Ok(mv) => mv.map(|verity| verity.to_hex()),
516
517 Err(composefs::fsverity::MeasureVerityError::InvalidDigestAlgorithm { .. }) => {
518 composefs::fsverity::measure_verity_opt::<Sha512HashValue>(entry.open()?)?
519 .map(|verity| verity.to_hex())
520 }
521
522 Err(e) => Err(e)?,
523 };
524
525 if let Some(measured_verity) = measured_verity {
526 root.insert(
527 &entry_name,
528 Inode::Leaf(Rc::new(Leaf {
529 stat: MyStat::from((&entry_meta, xattrs)).0,
530 content: LeafContent::Regular(CustomMetadata::new(
531 "".into(),
532 Some(measured_verity),
533 )),
534 })),
535 );
536
537 continue;
538 }
539
540 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
541
542 let file = entry
543 .open()
544 .context(format!("Opening entry {entry_name:?}"))?;
545
546 let mut reader = BufReader::new(file);
547 std::io::copy(&mut reader, &mut hasher)?;
548
549 let content_digest = hex::encode(hasher.finish()?);
550
551 root.insert(
552 &entry_name,
553 Inode::Leaf(Rc::new(Leaf {
554 stat: MyStat::from((&entry_meta, xattrs)).0,
555 content: LeafContent::Regular(CustomMetadata::new(content_digest, None)),
556 })),
557 );
558 }
559
560 Ok(())
561}
562
563#[derive(Debug)]
564enum ModificationType {
565 Added,
566 Modified,
567 Removed,
568}
569
570impl std::fmt::Display for ModificationType {
571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572 write!(f, "{:?}", self)
573 }
574}
575
576impl ModificationType {
577 fn symbol(&self) -> &'static str {
578 match self {
579 ModificationType::Added => "+",
580 ModificationType::Modified => "~",
581 ModificationType::Removed => "-",
582 }
583 }
584}
585
586fn create_dir_with_perms(
587 new_etc_fd: &CapStdDir,
588 dir_name: &PathBuf,
589 stat: &Stat,
590 new_inode: Option<&Inode<CustomMetadata>>,
591) -> anyhow::Result<()> {
592 if new_inode.is_none() {
595 new_etc_fd
607 .create_dir_all(&dir_name)
608 .context(format!("Failed to create dir {dir_name:?}"))?;
609 }
610
611 new_etc_fd
612 .set_permissions(&dir_name, Permissions::from_mode(stat.st_mode))
613 .context(format!("Changing permissions for dir {dir_name:?}"))?;
614
615 rustix::fs::chownat(
616 &new_etc_fd,
617 dir_name,
618 Some(Uid::from_raw(stat.st_uid)),
619 Some(Gid::from_raw(stat.st_gid)),
620 AtFlags::SYMLINK_NOFOLLOW,
621 )
622 .context(format!("chown {dir_name:?}"))?;
623
624 copy_xattrs(&stat.xattrs, new_etc_fd, dir_name)?;
625
626 Ok(())
627}
628
629fn merge_leaf(
630 current_etc_fd: &CapStdDir,
631 new_etc_fd: &CapStdDir,
632 leaf: &Rc<Leaf<CustomMetadata>>,
633 new_inode: Option<&Inode<CustomMetadata>>,
634 file: &PathBuf,
635) -> anyhow::Result<()> {
636 let symlink = match &leaf.content {
637 LeafContent::Regular(..) => None,
638 LeafContent::Symlink(target) => Some(target),
639
640 _ => {
641 tracing::debug!("Found non file/symlink while merging. Ignoring");
642 return Ok(());
643 }
644 };
645
646 if matches!(new_inode, Some(Inode::Directory(..))) {
647 anyhow::bail!("Modified config file {file:?} newly defaults to directory. Cannot merge")
648 };
649
650 new_etc_fd
652 .remove_all_optional(&file)
653 .context(format!("Deleting {file:?}"))?;
654
655 if let Some(target) = symlink {
656 symlinkat(&**target, new_etc_fd, file).context(format!("Creating symlink {file:?}"))?;
658 } else {
659 current_etc_fd
660 .copy(&file, new_etc_fd, &file)
661 .with_context(|| format!("Copying file {file:?}"))?;
662 };
663
664 rustix::fs::chownat(
665 &new_etc_fd,
666 file,
667 Some(Uid::from_raw(leaf.stat.st_uid)),
668 Some(Gid::from_raw(leaf.stat.st_gid)),
669 AtFlags::SYMLINK_NOFOLLOW,
670 )
671 .context(format!("chown {file:?}"))?;
672
673 copy_xattrs(&leaf.stat.xattrs, new_etc_fd, file)?;
674
675 Ok(())
676}
677
678fn merge_modified_files(
679 files: &Vec<PathBuf>,
680 current_etc_fd: &CapStdDir,
681 current_etc_dirtree: &Directory<CustomMetadata>,
682 new_etc_fd: &CapStdDir,
683 new_etc_dirtree: &Directory<CustomMetadata>,
684) -> anyhow::Result<()> {
685 for file in files {
686 let (dir, filename) = current_etc_dirtree
687 .split(OsStr::new(&file))
688 .context("Getting directory and file")?;
689
690 let current_inode = dir
691 .lookup(filename)
692 .ok_or_else(|| anyhow::anyhow!("{filename:?} not found"))?;
693
694 let res = new_etc_dirtree.split(OsStr::new(&file));
696
697 match res {
698 Ok((new_dir, filename)) => {
699 let new_inode = new_dir.lookup(filename);
700
701 match current_inode {
702 Inode::Directory(..) => {
703 create_dir_with_perms(new_etc_fd, file, current_inode.stat(), new_inode)?;
704 }
705
706 Inode::Leaf(leaf) => {
707 merge_leaf(current_etc_fd, new_etc_fd, leaf, new_inode, file)?
708 }
709 };
710 }
711
712 Err(ImageError::NotFound(..)) => match current_inode {
714 Inode::Directory(..) => {
715 create_dir_with_perms(new_etc_fd, file, current_inode.stat(), None)?
716 }
717
718 Inode::Leaf(leaf) => {
719 merge_leaf(current_etc_fd, new_etc_fd, leaf, None, file)?;
720 }
721 },
722
723 Err(e) => Err(e)?,
724 };
725 }
726
727 Ok(())
728}
729
730#[context("Merging")]
734pub fn merge(
735 current_etc_fd: &CapStdDir,
736 current_etc_dirtree: &Directory<CustomMetadata>,
737 new_etc_fd: &CapStdDir,
738 new_etc_dirtree: &Directory<CustomMetadata>,
739 diff: &Diff,
740) -> anyhow::Result<()> {
741 merge_modified_files(
742 &diff.added,
743 current_etc_fd,
744 current_etc_dirtree,
745 new_etc_fd,
746 new_etc_dirtree,
747 )
748 .context("Merging added files")?;
749
750 merge_modified_files(
751 &diff.modified,
752 current_etc_fd,
753 current_etc_dirtree,
754 new_etc_fd,
755 new_etc_dirtree,
756 )
757 .context("Merging modified files")?;
758
759 for removed in &diff.removed {
760 let stat = new_etc_fd.metadata_optional(&removed)?;
761
762 let Some(stat) = stat else {
763 continue;
766 };
767
768 if stat.is_file() || stat.is_symlink() {
769 new_etc_fd.remove_file(&removed)?;
770 } else if stat.is_dir() {
771 new_etc_fd.remove_dir_all(&removed)?;
774 }
775 }
776
777 Ok(())
778}
779
780#[cfg(test)]
781mod tests {
782 use cap_std::fs::PermissionsExt;
783 use cap_std_ext::cap_std::fs::Metadata;
784
785 use super::*;
786
787 const FILES: &[(&str, &str)] = &[
788 ("a/file1", "a-file1"),
789 ("a/file2", "a-file2"),
790 ("a/b/file1", "ab-file1"),
791 ("a/b/file2", "ab-file2"),
792 ("a/b/c/fileabc", "abc-file1"),
793 ("a/b/c/modify-perms", "modify-perms"),
794 ("a/b/c/to-be-removed", "remove this"),
795 ("to-be-removed", "remove this 2"),
796 ];
797
798 #[test]
799 fn test_etc_diff_plus_merge() -> anyhow::Result<()> {
800 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
801
802 tempdir.create_dir("pristine_etc")?;
803 tempdir.create_dir("current_etc")?;
804 tempdir.create_dir("new_etc")?;
805
806 let p = tempdir.open_dir("pristine_etc")?;
807 let c = tempdir.open_dir("current_etc")?;
808 let n = tempdir.open_dir("new_etc")?;
809
810 p.create_dir_all("a/b/c")?;
811 c.create_dir_all("a/b/c")?;
812
813 for (file, content) in FILES {
814 p.write(file, content.as_bytes())?;
815 c.write(file, content.as_bytes())?;
816 }
817
818 let new_files = ["new_file", "a/new_file", "a/b/c/new_file"];
819
820 for file in new_files {
822 c.write(file, b"hello")?;
823 }
824
825 let overwritten_files = [FILES[1].0, FILES[4].0];
826 let perm_changed_files = [FILES[5].0];
827
828 c.write(overwritten_files[0], b"some new content")?;
830 c.write(overwritten_files[1], b"some newer content")?;
831
832 let file = c.open(perm_changed_files[0])?;
834 file.set_permissions(cap_std::fs::Permissions::from_mode(0o400))?;
836
837 let deleted_files = [FILES[6].0, FILES[7].0];
839 c.remove_file(deleted_files[0])?;
840 c.remove_file(deleted_files[1])?;
841
842 let (pristine_etc_files, current_etc_files, new_etc_files) =
843 traverse_etc(&p, &c, Some(&n))?;
844
845 let res = compute_diff(
846 &pristine_etc_files,
847 ¤t_etc_files,
848 new_etc_files.as_ref().unwrap(),
849 )?;
850
851 merge(
852 &c,
853 ¤t_etc_files,
854 &n,
855 new_etc_files.as_ref().unwrap(),
856 &res,
857 )
858 .expect("Merge failed");
859
860 let added_dirs = ["a", "a/b", "a/b/c"];
861
862 assert_eq!(res.added.len(), new_files.len() + added_dirs.len());
864
865 let all_modified_files = overwritten_files
867 .iter()
868 .chain(&perm_changed_files)
869 .collect::<Vec<_>>();
870
871 assert_eq!(res.modified.len(), all_modified_files.len());
872 assert!(res.modified.iter().all(|file| {
873 all_modified_files
874 .iter()
875 .find(|x| PathBuf::from(*x) == *file)
876 .is_some()
877 }));
878
879 assert_eq!(res.removed.len(), deleted_files.len());
881 assert!(res.removed.iter().all(|file| {
882 deleted_files
883 .iter()
884 .find(|x| PathBuf::from(*x) == *file)
885 .is_some()
886 }));
887
888 Ok(())
889 }
890
891 fn compare_meta(meta1: Metadata, meta2: Metadata) -> bool {
892 return meta1.is_file() == meta2.is_file()
893 && meta1.is_dir() == meta2.is_dir()
894 && meta1.is_symlink() == meta2.is_symlink()
895 && meta1.mode() == meta2.mode()
896 && meta1.uid() == meta2.uid()
897 && meta1.gid() == meta2.gid();
898 }
899
900 fn files_eq(current_etc: &CapStdDir, new_etc: &CapStdDir, path: &str) -> anyhow::Result<bool> {
901 return Ok(
902 compare_meta(current_etc.metadata(path)?, new_etc.metadata(path)?)
903 && current_etc.read(path)? == new_etc.read(path)?,
904 );
905 }
906
907 #[test]
908 fn test_merge() -> anyhow::Result<()> {
909 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
910
911 tempdir.create_dir("pristine_etc")?;
912 tempdir.create_dir("current_etc")?;
913 tempdir.create_dir("new_etc")?;
914
915 let p = tempdir.open_dir("pristine_etc")?;
916 let c = tempdir.open_dir("current_etc")?;
917 let n = tempdir.open_dir("new_etc")?;
918
919 p.create_dir_all("a/b")?;
920 c.create_dir_all("a/b")?;
921 n.create_dir_all("a/b")?;
922
923 c.write("new_file.txt", "text1")?;
926 c.write("a/new_file.txt", "text2")?;
927 c.write("a/b/new_file.txt", "text3")?;
928
929 c.write("present_file.txt", "new-present-text1")?;
931 c.write("a/present_file.txt", "new-present-text2")?;
932 c.write("a/b/present_file.txt", "new-present-text3")?;
933
934 n.write("present_file.txt", "present-text1")?;
935 n.write("a/present_file.txt", "present-text2")?;
936 n.write("a/b/present_file.txt", "present-text3")?;
937
938 p.write("content-modify.txt", "old-content1")?;
940 p.write("a/content-modify.txt", "old-content2")?;
941 p.write("a/b/content-modify.txt", "old-content3")?;
942
943 c.write("content-modify.txt", "new-content1")?;
944 c.write("a/content-modify.txt", "new-content2")?;
945 c.write("a/b/content-modify.txt", "new-content3")?;
946
947 p.write("content-modify-present.txt", "old-present-content1")?;
949 p.write("a/content-modify-present.txt", "old-present-content2")?;
950 p.write("a/b/content-modify-present.txt", "old-present-content3")?;
951
952 c.write("content-modify-present.txt", "current-present-content1")?;
953 c.write("a/content-modify-present.txt", "current-present-content2")?;
954 c.write("a/b/content-modify-present.txt", "current-present-content3")?;
955
956 n.write("content-modify-present.txt", "new-present-content1")?;
957 n.write("a/content-modify-present.txt", "new-present-content2")?;
958 n.write("a/b/content-modify-present.txt", "new-present-content3")?;
959
960 p.write("permission-modify.txt", "old-content1")?;
962 p.write("a/permission-modify.txt", "old-content2")?;
963 p.write("a/b/permission-modify.txt", "old-content3")?;
964
965 c.atomic_write_with_perms(
966 "permission-modify.txt",
967 "old-content1",
968 Permissions::from_mode(0o755),
969 )?;
970 c.atomic_write_with_perms(
971 "a/permission-modify.txt",
972 "old-content2",
973 Permissions::from_mode(0o766),
974 )?;
975 c.atomic_write_with_perms(
976 "a/b/permission-modify.txt",
977 "old-content3",
978 Permissions::from_mode(0o744),
979 )?;
980
981 p.write("permission-modify-present.txt", "old-present-content1")?;
983 p.write("a/permission-modify-present.txt", "old-present-content2")?;
984 p.write("a/b/permission-modify-present.txt", "old-present-content3")?;
985
986 c.atomic_write_with_perms(
987 "permission-modify-present.txt",
988 "old-present-content1",
989 Permissions::from_mode(0o755),
990 )?;
991 c.atomic_write_with_perms(
992 "a/permission-modify-present.txt",
993 "old-present-content2",
994 Permissions::from_mode(0o766),
995 )?;
996 c.atomic_write_with_perms(
997 "a/b/permission-modify-present.txt",
998 "old-present-content3",
999 Permissions::from_mode(0o744),
1000 )?;
1001
1002 n.write("permission-modify-present.txt", "new-present-content1")?;
1003 n.write("a/permission-modify-present.txt", "old-present-content2")?;
1004 n.write("a/b/permission-modify-present.txt", "new-present-content3")?;
1005
1006 c.create_dir_all("new/dir/tree/here")?;
1008
1009 p.create_dir_all("existing/tree")?;
1011 c.create_dir_all("existing/tree/another/dir/tree")?;
1012 c.write(
1013 "existing/tree/another/dir/tree/file.txt",
1014 "dir-tree-contents",
1015 )?;
1016
1017 p.create_dir_all("dir/perms")?;
1019 p.create_dir_all("dir/perms/wo")?;
1020 p.create_dir_all("dir/perms/wo/ro")?;
1021
1022 c.create_dir_all("dir/perms")?;
1023 c.set_permissions("dir/perms", Permissions::from_mode(0o777))?;
1024
1025 c.create_dir_all("dir/perms/rwx")?;
1026 c.set_permissions("dir/perms/rwx", Permissions::from_mode(0o777))?;
1027
1028 c.create_dir_all("dir/perms/wo")?;
1029 c.set_permissions("dir/perms/wo", Permissions::from_mode(0o733))?;
1030
1031 c.create_dir_all("dir/perms/wo/ro")?;
1032 c.set_permissions("dir/perms/wo/ro", Permissions::from_mode(0o775))?;
1033
1034 n.create_dir_all("dir/perms")?;
1035 n.write("dir/perms/some-file", "Some-file")?;
1036
1037 let (pristine_etc_files, current_etc_files, new_etc_files) =
1038 traverse_etc(&p, &c, Some(&n))?;
1039 let diff = compute_diff(
1040 &pristine_etc_files,
1041 ¤t_etc_files,
1042 &new_etc_files.as_ref().unwrap(),
1043 )?;
1044 merge(&c, ¤t_etc_files, &n, &new_etc_files.unwrap(), &diff)?;
1045
1046 assert!(files_eq(&c, &n, "new_file.txt")?);
1047 assert!(files_eq(&c, &n, "a/new_file.txt")?);
1048 assert!(files_eq(&c, &n, "a/b/new_file.txt")?);
1049
1050 assert!(files_eq(&c, &n, "present_file.txt")?);
1051 assert!(files_eq(&c, &n, "a/present_file.txt")?);
1052 assert!(files_eq(&c, &n, "a/b/present_file.txt")?);
1053
1054 assert!(files_eq(&c, &n, "content-modify.txt")?);
1055 assert!(files_eq(&c, &n, "a/content-modify.txt")?);
1056 assert!(files_eq(&c, &n, "a/b/content-modify.txt")?);
1057
1058 assert!(files_eq(&c, &n, "content-modify-present.txt")?);
1059 assert!(files_eq(&c, &n, "a/content-modify-present.txt")?);
1060 assert!(files_eq(&c, &n, "a/b/content-modify-present.txt")?);
1061
1062 assert!(files_eq(&c, &n, "permission-modify.txt")?);
1063 assert!(files_eq(&c, &n, "a/permission-modify.txt")?);
1064 assert!(files_eq(&c, &n, "a/b/permission-modify.txt")?);
1065
1066 assert!(files_eq(&c, &n, "permission-modify-present.txt")?);
1067 assert!(files_eq(&c, &n, "a/permission-modify-present.txt")?);
1068 assert!(files_eq(&c, &n, "a/b/permission-modify-present.txt")?);
1069
1070 assert!(n.exists("new/dir/tree/here"));
1071 assert!(n.exists("existing/tree/another/dir/tree"));
1072 assert!(files_eq(&c, &n, "existing/tree/another/dir/tree/file.txt")?);
1073
1074 assert!(compare_meta(
1075 c.metadata("dir/perms")?,
1076 n.metadata("dir/perms")?
1077 ));
1078
1079 assert!(n.exists("dir/perms/some-file"));
1081
1082 const DIR_BITS: u32 = 0o040000;
1083
1084 assert_eq!(
1085 n.metadata("dir/perms/rwx").unwrap().mode(),
1086 DIR_BITS | 0o777
1087 );
1088 assert_eq!(n.metadata("dir/perms/wo").unwrap().mode(), DIR_BITS | 0o733);
1089 assert_eq!(
1090 n.metadata("dir/perms/wo/ro").unwrap().mode(),
1091 DIR_BITS | 0o775
1092 );
1093
1094 Ok(())
1095 }
1096
1097 #[test]
1098 fn file_to_dir() -> anyhow::Result<()> {
1099 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1100
1101 tempdir.create_dir("pristine_etc")?;
1102 tempdir.create_dir("current_etc")?;
1103 tempdir.create_dir("new_etc")?;
1104
1105 let p = tempdir.open_dir("pristine_etc")?;
1106 let c = tempdir.open_dir("current_etc")?;
1107 let n = tempdir.open_dir("new_etc")?;
1108
1109 p.write("file-to-dir", "some text")?;
1110 c.write("file-to-dir", "some text 1")?;
1111
1112 n.create_dir_all("file-to-dir")?;
1113
1114 let (pristine_etc_files, current_etc_files, new_etc_files) =
1115 traverse_etc(&p, &c, Some(&n))?;
1116 let diff = compute_diff(
1117 &pristine_etc_files,
1118 ¤t_etc_files,
1119 &new_etc_files.as_ref().unwrap(),
1120 )?;
1121
1122 let merge_res = merge(&c, ¤t_etc_files, &n, &new_etc_files.unwrap(), &diff);
1123
1124 assert!(merge_res.is_err());
1125 assert_eq!(
1126 merge_res.unwrap_err().root_cause().to_string(),
1127 "Modified config file \"file-to-dir\" newly defaults to directory. Cannot merge"
1128 );
1129
1130 Ok(())
1131 }
1132}