1use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
12
13use anyhow::{Context, Result, ensure};
14use composefs::util::DigestWrite;
15use fn_error_context::context;
16use sha2::{Digest, Sha256};
17
18use composefs::{
19 fsverity::FsVerityHashValue,
20 repository::Repository,
21 tree::{Directory, FileSystem, Inode, Stat},
22};
23
24use containers_image_proxy::oci_spec::image::Digest as OciDigest;
25
26use crate::skopeo::TAR_LAYER_CONTENT_TYPE;
27use crate::tar::{TarEntry, TarItem};
28
29#[context("Processing tar entry")]
37pub fn process_entry<ObjectID: FsVerityHashValue>(
38 filesystem: &mut FileSystem<ObjectID>,
39 entry: TarEntry<ObjectID>,
40) -> Result<()> {
41 if entry.path.file_name().is_none() {
42 ensure!(
44 matches!(entry.item, TarItem::Directory),
45 "Unpacking layer tar: filename {:?} must be a directory",
46 entry.path
47 );
48
49 filesystem.set_root_stat(entry.stat);
51 return Ok(());
52 }
53
54 let inode = match entry.item {
55 TarItem::Directory => Inode::Directory(Box::from(Directory::new(entry.stat))),
56 TarItem::Leaf(content) => {
57 let id = filesystem.push_leaf(entry.stat, content);
58 Inode::leaf(id)
59 }
60 TarItem::Hardlink(target) => {
61 let (dir, filename) = filesystem.root.split(&target)?;
62 Inode::leaf(dir.leaf_id(filename)?)
63 }
64 };
65
66 let (dir, filename) = filesystem
67 .root
68 .split_mut(entry.path.as_os_str())
69 .with_context(|| {
70 format!(
71 "Error unpacking container layer file {:?} {:?}",
72 entry.path, inode
73 )
74 })?;
75
76 let bytes = filename.as_bytes();
77 if let Some(whiteout) = bytes.strip_prefix(b".wh.") {
78 if whiteout == b".wh..opq" {
79 dir.clear();
81 } else {
82 dir.remove(OsStr::from_bytes(whiteout));
83 }
84 } else {
85 dir.merge(filename, inode);
86 }
87
88 Ok(())
89}
90
91pub fn create_filesystem<ObjectID: FsVerityHashValue>(
104 repo: &Repository<ObjectID>,
105 config_name: &OciDigest,
106 config_verity: Option<&ObjectID>,
107) -> Result<FileSystem<ObjectID>> {
108 let mut filesystem = FileSystem::new(Stat::uninitialized());
109
110 let oc = crate::open_config(repo, config_name, config_verity)?;
111 let config = oc.config;
112 let map = oc.layer_refs;
113
114 for diff_id in config.rootfs().diff_ids() {
115 let layer_verity = map
116 .get(diff_id.as_str())
117 .context("OCI config splitstream missing named ref to layer {diff_id}")?;
118
119 if config_verity.is_none() {
120 let mut layer_stream =
124 repo.open_stream("", Some(layer_verity), Some(TAR_LAYER_CONTENT_TYPE))?;
125 let mut context = DigestWrite(Sha256::new());
126 layer_stream.cat(repo, &mut context)?;
127 let content_hash = crate::sha256_output_to_digest(context.finalize());
128 ensure!(
129 content_hash.as_ref() == diff_id,
130 "Layer has incorrect checksum"
131 );
132 }
133
134 let mut layer_stream =
135 repo.open_stream("", Some(layer_verity), Some(TAR_LAYER_CONTENT_TYPE))?;
136 while let Some(entry) = crate::tar::get_entry(&mut layer_stream)? {
137 process_entry(&mut filesystem, entry)?;
138 }
139 }
140
141 filesystem.transform_for_oci()?;
144
145 filesystem.compact();
147
148 debug_assert!(
149 filesystem.fsck().is_ok(),
150 "create_filesystem produced invalid filesystem"
151 );
152 Ok(filesystem)
153}
154
155#[cfg(test)]
156mod test {
157 use composefs::{
158 dumpfile::write_dumpfile,
159 fsverity::Sha256HashValue,
160 repository::RepositoryConfig,
161 tree::{LeafContent, RegularFile, Stat},
162 };
163 use std::{collections::BTreeMap, io::BufRead, path::PathBuf};
164
165 use super::*;
166
167 fn file_entry<ObjectID: FsVerityHashValue>(path: &str) -> TarEntry<ObjectID> {
168 TarEntry {
169 path: PathBuf::from(path),
170 stat: Stat {
171 st_mode: 0o644,
172 st_uid: 0,
173 st_gid: 0,
174 st_mtim_sec: 0,
175 st_mtim_nsec: 0,
176 xattrs: BTreeMap::new(),
177 },
178 item: TarItem::Leaf(LeafContent::Regular(RegularFile::Inline([].into()))),
179 }
180 }
181
182 fn dir_entry<ObjectID: FsVerityHashValue>(path: &str) -> TarEntry<ObjectID> {
183 TarEntry {
184 path: PathBuf::from(path),
185 stat: Stat {
186 st_mode: 0o755,
187 st_uid: 0,
188 st_gid: 0,
189 st_mtim_sec: 0,
190 st_mtim_nsec: 0,
191 xattrs: BTreeMap::new(),
192 },
193 item: TarItem::Directory,
194 }
195 }
196
197 fn assert_files(fs: &FileSystem<impl FsVerityHashValue>, expected: &[&str]) -> Result<()> {
198 let mut out = vec![];
199 write_dumpfile(&mut out, fs)?;
200 let actual: Vec<String> = out
201 .lines()
202 .map(|line| line.unwrap().split_once(' ').unwrap().0.into())
203 .collect();
204
205 similar_asserts::assert_eq!(actual, expected);
206 Ok(())
207 }
208
209 fn append_tar_dir(builder: &mut ::tar::Builder<Vec<u8>>, name: &str) {
210 let mut header = ::tar::Header::new_ustar();
211 header.set_uid(0);
212 header.set_gid(0);
213 header.set_mode(0o755);
214 header.set_entry_type(::tar::EntryType::Directory);
215 header.set_size(0);
216 builder
217 .append_data(&mut header, name, std::io::empty())
218 .unwrap();
219 }
220
221 fn append_tar_file(builder: &mut ::tar::Builder<Vec<u8>>, name: &str, content: &[u8]) {
223 let mut header = ::tar::Header::new_ustar();
224 header.set_uid(0);
225 header.set_gid(0);
226 header.set_mode(0o644);
227 header.set_entry_type(::tar::EntryType::Regular);
228 header.set_size(content.len() as u64);
229 builder.append_data(&mut header, name, content).unwrap();
230 }
231
232 fn append_tar_symlink(builder: &mut ::tar::Builder<Vec<u8>>, name: &str, target: &str) {
234 let mut header = ::tar::Header::new_ustar();
235 header.set_uid(0);
236 header.set_gid(0);
237 header.set_mode(0o777);
238 header.set_entry_type(::tar::EntryType::Symlink);
239 header.set_size(0);
240 builder.append_link(&mut header, name, target).unwrap();
241 }
242
243 fn append_tar_hardlink(builder: &mut ::tar::Builder<Vec<u8>>, name: &str, target: &str) {
245 let mut header = ::tar::Header::new_ustar();
246 header.set_uid(0);
247 header.set_gid(0);
248 header.set_mode(0o644);
249 header.set_entry_type(::tar::EntryType::Link);
250 header.set_size(0);
251 builder.append_link(&mut header, name, target).unwrap();
252 }
253
254 fn build_baseimage() -> (Vec<u8>, String) {
259 let mut builder = ::tar::Builder::new(vec![]);
260
261 append_tar_dir(&mut builder, "bin"); append_tar_dir(&mut builder, "etc");
264 append_tar_dir(&mut builder, "tmp");
265 append_tar_dir(&mut builder, "usr");
266 append_tar_dir(&mut builder, "usr/bin");
267 append_tar_dir(&mut builder, "usr/lib");
268 append_tar_dir(&mut builder, "usr/share");
269 append_tar_dir(&mut builder, "usr/share/doc");
270 append_tar_dir(&mut builder, "var");
271 append_tar_dir(&mut builder, "var/log");
272
273 append_tar_file(&mut builder, "etc/hostname", b"busybox-container\n");
275 append_tar_file(
276 &mut builder,
277 "etc/resolv.conf",
278 b"nameserver 8.8.8.8\nnameserver 8.8.4.4\n",
279 );
280
281 append_tar_file(
283 &mut builder,
284 "etc/passwd",
285 b"root:x:0:0:root:/root:/bin/sh\nnobody:x:65534:65534:Nobody:/nonexistent:/usr/sbin/nologin\n\
286 daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n",
287 );
288
289 let busybox_content: Vec<u8> = (0..65536u64).map(|i| (i % 251) as u8).collect();
291 append_tar_file(&mut builder, "usr/bin/busybox", &busybox_content);
292
293 let libc_content: Vec<u8> = (0..32768u64).map(|i| (i % 241) as u8).collect();
294 append_tar_file(&mut builder, "usr/lib/libc.so", &libc_content);
295
296 let readme_content = "composefs-rs test image\n\
297 This is a synthetic busybox-like filesystem used for round-trip testing.\n\
298 It exercises inline files, external files, symlinks, and hardlinks.\n\
299 The filesystem layout mimics a minimal container image with /usr merge.\n\
300 Generated by build_baseimage() in the composefs-oci test suite.\n\
301 ----\n\
302 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod\n\
303 tempor incididunt ut labore et dolore magna aliqua.\n";
304 append_tar_file(
305 &mut builder,
306 "usr/share/doc/README",
307 readme_content.as_bytes(),
308 );
309
310 let messages_content: Vec<u8> = (0..8192u64).map(|i| (i % 239) as u8).collect();
311 append_tar_file(&mut builder, "var/log/messages", &messages_content);
312
313 append_tar_symlink(&mut builder, "usr/bin/cat", "busybox");
315 append_tar_symlink(&mut builder, "usr/bin/ls", "busybox");
316 append_tar_symlink(&mut builder, "usr/bin/sh", "busybox");
317 append_tar_symlink(&mut builder, "usr/lib/libc.so.6", "libc.so");
318
319 append_tar_hardlink(&mut builder, "usr/bin/cp", "usr/bin/busybox");
321
322 append_tar_symlink(&mut builder, "bin", "usr/bin");
326
327 let data = builder.into_inner().unwrap();
328 let diff_id = crate::sha256_content_digest(&data).to_string();
329 (data, diff_id)
330 }
331
332 #[tokio::test]
336 async fn test_build_baseimage_roundtrip() -> Result<()> {
337 use composefs::{
338 INLINE_CONTENT_MAX_V0,
339 repository::{Repository, RepositoryConfig},
340 test::tempdir,
341 };
342 use rustix::fs::CWD;
343 use std::ffi::OsStr;
344 use std::sync::Arc;
345
346 let (tar_data, diff_id_str) = build_baseimage();
347 let diff_id: OciDigest = diff_id_str.parse()?;
348
349 let repo_dir = tempdir();
350 let repo_path = repo_dir.path().join("repo");
351 let (repo, _) = Repository::<Sha256HashValue>::init_path(
352 CWD,
353 &repo_path,
354 RepositoryConfig::default().set_insecure(),
355 )?;
356 let repo = Arc::new(repo);
357 let (verity, _stats) =
358 crate::import_layer(&repo, &diff_id, Some("layer"), &tar_data[..]).await?;
359
360 let mut stream = repo.open_stream("refs/layer", Some(&verity), None)?;
361 let mut entries = vec![];
362 while let Some(entry) = crate::tar::get_entry(&mut stream)? {
363 entries.push(entry);
364 }
365
366 let by_path = |p: &str| -> &TarEntry<Sha256HashValue> {
368 entries
369 .iter()
370 .find(|e| e.path == PathBuf::from(p))
371 .unwrap_or_else(|| panic!("missing entry for {p}"))
372 };
373
374 let expected_dirs = [
376 "/bin", "/etc",
378 "/tmp",
379 "/usr",
380 "/usr/bin",
381 "/usr/lib",
382 "/usr/share",
383 "/usr/share/doc",
384 "/var",
385 "/var/log",
386 ];
387 for dir in &expected_dirs {
388 let entry = by_path(dir);
389 assert!(
390 matches!(entry.item, TarItem::Directory),
391 "{dir} should be a directory, got {:?}",
392 entry.item
393 );
394 assert_eq!(entry.stat.st_mode, 0o755, "{dir} mode");
395 }
396
397 let hostname = by_path("/etc/hostname");
399 match &hostname.item {
400 TarItem::Leaf(LeafContent::Regular(RegularFile::Inline(data))) => {
401 assert_eq!(data.as_ref(), b"busybox-container\n");
402 assert!(
403 data.len() <= INLINE_CONTENT_MAX_V0,
404 "hostname should be inline ({} bytes <= {INLINE_CONTENT_MAX_V0})",
405 data.len()
406 );
407 }
408 other => panic!("expected inline file for /etc/hostname, got {other:?}"),
409 }
410
411 let resolv = by_path("/etc/resolv.conf");
412 match &resolv.item {
413 TarItem::Leaf(LeafContent::Regular(RegularFile::Inline(data))) => {
414 assert!(data.starts_with(b"nameserver"));
415 assert!(
416 data.len() <= INLINE_CONTENT_MAX_V0,
417 "resolv.conf should be inline ({} bytes <= {INLINE_CONTENT_MAX_V0})",
418 data.len()
419 );
420 }
421 other => panic!("expected inline file for /etc/resolv.conf, got {other:?}"),
422 }
423
424 let passwd = by_path("/etc/passwd");
426 match &passwd.item {
427 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
428 assert!(
429 *size as usize > INLINE_CONTENT_MAX_V0,
430 "passwd should be external ({size} bytes > {INLINE_CONTENT_MAX_V0})"
431 );
432 }
433 other => panic!("expected external file for /etc/passwd, got {other:?}"),
434 }
435
436 let busybox = by_path("/usr/bin/busybox");
437 match &busybox.item {
438 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
439 assert_eq!(*size, 65536, "busybox should be 64KB");
440 }
441 other => panic!("expected external file for /usr/bin/busybox, got {other:?}"),
442 }
443
444 let libc = by_path("/usr/lib/libc.so");
445 match &libc.item {
446 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
447 assert_eq!(*size, 32768, "libc.so should be 32KB");
448 }
449 other => panic!("expected external file for /usr/lib/libc.so, got {other:?}"),
450 }
451
452 let readme = by_path("/usr/share/doc/README");
453 match &readme.item {
454 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
455 assert!(
456 *size as usize > INLINE_CONTENT_MAX_V0,
457 "README should be external ({size} bytes)"
458 );
459 }
460 other => panic!("expected external file for README, got {other:?}"),
461 }
462
463 let messages = by_path("/var/log/messages");
464 match &messages.item {
465 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
466 assert_eq!(*size, 8192, "messages should be 8KB");
467 }
468 other => panic!("expected external file for /var/log/messages, got {other:?}"),
469 }
470
471 let symlinks = [
473 ("/usr/bin/cat", "busybox"),
474 ("/usr/bin/ls", "busybox"),
475 ("/usr/bin/sh", "busybox"),
476 ("/usr/lib/libc.so.6", "libc.so"),
477 ];
478 for (path, target) in &symlinks {
479 let entry = by_path(path);
480 match &entry.item {
481 TarItem::Leaf(LeafContent::Symlink(t)) => {
482 assert_eq!(&**t, OsStr::new(target), "{path} symlink target");
483 }
484 other => panic!("expected symlink for {path}, got {other:?}"),
485 }
486 }
487
488 let cp = by_path("/usr/bin/cp");
491 match &cp.item {
492 TarItem::Hardlink(target) => {
493 assert_eq!(target, OsStr::new("/usr/bin/busybox"), "cp hardlink target");
494 }
495 other => panic!("expected hardlink for /usr/bin/cp, got {other:?}"),
496 }
497
498 let bin_entries: Vec<_> = entries
502 .iter()
503 .filter(|e| e.path == PathBuf::from("/bin"))
504 .collect();
505 assert!(
506 bin_entries.len() >= 2,
507 "/bin should appear as both a directory and a symlink"
508 );
509 let last_bin = bin_entries.last().unwrap();
510 match &last_bin.item {
511 TarItem::Leaf(LeafContent::Symlink(t)) => {
512 assert_eq!(&**t, OsStr::new("usr/bin"), "/bin symlink target");
513 }
514 other => panic!("expected symlink for final /bin, got {other:?}"),
515 }
516
517 let expected_count = 10 + 7 + 4 + 1 + 1; assert_eq!(
526 entries.len(),
527 expected_count,
528 "total entry count (dirs + files + symlinks + hardlinks)"
529 );
530
531 Ok(())
532 }
533
534 #[test]
535 fn test_process_entry() -> Result<()> {
536 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
537
538 process_entry(&mut fs, dir_entry("/a"))?;
540 process_entry(&mut fs, dir_entry("b"))?;
541 process_entry(&mut fs, dir_entry("c"))?;
542 assert_files(&fs, &["/", "/a", "/b", "/c"])?;
543
544 process_entry(&mut fs, file_entry("/a/b"))?;
546 process_entry(&mut fs, file_entry("/a/c"))?;
547 process_entry(&mut fs, file_entry("/b/a"))?;
548 process_entry(&mut fs, file_entry("/b/c"))?;
549 process_entry(&mut fs, file_entry("/c/a"))?;
550 process_entry(&mut fs, file_entry("/c/c"))?;
551 assert_files(
552 &fs,
553 &[
554 "/", "/a", "/a/b", "/a/c", "/b", "/b/a", "/b/c", "/c", "/c/a", "/c/c",
555 ],
556 )?;
557
558 process_entry(&mut fs, file_entry(".wh.a"))?; process_entry(&mut fs, file_entry("/b/.wh..wh..opq"))?; process_entry(&mut fs, file_entry("/c/.wh.c"))?; assert_files(&fs, &["/", "/b", "/c", "/c/a"])?;
563
564 Ok(())
565 }
566
567 #[test]
570 fn test_whiteout_file_removes_entry() -> Result<()> {
571 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
572
573 process_entry(&mut fs, dir_entry("/etc"))?;
574 process_entry(&mut fs, file_entry("/etc/hosts"))?;
575 process_entry(&mut fs, file_entry("/etc/passwd"))?;
576 assert_files(&fs, &["/", "/etc", "/etc/hosts", "/etc/passwd"])?;
577
578 process_entry(&mut fs, file_entry("/etc/.wh.hosts"))?;
580 assert_files(&fs, &["/", "/etc", "/etc/passwd"])?;
581
582 Ok(())
583 }
584
585 #[test]
586 fn test_whiteout_nonexistent_file_is_noop() -> Result<()> {
587 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
588
589 process_entry(&mut fs, dir_entry("/etc"))?;
590 process_entry(&mut fs, file_entry("/etc/hosts"))?;
591 assert_files(&fs, &["/", "/etc", "/etc/hosts"])?;
592
593 process_entry(&mut fs, file_entry("/etc/.wh.nosuchfile"))?;
595 assert_files(&fs, &["/", "/etc", "/etc/hosts"])?;
596
597 Ok(())
598 }
599
600 #[test]
601 fn test_whiteout_directory() -> Result<()> {
602 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
603
604 process_entry(&mut fs, dir_entry("/usr"))?;
605 process_entry(&mut fs, dir_entry("/usr/local"))?;
606 process_entry(&mut fs, file_entry("/usr/local/bin"))?;
607 process_entry(&mut fs, dir_entry("/etc"))?;
608 assert_files(&fs, &["/", "/etc", "/usr", "/usr/local", "/usr/local/bin"])?;
609
610 process_entry(&mut fs, file_entry("/usr/.wh.local"))?;
612 assert_files(&fs, &["/", "/etc", "/usr"])?;
613
614 Ok(())
615 }
616
617 #[test]
618 fn test_whiteout_in_root_directory() -> Result<()> {
619 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
620
621 process_entry(&mut fs, dir_entry("/mydir"))?;
622 process_entry(&mut fs, file_entry("/toplevel"))?;
623 assert_files(&fs, &["/", "/mydir", "/toplevel"])?;
624
625 process_entry(&mut fs, file_entry("/.wh.toplevel"))?;
627 assert_files(&fs, &["/", "/mydir"])?;
628
629 process_entry(&mut fs, file_entry(".wh.mydir"))?;
631 assert_files(&fs, &["/"])?;
632
633 Ok(())
634 }
635
636 #[test]
637 fn test_whiteout_in_nested_directory() -> Result<()> {
638 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
639
640 process_entry(&mut fs, dir_entry("/a"))?;
641 process_entry(&mut fs, dir_entry("/a/b"))?;
642 process_entry(&mut fs, dir_entry("/a/b/c"))?;
643 process_entry(&mut fs, file_entry("/a/b/c/deep"))?;
644 assert_files(&fs, &["/", "/a", "/a/b", "/a/b/c", "/a/b/c/deep"])?;
645
646 process_entry(&mut fs, file_entry("/a/b/c/.wh.deep"))?;
647 assert_files(&fs, &["/", "/a", "/a/b", "/a/b/c"])?;
648
649 Ok(())
650 }
651
652 #[test]
653 fn test_opaque_whiteout_clears_directory() -> Result<()> {
654 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
655
656 process_entry(&mut fs, dir_entry("/etc"))?;
657 process_entry(&mut fs, file_entry("/etc/hosts"))?;
658 process_entry(&mut fs, file_entry("/etc/passwd"))?;
659 process_entry(&mut fs, file_entry("/etc/resolv.conf"))?;
660 assert_files(
661 &fs,
662 &["/", "/etc", "/etc/hosts", "/etc/passwd", "/etc/resolv.conf"],
663 )?;
664
665 process_entry(&mut fs, file_entry("/etc/.wh..wh..opq"))?;
667 assert_files(&fs, &["/", "/etc"])?;
668
669 Ok(())
670 }
671
672 #[test]
673 fn test_opaque_whiteout_then_add_new_entries() -> Result<()> {
674 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
678
679 process_entry(&mut fs, dir_entry("/etc"))?;
680 process_entry(&mut fs, file_entry("/etc/old_config"))?;
681 process_entry(&mut fs, file_entry("/etc/another_old"))?;
682 assert_files(&fs, &["/", "/etc", "/etc/another_old", "/etc/old_config"])?;
683
684 process_entry(&mut fs, file_entry("/etc/.wh..wh..opq"))?;
686 assert_files(&fs, &["/", "/etc"])?;
687
688 process_entry(&mut fs, file_entry("/etc/new_config"))?;
690 process_entry(&mut fs, file_entry("/etc/new_other"))?;
691 assert_files(&fs, &["/", "/etc", "/etc/new_config", "/etc/new_other"])?;
692
693 Ok(())
694 }
695
696 #[test]
697 fn test_multiple_whiteouts_in_single_layer() -> Result<()> {
698 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
699
700 process_entry(&mut fs, dir_entry("/usr"))?;
701 process_entry(&mut fs, file_entry("/usr/a"))?;
702 process_entry(&mut fs, file_entry("/usr/b"))?;
703 process_entry(&mut fs, file_entry("/usr/c"))?;
704 process_entry(&mut fs, file_entry("/usr/d"))?;
705 assert_files(&fs, &["/", "/usr", "/usr/a", "/usr/b", "/usr/c", "/usr/d"])?;
706
707 process_entry(&mut fs, file_entry("/usr/.wh.a"))?;
709 process_entry(&mut fs, file_entry("/usr/.wh.c"))?;
710 assert_files(&fs, &["/", "/usr", "/usr/b", "/usr/d"])?;
711
712 Ok(())
713 }
714
715 #[test]
716 fn test_double_whiteout_is_idempotent() -> Result<()> {
717 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
718
719 process_entry(&mut fs, dir_entry("/d"))?;
720 process_entry(&mut fs, file_entry("/d/target"))?;
721 assert_files(&fs, &["/", "/d", "/d/target"])?;
722
723 process_entry(&mut fs, file_entry("/d/.wh.target"))?;
725 assert_files(&fs, &["/", "/d"])?;
726
727 process_entry(&mut fs, file_entry("/d/.wh.target"))?;
728 assert_files(&fs, &["/", "/d"])?;
729
730 Ok(())
731 }
732
733 #[test]
734 fn test_whiteout_unusual_name_dot_wh_dot() -> Result<()> {
735 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
740
741 process_entry(&mut fs, dir_entry("/d"))?;
742 process_entry(&mut fs, file_entry("/d/real_file"))?;
743 assert_files(&fs, &["/", "/d", "/d/real_file"])?;
744
745 process_entry(&mut fs, file_entry("/d/.wh..wh."))?;
750 assert_files(&fs, &["/", "/d", "/d/real_file"])?;
751
752 process_entry(&mut fs, file_entry("/d/.wh."))?;
755 assert_files(&fs, &["/", "/d", "/d/real_file"])?;
756
757 Ok(())
758 }
759
760 #[test]
761 fn test_whiteout_across_multiple_directories() -> Result<()> {
762 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
763
764 process_entry(&mut fs, dir_entry("/a"))?;
765 process_entry(&mut fs, dir_entry("/b"))?;
766 process_entry(&mut fs, file_entry("/a/file1"))?;
767 process_entry(&mut fs, file_entry("/a/file2"))?;
768 process_entry(&mut fs, file_entry("/b/file1"))?;
769 process_entry(&mut fs, file_entry("/b/file2"))?;
770 assert_files(
771 &fs,
772 &[
773 "/", "/a", "/a/file1", "/a/file2", "/b", "/b/file1", "/b/file2",
774 ],
775 )?;
776
777 process_entry(&mut fs, file_entry("/a/.wh.file1"))?;
779 process_entry(&mut fs, file_entry("/b/.wh.file2"))?;
780 assert_files(&fs, &["/", "/a", "/a/file2", "/b", "/b/file1"])?;
781
782 Ok(())
783 }
784
785 #[test]
786 fn test_opaque_whiteout_with_subdirectories() -> Result<()> {
787 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
789
790 process_entry(&mut fs, dir_entry("/parent"))?;
791 process_entry(&mut fs, dir_entry("/parent/child"))?;
792 process_entry(&mut fs, file_entry("/parent/child/deep"))?;
793 process_entry(&mut fs, file_entry("/parent/sibling"))?;
794 assert_files(
795 &fs,
796 &[
797 "/",
798 "/parent",
799 "/parent/child",
800 "/parent/child/deep",
801 "/parent/sibling",
802 ],
803 )?;
804
805 process_entry(&mut fs, file_entry("/parent/.wh..wh..opq"))?;
806 assert_files(&fs, &["/", "/parent"])?;
807
808 Ok(())
809 }
810
811 #[test]
812 fn test_whiteout_then_recreate() -> Result<()> {
813 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
815
816 process_entry(&mut fs, dir_entry("/etc"))?;
817 process_entry(&mut fs, file_entry("/etc/config"))?;
818 assert_files(&fs, &["/", "/etc", "/etc/config"])?;
819
820 process_entry(&mut fs, file_entry("/etc/.wh.config"))?;
822 assert_files(&fs, &["/", "/etc"])?;
823
824 process_entry(&mut fs, file_entry("/etc/config"))?;
825 assert_files(&fs, &["/", "/etc", "/etc/config"])?;
826
827 Ok(())
828 }
829}