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 mut current_path: PathBuf,
192 diff: &mut Diff,
193) -> anyhow::Result<()> {
194 use composefs::generic_tree::LeafContent::*;
195
196 for (path, inode) in current.sorted_entries() {
197 current_path.push(path);
198
199 match inode {
200 Inode::Directory(curr_dir) => {
201 match pristine.get_directory(path) {
202 Ok(old_dir) => {
203 if !stat_eq_ignore_mtime(&curr_dir.stat, &old_dir.stat) {
204 diff.modified.push(current_path.clone());
206 }
207
208 get_modifications(old_dir, &curr_dir, current_path.clone(), diff)?
209 }
210
211 Err(ImageError::NotFound(..)) => {
212 diff.added.push(current_path.clone());
214
215 collect_all_files(&curr_dir, current_path.clone(), &mut diff.added);
217 }
218
219 Err(ImageError::NotADirectory(..)) => {
220 diff.modified.push(current_path.clone());
223 }
224
225 Err(e) => Err(e)?,
226 }
227 }
228
229 Inode::Leaf(leaf) => match pristine.ref_leaf(path) {
230 Ok(old_leaf) => {
231 if !stat_eq_ignore_mtime(&old_leaf.stat, &leaf.stat) {
232 diff.modified.push(current_path.clone());
233 current_path.pop();
234 continue;
235 }
236
237 match (&old_leaf.content, &leaf.content) {
238 (Regular(old_meta), Regular(current_meta)) => {
239 if old_meta.content_hash != current_meta.content_hash {
240 diff.modified.push(current_path.clone());
242 }
243 }
244
245 (Symlink(old_link), Symlink(current_link)) => {
246 if old_link != current_link {
247 diff.modified.push(current_path.clone());
249 }
250 }
251
252 (Symlink(..), Regular(..)) | (Regular(..), Symlink(..)) => {
253 diff.modified.push(current_path.clone());
255 }
256
257 (a, b) => {
258 unreachable!("{a:?} modified to {b:?}")
259 }
260 }
261 }
262
263 Err(ImageError::IsADirectory(..)) => {
264 diff.modified.push(current_path.clone());
266 }
267
268 Err(ImageError::NotFound(..)) => {
269 diff.added.push(current_path.clone());
271 }
272
273 Err(e) => Err(e).context(format!("{path:?}"))?,
274 },
275 }
276
277 current_path.pop();
278 }
279
280 Ok(())
281}
282
283pub fn traverse_etc(
311 pristine_etc: &CapStdDir,
312 current_etc: &CapStdDir,
313 new_etc: Option<&CapStdDir>,
314) -> anyhow::Result<(
315 Directory<CustomMetadata>,
316 Directory<CustomMetadata>,
317 Option<Directory<CustomMetadata>>,
318)> {
319 let mut pristine_etc_files = Directory::default();
320 recurse_dir(pristine_etc, &mut pristine_etc_files)
321 .context(format!("Recursing {pristine_etc:?}"))?;
322
323 let mut current_etc_files = Directory::default();
324 recurse_dir(current_etc, &mut current_etc_files)
325 .context(format!("Recursing {current_etc:?}"))?;
326
327 let new_etc_files = match new_etc {
328 Some(new_etc) => {
329 let mut new_etc_files = Directory::default();
330 recurse_dir(new_etc, &mut new_etc_files).context(format!("Recursing {new_etc:?}"))?;
331
332 Some(new_etc_files)
333 }
334
335 None => None,
336 };
337
338 return Ok((pristine_etc_files, current_etc_files, new_etc_files));
339}
340
341#[context("Computing diff")]
343pub fn compute_diff(
344 pristine_etc_files: &Directory<CustomMetadata>,
345 current_etc_files: &Directory<CustomMetadata>,
346) -> anyhow::Result<Diff> {
347 let mut diff = Diff {
348 added: vec![],
349 modified: vec![],
350 removed: vec![],
351 };
352
353 get_modifications(
354 &pristine_etc_files,
355 ¤t_etc_files,
356 PathBuf::new(),
357 &mut diff,
358 )?;
359
360 get_deletions(
361 &pristine_etc_files,
362 ¤t_etc_files,
363 PathBuf::new(),
364 &mut diff,
365 )?;
366
367 Ok(diff)
368}
369
370pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
372 use owo_colors::OwoColorize;
373
374 for added in &diff.added {
375 let _ = writeln!(writer, "{} {added:?}", ModificationType::Added.green());
376 }
377
378 for modified in &diff.modified {
379 let _ = writeln!(writer, "{} {modified:?}", ModificationType::Modified.cyan());
380 }
381
382 for removed in &diff.removed {
383 let _ = writeln!(writer, "{} {removed:?}", ModificationType::Removed.red());
384 }
385}
386
387#[context("Collecting xattrs")]
388fn collect_xattrs(etc_fd: &CapStdDir, rel_path: impl AsRef<Path>) -> anyhow::Result<Xattrs> {
389 let link = format!("/proc/self/fd/{}", etc_fd.as_fd().as_raw_fd());
390 let path = Path::new(&link).join(rel_path);
391
392 const DEFAULT_SIZE: usize = 128;
393
394 let mut xattrs_name_buf: Vec<u8> = vec![0; DEFAULT_SIZE];
396 let mut size = llistxattr(&path, &mut xattrs_name_buf).context("llistxattr")?;
397
398 if size > xattrs_name_buf.capacity() {
399 xattrs_name_buf.resize(size, 0);
400 size = llistxattr(&path, &mut xattrs_name_buf).context("llistxattr")?;
401 }
402
403 let xattrs: Xattrs = RefCell::new(BTreeMap::new());
404
405 for name_buf in xattrs_name_buf[..size]
406 .split(|&b| b == 0)
407 .filter(|x| !x.is_empty())
408 {
409 let name = OsStr::from_bytes(name_buf);
410
411 let mut xattrs_value_buf = vec![0; DEFAULT_SIZE];
412 let mut size = lgetxattr(&path, name_buf, &mut xattrs_value_buf).context("lgetxattr")?;
413
414 if size > xattrs_value_buf.capacity() {
415 xattrs_value_buf.resize(size, 0);
416 size = lgetxattr(&path, name_buf, &mut xattrs_value_buf).context("lgetxattr")?;
417 }
418
419 xattrs.borrow_mut().insert(
420 Box::<OsStr>::from(name),
421 Box::<[u8]>::from(&xattrs_value_buf[..size]),
422 );
423 }
424
425 Ok(xattrs)
426}
427
428#[context("Copying xattrs")]
429fn copy_xattrs(xattrs: &Xattrs, new_etc_fd: &CapStdDir, path: &Path) -> anyhow::Result<()> {
430 for (attr, value) in xattrs.borrow().iter() {
431 let fdpath = &Path::new(&format!("/proc/self/fd/{}", new_etc_fd.as_raw_fd())).join(path);
432 lsetxattr(fdpath, attr.as_ref(), value, XattrFlags::empty())
433 .with_context(|| format!("setxattr {attr:?} for {fdpath:?}"))?;
434 }
435
436 Ok(())
437}
438
439fn recurse_dir(dir: &CapStdDir, root: &mut Directory<CustomMetadata>) -> anyhow::Result<()> {
440 for entry in dir.entries()? {
441 let entry = entry.context(format!("Getting entry"))?;
442 let entry_name = entry.file_name();
443
444 let entry_type = entry.file_type()?;
445
446 let entry_meta = entry
447 .metadata()
448 .context(format!("Getting metadata for {entry_name:?}"))?;
449
450 let xattrs = collect_xattrs(&dir, &entry_name)?;
451
452 if entry_type.is_symlink() {
454 let readlinkat_result = readlinkat(&dir, &entry_name, vec![])
455 .context(format!("readlinkat {entry_name:?}"))?;
456
457 let os_str = OsStr::from_bytes(readlinkat_result.as_bytes());
458
459 root.insert(
460 &entry_name,
461 Inode::Leaf(Rc::new(Leaf {
462 stat: MyStat::from((&entry_meta, xattrs)).0,
463 content: LeafContent::Symlink(Box::from(os_str)),
464 })),
465 );
466
467 continue;
468 }
469
470 if entry_type.is_dir() {
471 let dir = dir
472 .open_dir(&entry_name)
473 .with_context(|| format!("Opening dir {entry_name:?} inside {dir:?}"))?;
474
475 let mut directory = Directory::new(MyStat::from((&entry_meta, xattrs)).0);
476
477 recurse_dir(&dir, &mut directory)?;
478
479 root.insert(&entry_name, Inode::Directory(Box::new(directory)));
480
481 continue;
482 }
483
484 if !(entry_type.is_symlink() || entry_type.is_file()) {
485 tracing::debug!("Ignoring non-regular/non-symlink file: {:?}", entry_name);
488 continue;
489 }
490
491 let measured_verity =
495 composefs::fsverity::measure_verity_opt::<Sha256HashValue>(entry.open()?);
496
497 let measured_verity = match measured_verity {
498 Ok(mv) => mv.map(|verity| verity.to_hex()),
499
500 Err(composefs::fsverity::MeasureVerityError::InvalidDigestAlgorithm { .. }) => {
501 composefs::fsverity::measure_verity_opt::<Sha512HashValue>(entry.open()?)?
502 .map(|verity| verity.to_hex())
503 }
504
505 Err(e) => Err(e)?,
506 };
507
508 if let Some(measured_verity) = measured_verity {
509 root.insert(
510 &entry_name,
511 Inode::Leaf(Rc::new(Leaf {
512 stat: MyStat::from((&entry_meta, xattrs)).0,
513 content: LeafContent::Regular(CustomMetadata::new(
514 "".into(),
515 Some(measured_verity),
516 )),
517 })),
518 );
519
520 continue;
521 }
522
523 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
524
525 let file = entry
526 .open()
527 .context(format!("Opening entry {entry_name:?}"))?;
528
529 let mut reader = BufReader::new(file);
530 std::io::copy(&mut reader, &mut hasher)?;
531
532 let content_digest = hex::encode(hasher.finish()?);
533
534 root.insert(
535 &entry_name,
536 Inode::Leaf(Rc::new(Leaf {
537 stat: MyStat::from((&entry_meta, xattrs)).0,
538 content: LeafContent::Regular(CustomMetadata::new(content_digest, None)),
539 })),
540 );
541 }
542
543 Ok(())
544}
545
546#[derive(Debug)]
547enum ModificationType {
548 Added,
549 Modified,
550 Removed,
551}
552
553impl std::fmt::Display for ModificationType {
554 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555 write!(f, "{:?}", self)
556 }
557}
558
559impl ModificationType {
560 fn symbol(&self) -> &'static str {
561 match self {
562 ModificationType::Added => "+",
563 ModificationType::Modified => "~",
564 ModificationType::Removed => "-",
565 }
566 }
567}
568
569fn create_dir_with_perms(
570 new_etc_fd: &CapStdDir,
571 dir_name: &PathBuf,
572 stat: &Stat,
573 new_inode: Option<&Inode<CustomMetadata>>,
574) -> anyhow::Result<()> {
575 if new_inode.is_none() {
578 new_etc_fd
590 .create_dir_all(&dir_name)
591 .context(format!("Failed to create dir {dir_name:?}"))?;
592 }
593
594 new_etc_fd
595 .set_permissions(&dir_name, Permissions::from_mode(stat.st_mode))
596 .context(format!("Changing permissions for dir {dir_name:?}"))?;
597
598 rustix::fs::chownat(
599 &new_etc_fd,
600 dir_name,
601 Some(Uid::from_raw(stat.st_uid)),
602 Some(Gid::from_raw(stat.st_gid)),
603 AtFlags::SYMLINK_NOFOLLOW,
604 )
605 .context(format!("chown {dir_name:?}"))?;
606
607 copy_xattrs(&stat.xattrs, new_etc_fd, dir_name)?;
608
609 Ok(())
610}
611
612fn merge_leaf(
613 current_etc_fd: &CapStdDir,
614 new_etc_fd: &CapStdDir,
615 leaf: &Rc<Leaf<CustomMetadata>>,
616 new_inode: Option<&Inode<CustomMetadata>>,
617 file: &PathBuf,
618) -> anyhow::Result<()> {
619 let symlink = match &leaf.content {
620 LeafContent::Regular(..) => None,
621 LeafContent::Symlink(target) => Some(target),
622
623 _ => {
624 tracing::debug!("Found non file/symlink while merging. Ignoring");
625 return Ok(());
626 }
627 };
628
629 if matches!(new_inode, Some(Inode::Directory(..))) {
630 anyhow::bail!("Modified config file {file:?} newly defaults to directory. Cannot merge")
631 };
632
633 new_etc_fd
635 .remove_all_optional(&file)
636 .context(format!("Deleting {file:?}"))?;
637
638 if let Some(target) = symlink {
639 symlinkat(&**target, new_etc_fd, file).context(format!("Creating symlink {file:?}"))?;
641 } else {
642 current_etc_fd
643 .copy(&file, new_etc_fd, &file)
644 .context(format!("Copying file {file:?}"))?;
645 };
646
647 rustix::fs::chownat(
648 &new_etc_fd,
649 file,
650 Some(Uid::from_raw(leaf.stat.st_uid)),
651 Some(Gid::from_raw(leaf.stat.st_gid)),
652 AtFlags::SYMLINK_NOFOLLOW,
653 )
654 .context(format!("chown {file:?}"))?;
655
656 copy_xattrs(&leaf.stat.xattrs, new_etc_fd, file)?;
657
658 Ok(())
659}
660
661fn merge_modified_files(
662 files: &Vec<PathBuf>,
663 current_etc_fd: &CapStdDir,
664 current_etc_dirtree: &Directory<CustomMetadata>,
665 new_etc_fd: &CapStdDir,
666 new_etc_dirtree: &Directory<CustomMetadata>,
667) -> anyhow::Result<()> {
668 for file in files {
669 let (dir, filename) = current_etc_dirtree
670 .split(OsStr::new(&file))
671 .context("Getting directory and file")?;
672
673 let current_inode = dir
674 .lookup(filename)
675 .ok_or_else(|| anyhow::anyhow!("{filename:?} not found"))?;
676
677 let res = new_etc_dirtree.split(OsStr::new(&file));
679
680 match res {
681 Ok((new_dir, filename)) => {
682 let new_inode = new_dir.lookup(filename);
683
684 match current_inode {
685 Inode::Directory(..) => {
686 create_dir_with_perms(new_etc_fd, file, current_inode.stat(), new_inode)?;
687 }
688
689 Inode::Leaf(leaf) => {
690 merge_leaf(current_etc_fd, new_etc_fd, leaf, new_inode, file)?
691 }
692 };
693 }
694
695 Err(ImageError::NotFound(..)) => match current_inode {
697 Inode::Directory(..) => {
698 create_dir_with_perms(new_etc_fd, file, current_inode.stat(), None)?
699 }
700
701 Inode::Leaf(leaf) => {
702 merge_leaf(current_etc_fd, new_etc_fd, leaf, None, file)?;
703 }
704 },
705
706 Err(e) => Err(e)?,
707 };
708 }
709
710 Ok(())
711}
712
713#[context("Merging")]
717pub fn merge(
718 current_etc_fd: &CapStdDir,
719 current_etc_dirtree: &Directory<CustomMetadata>,
720 new_etc_fd: &CapStdDir,
721 new_etc_dirtree: &Directory<CustomMetadata>,
722 diff: Diff,
723) -> anyhow::Result<()> {
724 merge_modified_files(
725 &diff.added,
726 current_etc_fd,
727 current_etc_dirtree,
728 new_etc_fd,
729 new_etc_dirtree,
730 )
731 .context("Merging added files")?;
732
733 merge_modified_files(
734 &diff.modified,
735 current_etc_fd,
736 current_etc_dirtree,
737 new_etc_fd,
738 new_etc_dirtree,
739 )
740 .context("Merging modified files")?;
741
742 for removed in diff.removed {
743 let stat = new_etc_fd.metadata_optional(&removed)?;
744
745 let Some(stat) = stat else {
746 continue;
749 };
750
751 if stat.is_file() || stat.is_symlink() {
752 new_etc_fd.remove_file(&removed)?;
753 } else if stat.is_dir() {
754 new_etc_fd.remove_dir_all(&removed)?;
757 }
758 }
759
760 Ok(())
761}
762
763#[cfg(test)]
764mod tests {
765 use cap_std::fs::PermissionsExt;
766 use cap_std_ext::cap_std::fs::Metadata;
767
768 use super::*;
769
770 const FILES: &[(&str, &str)] = &[
771 ("a/file1", "a-file1"),
772 ("a/file2", "a-file2"),
773 ("a/b/file1", "ab-file1"),
774 ("a/b/file2", "ab-file2"),
775 ("a/b/c/fileabc", "abc-file1"),
776 ("a/b/c/modify-perms", "modify-perms"),
777 ("a/b/c/to-be-removed", "remove this"),
778 ("to-be-removed", "remove this 2"),
779 ];
780
781 #[test]
782 fn test_etc_diff() -> anyhow::Result<()> {
783 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
784
785 tempdir.create_dir("pristine_etc")?;
786 tempdir.create_dir("current_etc")?;
787 tempdir.create_dir("new_etc")?;
788
789 let p = tempdir.open_dir("pristine_etc")?;
790 let c = tempdir.open_dir("current_etc")?;
791 let n = tempdir.open_dir("new_etc")?;
792
793 p.create_dir_all("a/b/c")?;
794 c.create_dir_all("a/b/c")?;
795
796 for (file, content) in FILES {
797 p.write(file, content.as_bytes())?;
798 c.write(file, content.as_bytes())?;
799 }
800
801 let new_files = ["new_file", "a/new_file", "a/b/c/new_file"];
802
803 for file in new_files {
805 c.write(file, b"hello")?;
806 }
807
808 let overwritten_files = [FILES[1].0, FILES[4].0];
809 let perm_changed_files = [FILES[5].0];
810
811 c.write(overwritten_files[0], b"some new content")?;
813 c.write(overwritten_files[1], b"some newer content")?;
814
815 let file = c.open(perm_changed_files[0])?;
817 file.set_permissions(cap_std::fs::Permissions::from_mode(0o400))?;
819
820 let deleted_files = [FILES[6].0, FILES[7].0];
822 c.remove_file(deleted_files[0])?;
823 c.remove_file(deleted_files[1])?;
824
825 let (pristine_etc_files, current_etc_files, _) = traverse_etc(&p, &c, Some(&n))?;
826 let res = compute_diff(&pristine_etc_files, ¤t_etc_files)?;
827
828 assert_eq!(res.added.len(), new_files.len());
830 assert!(res.added.iter().all(|file| {
831 new_files
832 .iter()
833 .find(|x| PathBuf::from(*x) == *file)
834 .is_some()
835 }));
836
837 let all_modified_files = overwritten_files
839 .iter()
840 .chain(&perm_changed_files)
841 .collect::<Vec<_>>();
842
843 assert_eq!(res.modified.len(), all_modified_files.len());
844 assert!(res.modified.iter().all(|file| {
845 all_modified_files
846 .iter()
847 .find(|x| PathBuf::from(*x) == *file)
848 .is_some()
849 }));
850
851 assert_eq!(res.removed.len(), deleted_files.len());
853 assert!(res.removed.iter().all(|file| {
854 deleted_files
855 .iter()
856 .find(|x| PathBuf::from(*x) == *file)
857 .is_some()
858 }));
859
860 Ok(())
861 }
862
863 fn compare_meta(meta1: Metadata, meta2: Metadata) -> bool {
864 return meta1.is_file() == meta2.is_file()
865 && meta1.is_dir() == meta2.is_dir()
866 && meta1.is_symlink() == meta2.is_symlink()
867 && meta1.mode() == meta2.mode()
868 && meta1.uid() == meta2.uid()
869 && meta1.gid() == meta2.gid();
870 }
871
872 fn files_eq(current_etc: &CapStdDir, new_etc: &CapStdDir, path: &str) -> anyhow::Result<bool> {
873 return Ok(
874 compare_meta(current_etc.metadata(path)?, new_etc.metadata(path)?)
875 && current_etc.read(path)? == new_etc.read(path)?,
876 );
877 }
878
879 #[test]
880 fn test_merge() -> anyhow::Result<()> {
881 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
882
883 tempdir.create_dir("pristine_etc")?;
884 tempdir.create_dir("current_etc")?;
885 tempdir.create_dir("new_etc")?;
886
887 let p = tempdir.open_dir("pristine_etc")?;
888 let c = tempdir.open_dir("current_etc")?;
889 let n = tempdir.open_dir("new_etc")?;
890
891 p.create_dir_all("a/b")?;
892 c.create_dir_all("a/b")?;
893 n.create_dir_all("a/b")?;
894
895 c.write("new_file.txt", "text1")?;
898 c.write("a/new_file.txt", "text2")?;
899 c.write("a/b/new_file.txt", "text3")?;
900
901 c.write("present_file.txt", "new-present-text1")?;
903 c.write("a/present_file.txt", "new-present-text2")?;
904 c.write("a/b/present_file.txt", "new-present-text3")?;
905
906 n.write("present_file.txt", "present-text1")?;
907 n.write("a/present_file.txt", "present-text2")?;
908 n.write("a/b/present_file.txt", "present-text3")?;
909
910 p.write("content-modify.txt", "old-content1")?;
912 p.write("a/content-modify.txt", "old-content2")?;
913 p.write("a/b/content-modify.txt", "old-content3")?;
914
915 c.write("content-modify.txt", "new-content1")?;
916 c.write("a/content-modify.txt", "new-content2")?;
917 c.write("a/b/content-modify.txt", "new-content3")?;
918
919 p.write("content-modify-present.txt", "old-present-content1")?;
921 p.write("a/content-modify-present.txt", "old-present-content2")?;
922 p.write("a/b/content-modify-present.txt", "old-present-content3")?;
923
924 c.write("content-modify-present.txt", "current-present-content1")?;
925 c.write("a/content-modify-present.txt", "current-present-content2")?;
926 c.write("a/b/content-modify-present.txt", "current-present-content3")?;
927
928 n.write("content-modify-present.txt", "new-present-content1")?;
929 n.write("a/content-modify-present.txt", "new-present-content2")?;
930 n.write("a/b/content-modify-present.txt", "new-present-content3")?;
931
932 p.write("permission-modify.txt", "old-content1")?;
934 p.write("a/permission-modify.txt", "old-content2")?;
935 p.write("a/b/permission-modify.txt", "old-content3")?;
936
937 c.atomic_write_with_perms(
938 "permission-modify.txt",
939 "old-content1",
940 Permissions::from_mode(0o755),
941 )?;
942 c.atomic_write_with_perms(
943 "a/permission-modify.txt",
944 "old-content2",
945 Permissions::from_mode(0o766),
946 )?;
947 c.atomic_write_with_perms(
948 "a/b/permission-modify.txt",
949 "old-content3",
950 Permissions::from_mode(0o744),
951 )?;
952
953 p.write("permission-modify-present.txt", "old-present-content1")?;
955 p.write("a/permission-modify-present.txt", "old-present-content2")?;
956 p.write("a/b/permission-modify-present.txt", "old-present-content3")?;
957
958 c.atomic_write_with_perms(
959 "permission-modify-present.txt",
960 "old-present-content1",
961 Permissions::from_mode(0o755),
962 )?;
963 c.atomic_write_with_perms(
964 "a/permission-modify-present.txt",
965 "old-present-content2",
966 Permissions::from_mode(0o766),
967 )?;
968 c.atomic_write_with_perms(
969 "a/b/permission-modify-present.txt",
970 "old-present-content3",
971 Permissions::from_mode(0o744),
972 )?;
973
974 n.write("permission-modify-present.txt", "new-present-content1")?;
975 n.write("a/permission-modify-present.txt", "old-present-content2")?;
976 n.write("a/b/permission-modify-present.txt", "new-present-content3")?;
977
978 c.create_dir_all("new/dir/tree/here")?;
980
981 p.create_dir_all("existing/tree")?;
983 c.create_dir_all("existing/tree/another/dir/tree")?;
984 c.write(
985 "existing/tree/another/dir/tree/file.txt",
986 "dir-tree-contents",
987 )?;
988
989 p.create_dir_all("dir/perms")?;
991 p.create_dir_all("dir/perms/wo")?;
992 p.create_dir_all("dir/perms/wo/ro")?;
993
994 c.create_dir_all("dir/perms")?;
995 c.set_permissions("dir/perms", Permissions::from_mode(0o777))?;
996
997 c.create_dir_all("dir/perms/rwx")?;
998 c.set_permissions("dir/perms/rwx", Permissions::from_mode(0o777))?;
999
1000 c.create_dir_all("dir/perms/wo")?;
1001 c.set_permissions("dir/perms/wo", Permissions::from_mode(0o733))?;
1002
1003 c.create_dir_all("dir/perms/wo/ro")?;
1004 c.set_permissions("dir/perms/wo/ro", Permissions::from_mode(0o775))?;
1005
1006 n.create_dir_all("dir/perms")?;
1007 n.write("dir/perms/some-file", "Some-file")?;
1008
1009 let (pristine_etc_files, current_etc_files, new_etc_files) =
1010 traverse_etc(&p, &c, Some(&n))?;
1011 let diff = compute_diff(&pristine_etc_files, ¤t_etc_files)?;
1012 merge(&c, ¤t_etc_files, &n, &new_etc_files.unwrap(), diff)?;
1013
1014 assert!(files_eq(&c, &n, "new_file.txt")?);
1015 assert!(files_eq(&c, &n, "a/new_file.txt")?);
1016 assert!(files_eq(&c, &n, "a/b/new_file.txt")?);
1017
1018 assert!(files_eq(&c, &n, "present_file.txt")?);
1019 assert!(files_eq(&c, &n, "a/present_file.txt")?);
1020 assert!(files_eq(&c, &n, "a/b/present_file.txt")?);
1021
1022 assert!(files_eq(&c, &n, "content-modify.txt")?);
1023 assert!(files_eq(&c, &n, "a/content-modify.txt")?);
1024 assert!(files_eq(&c, &n, "a/b/content-modify.txt")?);
1025
1026 assert!(files_eq(&c, &n, "content-modify-present.txt")?);
1027 assert!(files_eq(&c, &n, "a/content-modify-present.txt")?);
1028 assert!(files_eq(&c, &n, "a/b/content-modify-present.txt")?);
1029
1030 assert!(files_eq(&c, &n, "permission-modify.txt")?);
1031 assert!(files_eq(&c, &n, "a/permission-modify.txt")?);
1032 assert!(files_eq(&c, &n, "a/b/permission-modify.txt")?);
1033
1034 assert!(files_eq(&c, &n, "permission-modify-present.txt")?);
1035 assert!(files_eq(&c, &n, "a/permission-modify-present.txt")?);
1036 assert!(files_eq(&c, &n, "a/b/permission-modify-present.txt")?);
1037
1038 assert!(n.exists("new/dir/tree/here"));
1039 assert!(n.exists("existing/tree/another/dir/tree"));
1040 assert!(files_eq(&c, &n, "existing/tree/another/dir/tree/file.txt")?);
1041
1042 assert!(compare_meta(
1043 c.metadata("dir/perms")?,
1044 n.metadata("dir/perms")?
1045 ));
1046
1047 assert!(n.exists("dir/perms/some-file"));
1049
1050 const DIR_BITS: u32 = 0o040000;
1051
1052 assert_eq!(
1053 n.metadata("dir/perms/rwx").unwrap().mode(),
1054 DIR_BITS | 0o777
1055 );
1056 assert_eq!(n.metadata("dir/perms/wo").unwrap().mode(), DIR_BITS | 0o733);
1057 assert_eq!(
1058 n.metadata("dir/perms/wo/ro").unwrap().mode(),
1059 DIR_BITS | 0o775
1060 );
1061
1062 Ok(())
1063 }
1064
1065 #[test]
1066 fn file_to_dir() -> anyhow::Result<()> {
1067 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1068
1069 tempdir.create_dir("pristine_etc")?;
1070 tempdir.create_dir("current_etc")?;
1071 tempdir.create_dir("new_etc")?;
1072
1073 let p = tempdir.open_dir("pristine_etc")?;
1074 let c = tempdir.open_dir("current_etc")?;
1075 let n = tempdir.open_dir("new_etc")?;
1076
1077 p.write("file-to-dir", "some text")?;
1078 c.write("file-to-dir", "some text 1")?;
1079
1080 n.create_dir_all("file-to-dir")?;
1081
1082 let (pristine_etc_files, current_etc_files, new_etc_files) =
1083 traverse_etc(&p, &c, Some(&n))?;
1084 let diff = compute_diff(&pristine_etc_files, ¤t_etc_files)?;
1085
1086 let merge_res = merge(&c, ¤t_etc_files, &n, &new_etc_files.unwrap(), diff);
1087
1088 assert!(merge_res.is_err());
1089 assert_eq!(
1090 merge_res.unwrap_err().root_cause().to_string(),
1091 "Modified config file \"file-to-dir\" newly defaults to directory. Cannot merge"
1092 );
1093
1094 Ok(())
1095 }
1096}