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