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