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)]
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.leaf_id(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 pristine_leaves: &[Leaf<CustomMetadata>],
192 current_leaves: &[Leaf<CustomMetadata>],
193 new: &Directory<CustomMetadata>,
194 mut current_path: PathBuf,
195 diff: &mut Diff,
196) -> anyhow::Result<()> {
197 use composefs::generic_tree::LeafContent::*;
198
199 for (path, inode) in current.sorted_entries() {
200 current_path.push(path);
201
202 match inode {
203 Inode::Directory(curr_dir) => {
204 match pristine.get_directory(path) {
205 Ok(old_dir) => {
206 if !stat_eq_ignore_mtime(&curr_dir.stat, &old_dir.stat) {
207 diff.modified.push(current_path.clone());
209 }
210
211 let total_added = diff.added.len();
212 let total_modified = diff.modified.len();
213
214 get_modifications(
215 old_dir,
216 &curr_dir,
217 pristine_leaves,
218 current_leaves,
219 new,
220 current_path.clone(),
221 diff,
222 )?;
223
224 if new.get_directory_opt(¤t_path.as_os_str())?.is_none() {
228 if diff.added.len() != total_added {
229 diff.added.insert(total_added, current_path.clone());
230 } else if diff.modified.len() != total_modified {
231 diff.modified.insert(total_modified, current_path.clone());
232 }
233 }
234 }
235
236 Err(ImageError::NotFound(..)) => {
237 diff.added.push(current_path.clone());
239
240 collect_all_files(&curr_dir, current_path.clone(), &mut diff.added);
242 }
243
244 Err(ImageError::NotADirectory(..)) => {
245 diff.modified.push(current_path.clone());
248 }
249
250 Err(e) => Err(e)?,
251 }
252 }
253
254 Inode::Leaf(leaf_id, _) => match pristine.leaf_id(path) {
255 Ok(old_leaf_id) => {
256 let leaf = ¤t_leaves[leaf_id.0];
257 let old_leaf = &pristine_leaves[old_leaf_id.0];
258 if !stat_eq_ignore_mtime(&old_leaf.stat, &leaf.stat) {
259 diff.modified.push(current_path.clone());
260 current_path.pop();
261 continue;
262 }
263
264 match (&old_leaf.content, &leaf.content) {
265 (Regular(old_meta), Regular(current_meta)) => {
266 if old_meta.content_hash != current_meta.content_hash {
267 diff.modified.push(current_path.clone());
269 }
270 }
271
272 (Symlink(old_link), Symlink(current_link)) => {
273 if old_link != current_link {
274 diff.modified.push(current_path.clone());
276 }
277 }
278
279 (Symlink(..), Regular(..)) | (Regular(..), Symlink(..)) => {
280 diff.modified.push(current_path.clone());
282 }
283
284 (a, b) => {
285 unreachable!("{a:?} modified to {b:?}")
286 }
287 }
288 }
289
290 Err(ImageError::IsADirectory(..)) => {
291 diff.modified.push(current_path.clone());
293 }
294
295 Err(ImageError::NotFound(..)) => {
296 diff.added.push(current_path.clone());
298 }
299
300 Err(e) => Err(e).context(format!("{path:?}"))?,
301 },
302 }
303
304 current_path.pop();
305 }
306
307 Ok(())
308}
309
310pub fn traverse_etc(
338 pristine_etc: &CapStdDir,
339 current_etc: &CapStdDir,
340 new_etc: Option<&CapStdDir>,
341) -> anyhow::Result<(
342 FileSystem<CustomMetadata>,
343 FileSystem<CustomMetadata>,
344 Option<FileSystem<CustomMetadata>>,
345)> {
346 let mut pristine_etc_files = FileSystem::new(Stat::uninitialized());
347 recurse_dir(
348 pristine_etc,
349 &mut pristine_etc_files.root,
350 &mut pristine_etc_files.leaves,
351 )
352 .context(format!("Recursing {pristine_etc:?}"))?;
353
354 let mut current_etc_files = FileSystem::new(Stat::uninitialized());
355 recurse_dir(
356 current_etc,
357 &mut current_etc_files.root,
358 &mut current_etc_files.leaves,
359 )
360 .context(format!("Recursing {current_etc:?}"))?;
361
362 let new_etc_files = match new_etc {
363 Some(new_etc) => {
364 let mut new_etc_files = FileSystem::new(Stat::uninitialized());
365 recurse_dir(new_etc, &mut new_etc_files.root, &mut new_etc_files.leaves)
366 .context(format!("Recursing {new_etc:?}"))?;
367
368 Some(new_etc_files)
369 }
370
371 None => None,
372 };
373
374 return Ok((pristine_etc_files, current_etc_files, new_etc_files));
375}
376
377#[context("Computing diff")]
379pub fn compute_diff(
380 pristine_etc_files: &FileSystem<CustomMetadata>,
381 current_etc_files: &FileSystem<CustomMetadata>,
382 new_etc_files: &FileSystem<CustomMetadata>,
383) -> anyhow::Result<Diff> {
384 let mut diff = Diff {
385 added: vec![],
386 modified: vec![],
387 removed: vec![],
388 };
389
390 get_modifications(
391 &pristine_etc_files.root,
392 ¤t_etc_files.root,
393 &pristine_etc_files.leaves,
394 ¤t_etc_files.leaves,
395 &new_etc_files.root,
396 PathBuf::new(),
397 &mut diff,
398 )?;
399
400 get_deletions(
401 &pristine_etc_files.root,
402 ¤t_etc_files.root,
403 PathBuf::new(),
404 &mut diff,
405 )?;
406
407 Ok(diff)
408}
409
410pub fn print_diff(diff: &Diff, writer: &mut impl Write) {
412 use owo_colors::OwoColorize;
413
414 for added in &diff.added {
415 let _ = writeln!(writer, "{} {added:?}", ModificationType::Added.green());
416 }
417
418 for modified in &diff.modified {
419 let _ = writeln!(writer, "{} {modified:?}", ModificationType::Modified.cyan());
420 }
421
422 for removed in &diff.removed {
423 let _ = writeln!(writer, "{} {removed:?}", ModificationType::Removed.red());
424 }
425}
426
427#[context("Collecting xattrs")]
428fn collect_xattrs(etc_fd: &CapStdDir, rel_path: impl AsRef<Path>) -> anyhow::Result<Xattrs> {
429 let link = format!("/proc/self/fd/{}", etc_fd.as_fd().as_raw_fd());
430 let path = Path::new(&link).join(rel_path);
431
432 const DEFAULT_SIZE: usize = 128;
433
434 let mut xattrs_name_buf: Vec<u8> = vec![0; DEFAULT_SIZE];
436 let mut size = llistxattr(&path, &mut xattrs_name_buf).context("llistxattr")?;
437
438 if size > xattrs_name_buf.capacity() {
439 xattrs_name_buf.resize(size, 0);
440 size = llistxattr(&path, &mut xattrs_name_buf).context("llistxattr")?;
441 }
442
443 let mut xattrs: Xattrs = BTreeMap::new();
444
445 for name_buf in xattrs_name_buf[..size]
446 .split(|&b| b == 0)
447 .filter(|x| !x.is_empty())
448 {
449 let name = OsStr::from_bytes(name_buf);
450
451 let mut xattrs_value_buf = vec![0; DEFAULT_SIZE];
452 let mut size = lgetxattr(&path, name_buf, &mut xattrs_value_buf).context("lgetxattr")?;
453
454 if size > xattrs_value_buf.capacity() {
455 xattrs_value_buf.resize(size, 0);
456 size = lgetxattr(&path, name_buf, &mut xattrs_value_buf).context("lgetxattr")?;
457 }
458
459 xattrs.insert(
460 Box::<OsStr>::from(name),
461 Box::<[u8]>::from(&xattrs_value_buf[..size]),
462 );
463 }
464
465 Ok(xattrs)
466}
467
468#[context("Copying xattrs")]
469fn copy_xattrs(xattrs: &Xattrs, new_etc_fd: &CapStdDir, path: &Path) -> anyhow::Result<()> {
470 for (attr, value) in xattrs.iter() {
471 let fdpath = &Path::new(&format!("/proc/self/fd/{}", new_etc_fd.as_raw_fd())).join(path);
472 lsetxattr(fdpath, attr.as_ref(), value, XattrFlags::empty())
473 .with_context(|| format!("setxattr {attr:?} for {fdpath:?}"))?;
474 }
475
476 Ok(())
477}
478
479fn recurse_dir(
480 dir: &CapStdDir,
481 root: &mut Directory<CustomMetadata>,
482 leaves: &mut Vec<Leaf<CustomMetadata>>,
483) -> anyhow::Result<()> {
484 for entry in dir.entries()? {
485 let entry = entry.context(format!("Getting entry"))?;
486 let entry_name = entry.file_name();
487
488 let entry_type = entry.file_type()?;
489
490 let entry_meta = entry
491 .metadata()
492 .context(format!("Getting metadata for {entry_name:?}"))?;
493
494 let xattrs = collect_xattrs(&dir, &entry_name)?;
495
496 if entry_type.is_symlink() {
498 let readlinkat_result = readlinkat(&dir, &entry_name, vec![])
499 .context(format!("readlinkat {entry_name:?}"))?;
500
501 let os_str = OsStr::from_bytes(readlinkat_result.as_bytes());
502
503 let id = LeafId(leaves.len());
504 leaves.push(Leaf {
505 stat: MyStat::from((&entry_meta, xattrs)).0,
506 content: LeafContent::Symlink(Box::from(os_str)),
507 });
508 root.insert(&entry_name, Inode::leaf(id));
509
510 continue;
511 }
512
513 if entry_type.is_dir() {
514 let dir = dir
515 .open_dir(&entry_name)
516 .with_context(|| format!("Opening dir {entry_name:?} inside {dir:?}"))?;
517
518 let mut directory = Directory::new(MyStat::from((&entry_meta, xattrs)).0);
519
520 recurse_dir(&dir, &mut directory, leaves)?;
521
522 root.insert(&entry_name, Inode::Directory(Box::new(directory)));
523
524 continue;
525 }
526
527 if !(entry_type.is_symlink() || entry_type.is_file()) {
528 tracing::debug!("Ignoring non-regular/non-symlink file: {:?}", entry_name);
531 continue;
532 }
533
534 let measured_verity =
538 composefs::fsverity::measure_verity_opt::<Sha256HashValue>(entry.open()?);
539
540 let measured_verity = match measured_verity {
541 Ok(mv) => mv.map(|verity| verity.to_hex()),
542
543 Err(composefs::fsverity::MeasureVerityError::InvalidDigestAlgorithm { .. }) => {
544 composefs::fsverity::measure_verity_opt::<Sha512HashValue>(entry.open()?)?
545 .map(|verity| verity.to_hex())
546 }
547
548 Err(e) => Err(e)?,
549 };
550
551 if let Some(measured_verity) = measured_verity {
552 let id = LeafId(leaves.len());
553 leaves.push(Leaf {
554 stat: MyStat::from((&entry_meta, xattrs)).0,
555 content: LeafContent::Regular(CustomMetadata::new(
556 "".into(),
557 Some(measured_verity),
558 )),
559 });
560 root.insert(&entry_name, Inode::leaf(id));
561
562 continue;
563 }
564
565 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
566
567 let file = entry
568 .open()
569 .context(format!("Opening entry {entry_name:?}"))?;
570
571 let mut reader = BufReader::new(file);
572 std::io::copy(&mut reader, &mut hasher)?;
573
574 let content_digest = hex::encode(hasher.finish()?);
575
576 let id = LeafId(leaves.len());
577 leaves.push(Leaf {
578 stat: MyStat::from((&entry_meta, xattrs)).0,
579 content: LeafContent::Regular(CustomMetadata::new(content_digest, None)),
580 });
581 root.insert(&entry_name, Inode::leaf(id));
582 }
583
584 Ok(())
585}
586
587#[derive(Debug)]
588enum ModificationType {
589 Added,
590 Modified,
591 Removed,
592}
593
594impl std::fmt::Display for ModificationType {
595 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
596 write!(f, "{:?}", self)
597 }
598}
599
600impl ModificationType {
601 fn symbol(&self) -> &'static str {
602 match self {
603 ModificationType::Added => "+",
604 ModificationType::Modified => "~",
605 ModificationType::Removed => "-",
606 }
607 }
608}
609
610fn create_dir_with_perms(
611 new_etc_fd: &CapStdDir,
612 dir_name: &PathBuf,
613 stat: &Stat,
614 new_inode: Option<&Inode<CustomMetadata>>,
615) -> anyhow::Result<()> {
616 if new_inode.is_none() {
619 new_etc_fd
631 .create_dir_all(&dir_name)
632 .context(format!("Failed to create dir {dir_name:?}"))?;
633 }
634
635 new_etc_fd
636 .set_permissions(&dir_name, Permissions::from_mode(stat.st_mode))
637 .context(format!("Changing permissions for dir {dir_name:?}"))?;
638
639 rustix::fs::chownat(
640 &new_etc_fd,
641 dir_name,
642 Some(Uid::from_raw(stat.st_uid)),
643 Some(Gid::from_raw(stat.st_gid)),
644 AtFlags::SYMLINK_NOFOLLOW,
645 )
646 .context(format!("chown {dir_name:?}"))?;
647
648 copy_xattrs(&stat.xattrs, new_etc_fd, dir_name)?;
649
650 Ok(())
651}
652
653fn merge_leaf(
654 current_etc_fd: &CapStdDir,
655 new_etc_fd: &CapStdDir,
656 leaf: &Leaf<CustomMetadata>,
657 new_inode: Option<&Inode<CustomMetadata>>,
658 file: &PathBuf,
659) -> anyhow::Result<()> {
660 let symlink = match &leaf.content {
661 LeafContent::Regular(..) => None,
662 LeafContent::Symlink(target) => Some(target),
663
664 _ => {
665 tracing::debug!("Found non file/symlink while merging. Ignoring");
666 return Ok(());
667 }
668 };
669
670 if matches!(new_inode, Some(Inode::Directory(..))) {
671 anyhow::bail!("Modified config file {file:?} newly defaults to directory. Cannot merge")
672 };
673
674 new_etc_fd
676 .remove_all_optional(&file)
677 .context(format!("Deleting {file:?}"))?;
678
679 if let Some(target) = symlink {
680 symlinkat(&**target, new_etc_fd, file).context(format!("Creating symlink {file:?}"))?;
682 } else {
683 current_etc_fd
684 .copy(&file, new_etc_fd, &file)
685 .with_context(|| format!("Copying file {file:?}"))?;
686 };
687
688 rustix::fs::chownat(
689 &new_etc_fd,
690 file,
691 Some(Uid::from_raw(leaf.stat.st_uid)),
692 Some(Gid::from_raw(leaf.stat.st_gid)),
693 AtFlags::SYMLINK_NOFOLLOW,
694 )
695 .context(format!("chown {file:?}"))?;
696
697 copy_xattrs(&leaf.stat.xattrs, new_etc_fd, file)?;
698
699 Ok(())
700}
701
702fn merge_modified_files(
703 files: &Vec<PathBuf>,
704 current_etc_fd: &CapStdDir,
705 current_etc_dirtree: &Directory<CustomMetadata>,
706 current_leaves: &[Leaf<CustomMetadata>],
707 new_etc_fd: &CapStdDir,
708 new_etc_dirtree: &Directory<CustomMetadata>,
709) -> anyhow::Result<()> {
710 for file in files {
711 let (dir, filename) = current_etc_dirtree
712 .split(OsStr::new(&file))
713 .context("Getting directory and file")?;
714
715 let current_inode = dir
716 .lookup(filename)
717 .ok_or_else(|| anyhow::anyhow!("{filename:?} not found"))?;
718
719 let res = new_etc_dirtree.split(OsStr::new(&file));
721
722 match res {
723 Ok((new_dir, filename)) => {
724 let new_inode = new_dir.lookup(filename);
725
726 match current_inode {
727 Inode::Directory(..) => {
728 create_dir_with_perms(
729 new_etc_fd,
730 file,
731 current_inode.stat(current_leaves),
732 new_inode,
733 )?;
734 }
735
736 Inode::Leaf(leaf_id, _) => {
737 let leaf = ¤t_leaves[leaf_id.0];
738 merge_leaf(current_etc_fd, new_etc_fd, leaf, new_inode, file)?
739 }
740 };
741 }
742
743 Err(ImageError::NotFound(..)) => match current_inode {
745 Inode::Directory(..) => create_dir_with_perms(
746 new_etc_fd,
747 file,
748 current_inode.stat(current_leaves),
749 None,
750 )?,
751
752 Inode::Leaf(leaf_id, _) => {
753 let leaf = ¤t_leaves[leaf_id.0];
754 merge_leaf(current_etc_fd, new_etc_fd, leaf, None, file)?;
755 }
756 },
757
758 Err(e) => Err(e)?,
759 };
760 }
761
762 Ok(())
763}
764
765#[context("Merging")]
769pub fn merge(
770 current_etc_fd: &CapStdDir,
771 current_etc_dirtree: &FileSystem<CustomMetadata>,
772 new_etc_fd: &CapStdDir,
773 new_etc_dirtree: &FileSystem<CustomMetadata>,
774 diff: &Diff,
775) -> anyhow::Result<()> {
776 merge_modified_files(
777 &diff.added,
778 current_etc_fd,
779 ¤t_etc_dirtree.root,
780 ¤t_etc_dirtree.leaves,
781 new_etc_fd,
782 &new_etc_dirtree.root,
783 )
784 .context("Merging added files")?;
785
786 merge_modified_files(
787 &diff.modified,
788 current_etc_fd,
789 ¤t_etc_dirtree.root,
790 ¤t_etc_dirtree.leaves,
791 new_etc_fd,
792 &new_etc_dirtree.root,
793 )
794 .context("Merging modified files")?;
795
796 for removed in &diff.removed {
797 let stat = new_etc_fd.symlink_metadata_optional(&removed)?;
800
801 let Some(stat) = stat else {
802 continue;
805 };
806
807 if stat.is_file() || stat.is_symlink() {
808 new_etc_fd.remove_file(&removed)?;
809 } else if stat.is_dir() {
810 new_etc_fd.remove_dir_all(&removed)?;
813 }
814 }
815
816 Ok(())
817}
818
819#[cfg(test)]
820mod tests {
821 use cap_std::fs::PermissionsExt;
822 use cap_std_ext::cap_std::fs::Metadata;
823
824 use super::*;
825
826 const FILES: &[(&str, &str)] = &[
827 ("a/file1", "a-file1"),
828 ("a/file2", "a-file2"),
829 ("a/b/file1", "ab-file1"),
830 ("a/b/file2", "ab-file2"),
831 ("a/b/c/fileabc", "abc-file1"),
832 ("a/b/c/modify-perms", "modify-perms"),
833 ("a/b/c/to-be-removed", "remove this"),
834 ("to-be-removed", "remove this 2"),
835 ];
836
837 #[test]
838 fn test_etc_diff_plus_merge() -> anyhow::Result<()> {
839 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
840
841 tempdir.create_dir("pristine_etc")?;
842 tempdir.create_dir("current_etc")?;
843 tempdir.create_dir("new_etc")?;
844
845 let p = tempdir.open_dir("pristine_etc")?;
846 let c = tempdir.open_dir("current_etc")?;
847 let n = tempdir.open_dir("new_etc")?;
848
849 p.create_dir_all("a/b/c")?;
850 c.create_dir_all("a/b/c")?;
851
852 for (file, content) in FILES {
853 p.write(file, content.as_bytes())?;
854 c.write(file, content.as_bytes())?;
855 }
856
857 let new_files = ["new_file", "a/new_file", "a/b/c/new_file"];
858
859 for file in new_files {
861 c.write(file, b"hello")?;
862 }
863
864 let overwritten_files = [FILES[1].0, FILES[4].0];
865 let perm_changed_files = [FILES[5].0];
866
867 c.write(overwritten_files[0], b"some new content")?;
869 c.write(overwritten_files[1], b"some newer content")?;
870
871 let file = c.open(perm_changed_files[0])?;
873 file.set_permissions(cap_std::fs::Permissions::from_mode(0o400))?;
875
876 let deleted_files = [FILES[6].0, FILES[7].0];
878 c.remove_file(deleted_files[0])?;
879 c.remove_file(deleted_files[1])?;
880
881 let (pristine_etc_files, current_etc_files, new_etc_files) =
882 traverse_etc(&p, &c, Some(&n))?;
883
884 let res = compute_diff(
885 &pristine_etc_files,
886 ¤t_etc_files,
887 new_etc_files.as_ref().unwrap(),
888 )?;
889
890 merge(
891 &c,
892 ¤t_etc_files,
893 &n,
894 new_etc_files.as_ref().unwrap(),
895 &res,
896 )
897 .expect("Merge failed");
898
899 let added_dirs = ["a", "a/b", "a/b/c"];
900
901 assert_eq!(res.added.len(), new_files.len() + added_dirs.len());
903
904 let all_modified_files = overwritten_files
906 .iter()
907 .chain(&perm_changed_files)
908 .collect::<Vec<_>>();
909
910 assert_eq!(res.modified.len(), all_modified_files.len());
911 assert!(res.modified.iter().all(|file| {
912 all_modified_files
913 .iter()
914 .find(|x| PathBuf::from(*x) == *file)
915 .is_some()
916 }));
917
918 assert_eq!(res.removed.len(), deleted_files.len());
920 assert!(res.removed.iter().all(|file| {
921 deleted_files
922 .iter()
923 .find(|x| PathBuf::from(*x) == *file)
924 .is_some()
925 }));
926
927 Ok(())
928 }
929
930 fn compare_meta(meta1: Metadata, meta2: Metadata) -> bool {
931 return meta1.is_file() == meta2.is_file()
932 && meta1.is_dir() == meta2.is_dir()
933 && meta1.is_symlink() == meta2.is_symlink()
934 && meta1.mode() == meta2.mode()
935 && meta1.uid() == meta2.uid()
936 && meta1.gid() == meta2.gid();
937 }
938
939 fn files_eq(current_etc: &CapStdDir, new_etc: &CapStdDir, path: &str) -> anyhow::Result<bool> {
940 return Ok(
941 compare_meta(current_etc.metadata(path)?, new_etc.metadata(path)?)
942 && current_etc.read(path)? == new_etc.read(path)?,
943 );
944 }
945
946 #[test]
947 fn test_merge() -> anyhow::Result<()> {
948 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
949
950 tempdir.create_dir("pristine_etc")?;
951 tempdir.create_dir("current_etc")?;
952 tempdir.create_dir("new_etc")?;
953
954 let p = tempdir.open_dir("pristine_etc")?;
955 let c = tempdir.open_dir("current_etc")?;
956 let n = tempdir.open_dir("new_etc")?;
957
958 p.create_dir_all("a/b")?;
959 c.create_dir_all("a/b")?;
960 n.create_dir_all("a/b")?;
961
962 c.write("new_file.txt", "text1")?;
965 c.write("a/new_file.txt", "text2")?;
966 c.write("a/b/new_file.txt", "text3")?;
967
968 c.write("present_file.txt", "new-present-text1")?;
970 c.write("a/present_file.txt", "new-present-text2")?;
971 c.write("a/b/present_file.txt", "new-present-text3")?;
972
973 n.write("present_file.txt", "present-text1")?;
974 n.write("a/present_file.txt", "present-text2")?;
975 n.write("a/b/present_file.txt", "present-text3")?;
976
977 p.write("content-modify.txt", "old-content1")?;
979 p.write("a/content-modify.txt", "old-content2")?;
980 p.write("a/b/content-modify.txt", "old-content3")?;
981
982 c.write("content-modify.txt", "new-content1")?;
983 c.write("a/content-modify.txt", "new-content2")?;
984 c.write("a/b/content-modify.txt", "new-content3")?;
985
986 p.write("content-modify-present.txt", "old-present-content1")?;
988 p.write("a/content-modify-present.txt", "old-present-content2")?;
989 p.write("a/b/content-modify-present.txt", "old-present-content3")?;
990
991 c.write("content-modify-present.txt", "current-present-content1")?;
992 c.write("a/content-modify-present.txt", "current-present-content2")?;
993 c.write("a/b/content-modify-present.txt", "current-present-content3")?;
994
995 n.write("content-modify-present.txt", "new-present-content1")?;
996 n.write("a/content-modify-present.txt", "new-present-content2")?;
997 n.write("a/b/content-modify-present.txt", "new-present-content3")?;
998
999 p.write("permission-modify.txt", "old-content1")?;
1001 p.write("a/permission-modify.txt", "old-content2")?;
1002 p.write("a/b/permission-modify.txt", "old-content3")?;
1003
1004 c.atomic_write_with_perms(
1005 "permission-modify.txt",
1006 "old-content1",
1007 Permissions::from_mode(0o755),
1008 )?;
1009 c.atomic_write_with_perms(
1010 "a/permission-modify.txt",
1011 "old-content2",
1012 Permissions::from_mode(0o766),
1013 )?;
1014 c.atomic_write_with_perms(
1015 "a/b/permission-modify.txt",
1016 "old-content3",
1017 Permissions::from_mode(0o744),
1018 )?;
1019
1020 p.write("permission-modify-present.txt", "old-present-content1")?;
1022 p.write("a/permission-modify-present.txt", "old-present-content2")?;
1023 p.write("a/b/permission-modify-present.txt", "old-present-content3")?;
1024
1025 c.atomic_write_with_perms(
1026 "permission-modify-present.txt",
1027 "old-present-content1",
1028 Permissions::from_mode(0o755),
1029 )?;
1030 c.atomic_write_with_perms(
1031 "a/permission-modify-present.txt",
1032 "old-present-content2",
1033 Permissions::from_mode(0o766),
1034 )?;
1035 c.atomic_write_with_perms(
1036 "a/b/permission-modify-present.txt",
1037 "old-present-content3",
1038 Permissions::from_mode(0o744),
1039 )?;
1040
1041 n.write("permission-modify-present.txt", "new-present-content1")?;
1042 n.write("a/permission-modify-present.txt", "old-present-content2")?;
1043 n.write("a/b/permission-modify-present.txt", "new-present-content3")?;
1044
1045 c.create_dir_all("new/dir/tree/here")?;
1047
1048 p.create_dir_all("existing/tree")?;
1050 c.create_dir_all("existing/tree/another/dir/tree")?;
1051 c.write(
1052 "existing/tree/another/dir/tree/file.txt",
1053 "dir-tree-contents",
1054 )?;
1055
1056 p.create_dir_all("dir/perms")?;
1058 p.create_dir_all("dir/perms/wo")?;
1059 p.create_dir_all("dir/perms/wo/ro")?;
1060
1061 c.create_dir_all("dir/perms")?;
1062 c.set_permissions("dir/perms", Permissions::from_mode(0o777))?;
1063
1064 c.create_dir_all("dir/perms/rwx")?;
1065 c.set_permissions("dir/perms/rwx", Permissions::from_mode(0o777))?;
1066
1067 c.create_dir_all("dir/perms/wo")?;
1068 c.set_permissions("dir/perms/wo", Permissions::from_mode(0o733))?;
1069
1070 c.create_dir_all("dir/perms/wo/ro")?;
1071 c.set_permissions("dir/perms/wo/ro", Permissions::from_mode(0o775))?;
1072
1073 n.create_dir_all("dir/perms")?;
1074 n.write("dir/perms/some-file", "Some-file")?;
1075
1076 p.write("ext-symlink", "pristine content")?;
1080 symlinkat("/usr/bin/bash", &n, "ext-symlink")?;
1081
1082 let (pristine_etc_files, current_etc_files, new_etc_files) =
1083 traverse_etc(&p, &c, Some(&n))?;
1084 let diff = compute_diff(
1085 &pristine_etc_files,
1086 ¤t_etc_files,
1087 &new_etc_files.as_ref().unwrap(),
1088 )?;
1089 merge(&c, ¤t_etc_files, &n, &new_etc_files.unwrap(), &diff)?;
1090
1091 assert!(files_eq(&c, &n, "new_file.txt")?);
1092 assert!(files_eq(&c, &n, "a/new_file.txt")?);
1093 assert!(files_eq(&c, &n, "a/b/new_file.txt")?);
1094
1095 assert!(files_eq(&c, &n, "present_file.txt")?);
1096 assert!(files_eq(&c, &n, "a/present_file.txt")?);
1097 assert!(files_eq(&c, &n, "a/b/present_file.txt")?);
1098
1099 assert!(files_eq(&c, &n, "content-modify.txt")?);
1100 assert!(files_eq(&c, &n, "a/content-modify.txt")?);
1101 assert!(files_eq(&c, &n, "a/b/content-modify.txt")?);
1102
1103 assert!(files_eq(&c, &n, "content-modify-present.txt")?);
1104 assert!(files_eq(&c, &n, "a/content-modify-present.txt")?);
1105 assert!(files_eq(&c, &n, "a/b/content-modify-present.txt")?);
1106
1107 assert!(files_eq(&c, &n, "permission-modify.txt")?);
1108 assert!(files_eq(&c, &n, "a/permission-modify.txt")?);
1109 assert!(files_eq(&c, &n, "a/b/permission-modify.txt")?);
1110
1111 assert!(files_eq(&c, &n, "permission-modify-present.txt")?);
1112 assert!(files_eq(&c, &n, "a/permission-modify-present.txt")?);
1113 assert!(files_eq(&c, &n, "a/b/permission-modify-present.txt")?);
1114
1115 assert!(n.exists("new/dir/tree/here"));
1116 assert!(n.exists("existing/tree/another/dir/tree"));
1117 assert!(files_eq(&c, &n, "existing/tree/another/dir/tree/file.txt")?);
1118
1119 assert!(compare_meta(
1120 c.metadata("dir/perms")?,
1121 n.metadata("dir/perms")?
1122 ));
1123
1124 assert!(n.exists("dir/perms/some-file"));
1126
1127 const DIR_BITS: u32 = 0o040000;
1128
1129 assert_eq!(
1130 n.metadata("dir/perms/rwx").unwrap().mode(),
1131 DIR_BITS | 0o777
1132 );
1133 assert_eq!(n.metadata("dir/perms/wo").unwrap().mode(), DIR_BITS | 0o733);
1134 assert_eq!(
1135 n.metadata("dir/perms/wo/ro").unwrap().mode(),
1136 DIR_BITS | 0o775
1137 );
1138
1139 assert!(!n.exists("ext-symlink"));
1141
1142 Ok(())
1143 }
1144
1145 #[test]
1146 fn file_to_dir() -> anyhow::Result<()> {
1147 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1148
1149 tempdir.create_dir("pristine_etc")?;
1150 tempdir.create_dir("current_etc")?;
1151 tempdir.create_dir("new_etc")?;
1152
1153 let p = tempdir.open_dir("pristine_etc")?;
1154 let c = tempdir.open_dir("current_etc")?;
1155 let n = tempdir.open_dir("new_etc")?;
1156
1157 p.write("file-to-dir", "some text")?;
1158 c.write("file-to-dir", "some text 1")?;
1159
1160 n.create_dir_all("file-to-dir")?;
1161
1162 let (pristine_etc_files, current_etc_files, new_etc_files) =
1163 traverse_etc(&p, &c, Some(&n))?;
1164 let diff = compute_diff(
1165 &pristine_etc_files,
1166 ¤t_etc_files,
1167 &new_etc_files.as_ref().unwrap(),
1168 )?;
1169
1170 let merge_res = merge(&c, ¤t_etc_files, &n, &new_etc_files.unwrap(), &diff);
1171
1172 assert!(merge_res.is_err());
1173 assert_eq!(
1174 merge_res.unwrap_err().root_cause().to_string(),
1175 "Modified config file \"file-to-dir\" newly defaults to directory. Cannot merge"
1176 );
1177
1178 Ok(())
1179 }
1180}