1use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
12
13use anyhow::{Context, Result, ensure};
14use composefs::digest::{Digest, Sha256};
15use composefs::util::DigestWrite;
16use fn_error_context::context;
17
18use composefs::{
19 fsverity::FsVerityHashValue,
20 generic_tree::OciTransformOptions,
21 repository::Repository,
22 tree::{Directory, FileSystem, Inode, Stat},
23};
24
25use containers_image_proxy::oci_spec::image::Digest as OciDigest;
26
27use crate::skopeo::TAR_LAYER_CONTENT_TYPE;
28use crate::tar::{TarEntry, TarItem};
29
30#[context("Processing tar entry")]
38pub fn process_entry<ObjectID: FsVerityHashValue>(
39 filesystem: &mut FileSystem<ObjectID>,
40 entry: TarEntry<ObjectID>,
41) -> Result<()> {
42 if entry.path.file_name().is_none() {
43 ensure!(
45 matches!(entry.item, TarItem::Directory),
46 "Unpacking layer tar: filename {:?} must be a directory",
47 entry.path
48 );
49
50 filesystem.set_root_stat(entry.stat);
52 return Ok(());
53 }
54
55 let inode = match entry.item {
56 TarItem::Directory => Inode::Directory(Box::from(Directory::new(entry.stat))),
57 TarItem::Leaf(content) => {
58 let id = filesystem.push_leaf(entry.stat, content);
59 Inode::leaf(id)
60 }
61 TarItem::Hardlink(target) => {
62 let (dir, filename) = filesystem.root.split(&target)?;
63 Inode::leaf(dir.leaf_id(filename)?)
64 }
65 };
66
67 let (dir, filename) = filesystem
68 .root
69 .split_mut(entry.path.as_os_str())
70 .with_context(|| {
71 format!(
72 "Error unpacking container layer file {:?} {:?}",
73 entry.path, inode
74 )
75 })?;
76
77 let bytes = filename.as_bytes();
78 if let Some(whiteout) = bytes.strip_prefix(b".wh.") {
79 if whiteout == b".wh..opq" {
80 dir.clear();
82 } else {
83 dir.remove(OsStr::from_bytes(whiteout));
84 }
85 } else {
86 dir.merge(filename, inode);
87 }
88
89 Ok(())
90}
91
92pub fn create_filesystem<ObjectID: FsVerityHashValue>(
108 repo: &Repository<ObjectID>,
109 config_name: &OciDigest,
110 config_verity: Option<&ObjectID>,
111 options: &OciTransformOptions,
112) -> Result<FileSystem<ObjectID>> {
113 let mut filesystem = FileSystem::new(Stat::uninitialized());
114
115 let oc = crate::open_config(repo, config_name, config_verity)?;
116 let config = oc.config;
117 let map = oc.layer_refs;
118
119 for diff_id in config.rootfs().diff_ids() {
120 let layer_verity = map
121 .get(diff_id.as_str())
122 .context("OCI config splitstream missing named ref to layer {diff_id}")?;
123
124 if config_verity.is_none() {
125 let mut layer_stream =
129 repo.open_stream("", Some(layer_verity), Some(TAR_LAYER_CONTENT_TYPE))?;
130 let mut context = DigestWrite(Sha256::new());
131 layer_stream.cat(repo, &mut context)?;
132 let content_hash = crate::sha256_output_to_digest(context.finalize());
133 ensure!(
134 content_hash.as_ref() == diff_id,
135 "Layer has incorrect checksum"
136 );
137 }
138
139 let mut layer_stream =
140 repo.open_stream("", Some(layer_verity), Some(TAR_LAYER_CONTENT_TYPE))?;
141 while let Some(entry) = crate::tar::get_entry(&mut layer_stream)? {
142 process_entry(&mut filesystem, entry)?;
143 }
144 }
145
146 filesystem.transform_for_oci(options)?;
151
152 debug_assert!(
153 filesystem.fsck().is_ok(),
154 "create_filesystem produced invalid filesystem"
155 );
156 Ok(filesystem)
157}
158
159#[cfg(test)]
160mod test {
161 use composefs::{
162 dumpfile::write_dumpfile,
163 fsverity::Sha256HashValue,
164 repository::RepositoryConfig,
165 tree::{LeafContent, RegularFile, Stat},
166 };
167 use std::{collections::BTreeMap, io::BufRead, path::PathBuf};
168
169 use super::*;
170
171 fn file_entry<ObjectID: FsVerityHashValue>(path: &str) -> TarEntry<ObjectID> {
172 TarEntry {
173 path: PathBuf::from(path),
174 stat: Stat {
175 st_mode: 0o644,
176 st_uid: 0,
177 st_gid: 0,
178 st_mtim_sec: 0,
179 st_mtim_nsec: 0,
180 xattrs: BTreeMap::new(),
181 },
182 item: TarItem::Leaf(LeafContent::Regular(RegularFile::Inline([].into()))),
183 }
184 }
185
186 fn dir_entry<ObjectID: FsVerityHashValue>(path: &str) -> TarEntry<ObjectID> {
187 TarEntry {
188 path: PathBuf::from(path),
189 stat: Stat {
190 st_mode: 0o755,
191 st_uid: 0,
192 st_gid: 0,
193 st_mtim_sec: 0,
194 st_mtim_nsec: 0,
195 xattrs: BTreeMap::new(),
196 },
197 item: TarItem::Directory,
198 }
199 }
200
201 fn assert_files(fs: &FileSystem<impl FsVerityHashValue>, expected: &[&str]) -> Result<()> {
202 let mut out = vec![];
203 write_dumpfile(&mut out, fs)?;
204 let actual: Vec<String> = out
205 .lines()
206 .map(|line| line.unwrap().split_once(' ').unwrap().0.into())
207 .collect();
208
209 similar_asserts::assert_eq!(actual, expected);
210 Ok(())
211 }
212
213 fn append_tar_dir(builder: &mut ::tar::Builder<Vec<u8>>, name: &str) {
214 let mut header = ::tar::Header::new_ustar();
215 header.set_uid(0);
216 header.set_gid(0);
217 header.set_mode(0o755);
218 header.set_entry_type(::tar::EntryType::Directory);
219 header.set_size(0);
220 builder
221 .append_data(&mut header, name, std::io::empty())
222 .unwrap();
223 }
224
225 fn append_tar_file(builder: &mut ::tar::Builder<Vec<u8>>, name: &str, content: &[u8]) {
227 let mut header = ::tar::Header::new_ustar();
228 header.set_uid(0);
229 header.set_gid(0);
230 header.set_mode(0o644);
231 header.set_entry_type(::tar::EntryType::Regular);
232 header.set_size(content.len() as u64);
233 builder.append_data(&mut header, name, content).unwrap();
234 }
235
236 fn append_tar_symlink(builder: &mut ::tar::Builder<Vec<u8>>, name: &str, target: &str) {
238 let mut header = ::tar::Header::new_ustar();
239 header.set_uid(0);
240 header.set_gid(0);
241 header.set_mode(0o777);
242 header.set_entry_type(::tar::EntryType::Symlink);
243 header.set_size(0);
244 builder.append_link(&mut header, name, target).unwrap();
245 }
246
247 fn append_tar_hardlink(builder: &mut ::tar::Builder<Vec<u8>>, name: &str, target: &str) {
249 let mut header = ::tar::Header::new_ustar();
250 header.set_uid(0);
251 header.set_gid(0);
252 header.set_mode(0o644);
253 header.set_entry_type(::tar::EntryType::Link);
254 header.set_size(0);
255 builder.append_link(&mut header, name, target).unwrap();
256 }
257
258 fn build_baseimage() -> (Vec<u8>, String) {
263 let mut builder = ::tar::Builder::new(vec![]);
264
265 append_tar_dir(&mut builder, "bin"); append_tar_dir(&mut builder, "etc");
268 append_tar_dir(&mut builder, "tmp");
269 append_tar_dir(&mut builder, "usr");
270 append_tar_dir(&mut builder, "usr/bin");
271 append_tar_dir(&mut builder, "usr/lib");
272 append_tar_dir(&mut builder, "usr/share");
273 append_tar_dir(&mut builder, "usr/share/doc");
274 append_tar_dir(&mut builder, "var");
275 append_tar_dir(&mut builder, "var/log");
276
277 append_tar_file(&mut builder, "etc/hostname", b"busybox-container\n");
279 append_tar_file(
280 &mut builder,
281 "etc/resolv.conf",
282 b"nameserver 8.8.8.8\nnameserver 8.8.4.4\n",
283 );
284
285 append_tar_file(
287 &mut builder,
288 "etc/passwd",
289 b"root:x:0:0:root:/root:/bin/sh\nnobody:x:65534:65534:Nobody:/nonexistent:/usr/sbin/nologin\n\
290 daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n",
291 );
292
293 let busybox_content: Vec<u8> = (0..65536u64).map(|i| (i % 251) as u8).collect();
295 append_tar_file(&mut builder, "usr/bin/busybox", &busybox_content);
296
297 let libc_content: Vec<u8> = (0..32768u64).map(|i| (i % 241) as u8).collect();
298 append_tar_file(&mut builder, "usr/lib/libc.so", &libc_content);
299
300 let readme_content = "composefs-rs test image\n\
301 This is a synthetic busybox-like filesystem used for round-trip testing.\n\
302 It exercises inline files, external files, symlinks, and hardlinks.\n\
303 The filesystem layout mimics a minimal container image with /usr merge.\n\
304 Generated by build_baseimage() in the composefs-oci test suite.\n\
305 ----\n\
306 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod\n\
307 tempor incididunt ut labore et dolore magna aliqua.\n";
308 append_tar_file(
309 &mut builder,
310 "usr/share/doc/README",
311 readme_content.as_bytes(),
312 );
313
314 let messages_content: Vec<u8> = (0..8192u64).map(|i| (i % 239) as u8).collect();
315 append_tar_file(&mut builder, "var/log/messages", &messages_content);
316
317 append_tar_symlink(&mut builder, "usr/bin/cat", "busybox");
319 append_tar_symlink(&mut builder, "usr/bin/ls", "busybox");
320 append_tar_symlink(&mut builder, "usr/bin/sh", "busybox");
321 append_tar_symlink(&mut builder, "usr/lib/libc.so.6", "libc.so");
322
323 append_tar_hardlink(&mut builder, "usr/bin/cp", "usr/bin/busybox");
325
326 append_tar_symlink(&mut builder, "bin", "usr/bin");
330
331 let data = builder.into_inner().unwrap();
332 let diff_id = crate::sha256_content_digest(&data).to_string();
333 (data, diff_id)
334 }
335
336 #[tokio::test]
340 async fn test_build_baseimage_roundtrip() -> Result<()> {
341 use composefs::{
342 INLINE_CONTENT_MAX_V0,
343 repository::{Repository, RepositoryConfig},
344 test::tempdir,
345 };
346 use rustix::fs::CWD;
347 use std::ffi::OsStr;
348 use std::sync::Arc;
349
350 let (tar_data, diff_id_str) = build_baseimage();
351 let diff_id: OciDigest = diff_id_str.parse()?;
352
353 let repo_dir = tempdir();
354 let repo_path = repo_dir.path().join("repo");
355 let (repo, _) = Repository::<Sha256HashValue>::init_path(
356 CWD,
357 &repo_path,
358 RepositoryConfig::default().set_insecure(),
359 )?;
360 let repo = Arc::new(repo);
361 let (verity, _stats) =
362 crate::import_layer(&repo, &diff_id, Some("layer"), &tar_data[..]).await?;
363
364 let mut stream = repo.open_stream("refs/layer", Some(&verity), None)?;
365 let mut entries = vec![];
366 while let Some(entry) = crate::tar::get_entry(&mut stream)? {
367 entries.push(entry);
368 }
369
370 let by_path = |p: &str| -> &TarEntry<Sha256HashValue> {
372 entries
373 .iter()
374 .find(|e| e.path == PathBuf::from(p))
375 .unwrap_or_else(|| panic!("missing entry for {p}"))
376 };
377
378 let expected_dirs = [
380 "/bin", "/etc",
382 "/tmp",
383 "/usr",
384 "/usr/bin",
385 "/usr/lib",
386 "/usr/share",
387 "/usr/share/doc",
388 "/var",
389 "/var/log",
390 ];
391 for dir in &expected_dirs {
392 let entry = by_path(dir);
393 assert!(
394 matches!(entry.item, TarItem::Directory),
395 "{dir} should be a directory, got {:?}",
396 entry.item
397 );
398 assert_eq!(entry.stat.st_mode, 0o755, "{dir} mode");
399 }
400
401 let hostname = by_path("/etc/hostname");
403 match &hostname.item {
404 TarItem::Leaf(LeafContent::Regular(RegularFile::Inline(data))) => {
405 assert_eq!(data.as_ref(), b"busybox-container\n");
406 assert!(
407 data.len() <= INLINE_CONTENT_MAX_V0,
408 "hostname should be inline ({} bytes <= {INLINE_CONTENT_MAX_V0})",
409 data.len()
410 );
411 }
412 other => panic!("expected inline file for /etc/hostname, got {other:?}"),
413 }
414
415 let resolv = by_path("/etc/resolv.conf");
416 match &resolv.item {
417 TarItem::Leaf(LeafContent::Regular(RegularFile::Inline(data))) => {
418 assert!(data.starts_with(b"nameserver"));
419 assert!(
420 data.len() <= INLINE_CONTENT_MAX_V0,
421 "resolv.conf should be inline ({} bytes <= {INLINE_CONTENT_MAX_V0})",
422 data.len()
423 );
424 }
425 other => panic!("expected inline file for /etc/resolv.conf, got {other:?}"),
426 }
427
428 let passwd = by_path("/etc/passwd");
430 match &passwd.item {
431 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
432 assert!(
433 *size as usize > INLINE_CONTENT_MAX_V0,
434 "passwd should be external ({size} bytes > {INLINE_CONTENT_MAX_V0})"
435 );
436 }
437 other => panic!("expected external file for /etc/passwd, got {other:?}"),
438 }
439
440 let busybox = by_path("/usr/bin/busybox");
441 match &busybox.item {
442 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
443 assert_eq!(*size, 65536, "busybox should be 64KB");
444 }
445 other => panic!("expected external file for /usr/bin/busybox, got {other:?}"),
446 }
447
448 let libc = by_path("/usr/lib/libc.so");
449 match &libc.item {
450 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
451 assert_eq!(*size, 32768, "libc.so should be 32KB");
452 }
453 other => panic!("expected external file for /usr/lib/libc.so, got {other:?}"),
454 }
455
456 let readme = by_path("/usr/share/doc/README");
457 match &readme.item {
458 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
459 assert!(
460 *size as usize > INLINE_CONTENT_MAX_V0,
461 "README should be external ({size} bytes)"
462 );
463 }
464 other => panic!("expected external file for README, got {other:?}"),
465 }
466
467 let messages = by_path("/var/log/messages");
468 match &messages.item {
469 TarItem::Leaf(LeafContent::Regular(RegularFile::External(_, size))) => {
470 assert_eq!(*size, 8192, "messages should be 8KB");
471 }
472 other => panic!("expected external file for /var/log/messages, got {other:?}"),
473 }
474
475 let symlinks = [
477 ("/usr/bin/cat", "busybox"),
478 ("/usr/bin/ls", "busybox"),
479 ("/usr/bin/sh", "busybox"),
480 ("/usr/lib/libc.so.6", "libc.so"),
481 ];
482 for (path, target) in &symlinks {
483 let entry = by_path(path);
484 match &entry.item {
485 TarItem::Leaf(LeafContent::Symlink(t)) => {
486 assert_eq!(&**t, OsStr::new(target), "{path} symlink target");
487 }
488 other => panic!("expected symlink for {path}, got {other:?}"),
489 }
490 }
491
492 let cp = by_path("/usr/bin/cp");
495 match &cp.item {
496 TarItem::Hardlink(target) => {
497 assert_eq!(target, OsStr::new("/usr/bin/busybox"), "cp hardlink target");
498 }
499 other => panic!("expected hardlink for /usr/bin/cp, got {other:?}"),
500 }
501
502 let bin_entries: Vec<_> = entries
506 .iter()
507 .filter(|e| e.path == PathBuf::from("/bin"))
508 .collect();
509 assert!(
510 bin_entries.len() >= 2,
511 "/bin should appear as both a directory and a symlink"
512 );
513 let last_bin = bin_entries.last().unwrap();
514 match &last_bin.item {
515 TarItem::Leaf(LeafContent::Symlink(t)) => {
516 assert_eq!(&**t, OsStr::new("usr/bin"), "/bin symlink target");
517 }
518 other => panic!("expected symlink for final /bin, got {other:?}"),
519 }
520
521 let expected_count = 10 + 7 + 4 + 1 + 1; assert_eq!(
530 entries.len(),
531 expected_count,
532 "total entry count (dirs + files + symlinks + hardlinks)"
533 );
534
535 Ok(())
536 }
537
538 #[test]
539 fn test_process_entry() -> Result<()> {
540 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
541
542 process_entry(&mut fs, dir_entry("/a"))?;
544 process_entry(&mut fs, dir_entry("b"))?;
545 process_entry(&mut fs, dir_entry("c"))?;
546 assert_files(&fs, &["/", "/a", "/b", "/c"])?;
547
548 process_entry(&mut fs, file_entry("/a/b"))?;
550 process_entry(&mut fs, file_entry("/a/c"))?;
551 process_entry(&mut fs, file_entry("/b/a"))?;
552 process_entry(&mut fs, file_entry("/b/c"))?;
553 process_entry(&mut fs, file_entry("/c/a"))?;
554 process_entry(&mut fs, file_entry("/c/c"))?;
555 assert_files(
556 &fs,
557 &[
558 "/", "/a", "/a/b", "/a/c", "/b", "/b/a", "/b/c", "/c", "/c/a", "/c/c",
559 ],
560 )?;
561
562 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"])?;
567
568 Ok(())
569 }
570
571 #[test]
574 fn test_whiteout_file_removes_entry() -> Result<()> {
575 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
576
577 process_entry(&mut fs, dir_entry("/etc"))?;
578 process_entry(&mut fs, file_entry("/etc/hosts"))?;
579 process_entry(&mut fs, file_entry("/etc/passwd"))?;
580 assert_files(&fs, &["/", "/etc", "/etc/hosts", "/etc/passwd"])?;
581
582 process_entry(&mut fs, file_entry("/etc/.wh.hosts"))?;
584 assert_files(&fs, &["/", "/etc", "/etc/passwd"])?;
585
586 Ok(())
587 }
588
589 #[test]
590 fn test_whiteout_nonexistent_file_is_noop() -> Result<()> {
591 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
592
593 process_entry(&mut fs, dir_entry("/etc"))?;
594 process_entry(&mut fs, file_entry("/etc/hosts"))?;
595 assert_files(&fs, &["/", "/etc", "/etc/hosts"])?;
596
597 process_entry(&mut fs, file_entry("/etc/.wh.nosuchfile"))?;
599 assert_files(&fs, &["/", "/etc", "/etc/hosts"])?;
600
601 Ok(())
602 }
603
604 #[test]
605 fn test_whiteout_directory() -> Result<()> {
606 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
607
608 process_entry(&mut fs, dir_entry("/usr"))?;
609 process_entry(&mut fs, dir_entry("/usr/local"))?;
610 process_entry(&mut fs, file_entry("/usr/local/bin"))?;
611 process_entry(&mut fs, dir_entry("/etc"))?;
612 assert_files(&fs, &["/", "/etc", "/usr", "/usr/local", "/usr/local/bin"])?;
613
614 process_entry(&mut fs, file_entry("/usr/.wh.local"))?;
616 assert_files(&fs, &["/", "/etc", "/usr"])?;
617
618 Ok(())
619 }
620
621 #[test]
622 fn test_whiteout_in_root_directory() -> Result<()> {
623 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
624
625 process_entry(&mut fs, dir_entry("/mydir"))?;
626 process_entry(&mut fs, file_entry("/toplevel"))?;
627 assert_files(&fs, &["/", "/mydir", "/toplevel"])?;
628
629 process_entry(&mut fs, file_entry("/.wh.toplevel"))?;
631 assert_files(&fs, &["/", "/mydir"])?;
632
633 process_entry(&mut fs, file_entry(".wh.mydir"))?;
635 assert_files(&fs, &["/"])?;
636
637 Ok(())
638 }
639
640 #[test]
641 fn test_whiteout_in_nested_directory() -> Result<()> {
642 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
643
644 process_entry(&mut fs, dir_entry("/a"))?;
645 process_entry(&mut fs, dir_entry("/a/b"))?;
646 process_entry(&mut fs, dir_entry("/a/b/c"))?;
647 process_entry(&mut fs, file_entry("/a/b/c/deep"))?;
648 assert_files(&fs, &["/", "/a", "/a/b", "/a/b/c", "/a/b/c/deep"])?;
649
650 process_entry(&mut fs, file_entry("/a/b/c/.wh.deep"))?;
651 assert_files(&fs, &["/", "/a", "/a/b", "/a/b/c"])?;
652
653 Ok(())
654 }
655
656 #[test]
657 fn test_opaque_whiteout_clears_directory() -> Result<()> {
658 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
659
660 process_entry(&mut fs, dir_entry("/etc"))?;
661 process_entry(&mut fs, file_entry("/etc/hosts"))?;
662 process_entry(&mut fs, file_entry("/etc/passwd"))?;
663 process_entry(&mut fs, file_entry("/etc/resolv.conf"))?;
664 assert_files(
665 &fs,
666 &["/", "/etc", "/etc/hosts", "/etc/passwd", "/etc/resolv.conf"],
667 )?;
668
669 process_entry(&mut fs, file_entry("/etc/.wh..wh..opq"))?;
671 assert_files(&fs, &["/", "/etc"])?;
672
673 Ok(())
674 }
675
676 #[test]
677 fn test_opaque_whiteout_then_add_new_entries() -> Result<()> {
678 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
682
683 process_entry(&mut fs, dir_entry("/etc"))?;
684 process_entry(&mut fs, file_entry("/etc/old_config"))?;
685 process_entry(&mut fs, file_entry("/etc/another_old"))?;
686 assert_files(&fs, &["/", "/etc", "/etc/another_old", "/etc/old_config"])?;
687
688 process_entry(&mut fs, file_entry("/etc/.wh..wh..opq"))?;
690 assert_files(&fs, &["/", "/etc"])?;
691
692 process_entry(&mut fs, file_entry("/etc/new_config"))?;
694 process_entry(&mut fs, file_entry("/etc/new_other"))?;
695 assert_files(&fs, &["/", "/etc", "/etc/new_config", "/etc/new_other"])?;
696
697 Ok(())
698 }
699
700 #[test]
701 fn test_multiple_whiteouts_in_single_layer() -> Result<()> {
702 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
703
704 process_entry(&mut fs, dir_entry("/usr"))?;
705 process_entry(&mut fs, file_entry("/usr/a"))?;
706 process_entry(&mut fs, file_entry("/usr/b"))?;
707 process_entry(&mut fs, file_entry("/usr/c"))?;
708 process_entry(&mut fs, file_entry("/usr/d"))?;
709 assert_files(&fs, &["/", "/usr", "/usr/a", "/usr/b", "/usr/c", "/usr/d"])?;
710
711 process_entry(&mut fs, file_entry("/usr/.wh.a"))?;
713 process_entry(&mut fs, file_entry("/usr/.wh.c"))?;
714 assert_files(&fs, &["/", "/usr", "/usr/b", "/usr/d"])?;
715
716 Ok(())
717 }
718
719 #[test]
720 fn test_double_whiteout_is_idempotent() -> Result<()> {
721 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
722
723 process_entry(&mut fs, dir_entry("/d"))?;
724 process_entry(&mut fs, file_entry("/d/target"))?;
725 assert_files(&fs, &["/", "/d", "/d/target"])?;
726
727 process_entry(&mut fs, file_entry("/d/.wh.target"))?;
729 assert_files(&fs, &["/", "/d"])?;
730
731 process_entry(&mut fs, file_entry("/d/.wh.target"))?;
732 assert_files(&fs, &["/", "/d"])?;
733
734 Ok(())
735 }
736
737 #[test]
738 fn test_whiteout_unusual_name_dot_wh_dot() -> Result<()> {
739 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
744
745 process_entry(&mut fs, dir_entry("/d"))?;
746 process_entry(&mut fs, file_entry("/d/real_file"))?;
747 assert_files(&fs, &["/", "/d", "/d/real_file"])?;
748
749 process_entry(&mut fs, file_entry("/d/.wh..wh."))?;
754 assert_files(&fs, &["/", "/d", "/d/real_file"])?;
755
756 process_entry(&mut fs, file_entry("/d/.wh."))?;
759 assert_files(&fs, &["/", "/d", "/d/real_file"])?;
760
761 Ok(())
762 }
763
764 #[test]
765 fn test_whiteout_across_multiple_directories() -> Result<()> {
766 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
767
768 process_entry(&mut fs, dir_entry("/a"))?;
769 process_entry(&mut fs, dir_entry("/b"))?;
770 process_entry(&mut fs, file_entry("/a/file1"))?;
771 process_entry(&mut fs, file_entry("/a/file2"))?;
772 process_entry(&mut fs, file_entry("/b/file1"))?;
773 process_entry(&mut fs, file_entry("/b/file2"))?;
774 assert_files(
775 &fs,
776 &[
777 "/", "/a", "/a/file1", "/a/file2", "/b", "/b/file1", "/b/file2",
778 ],
779 )?;
780
781 process_entry(&mut fs, file_entry("/a/.wh.file1"))?;
783 process_entry(&mut fs, file_entry("/b/.wh.file2"))?;
784 assert_files(&fs, &["/", "/a", "/a/file2", "/b", "/b/file1"])?;
785
786 Ok(())
787 }
788
789 #[test]
790 fn test_opaque_whiteout_with_subdirectories() -> Result<()> {
791 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
793
794 process_entry(&mut fs, dir_entry("/parent"))?;
795 process_entry(&mut fs, dir_entry("/parent/child"))?;
796 process_entry(&mut fs, file_entry("/parent/child/deep"))?;
797 process_entry(&mut fs, file_entry("/parent/sibling"))?;
798 assert_files(
799 &fs,
800 &[
801 "/",
802 "/parent",
803 "/parent/child",
804 "/parent/child/deep",
805 "/parent/sibling",
806 ],
807 )?;
808
809 process_entry(&mut fs, file_entry("/parent/.wh..wh..opq"))?;
810 assert_files(&fs, &["/", "/parent"])?;
811
812 Ok(())
813 }
814
815 #[test]
816 fn test_whiteout_then_recreate() -> Result<()> {
817 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
819
820 process_entry(&mut fs, dir_entry("/etc"))?;
821 process_entry(&mut fs, file_entry("/etc/config"))?;
822 assert_files(&fs, &["/", "/etc", "/etc/config"])?;
823
824 process_entry(&mut fs, file_entry("/etc/.wh.config"))?;
826 assert_files(&fs, &["/", "/etc"])?;
827
828 process_entry(&mut fs, file_entry("/etc/config"))?;
829 assert_files(&fs, &["/", "/etc", "/etc/config"])?;
830
831 Ok(())
832 }
833
834 #[tokio::test]
840 async fn test_create_filesystem_filters_xattrs() -> Result<()> {
841 use composefs::{generic_tree::XattrFiltering, repository::Repository, test::tempdir};
842 use rustix::fs::CWD;
843 use std::{cell::Cell, ffi::OsStr, sync::Arc};
844
845 let repo_dir = tempdir();
846 let repo_path = repo_dir.path().join("repo");
847 let (repo, _) = Repository::<Sha256HashValue>::init_path(
848 CWD,
849 &repo_path,
850 RepositoryConfig::default().set_insecure(),
851 )?;
852 let repo = Arc::new(repo);
853
854 let layer_str = "\
855/ 0 40755 2 0 0 0 0.0 - - -
856/etc 0 40755 2 0 0 0 0.0 - - - security.selinux=system_u:object_r:etc_t:s0
857/usr 0 40755 2 0 0 0 0.0 - - -
858/usr/bin 0 40755 2 0 0 0 0.0 - - -
859/usr/bin/foo 12 100755 1 0 0 0 0.0 - test_content - security.selinux=system_u:object_r:bin_t:s0 security.capability=\\x02\\x00\\x00\\x02\\x00\\x20\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00 user.testattr=hello
860";
861
862 let image = crate::test_util::create_multi_layer_image(&repo, None, &[layer_str]).await;
863 let expected_cap =
864 b"\x02\x00\x00\x02\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
865
866 for (mode, keep_user_xattr) in [
867 (XattrFiltering::AllowlistOnly, false),
868 (XattrFiltering::KeepUserXattrs, true),
869 ] {
870 let options = composefs::generic_tree::OciTransformOptions { xattrs: mode };
871 let fs = create_filesystem(&repo, &image.config_digest, None, &options)?;
872
873 let has_selinux = Cell::new(false);
876 fs.for_each_stat(|stat| {
877 if stat.xattrs.contains_key(OsStr::new("security.selinux")) {
878 has_selinux.set(true);
879 }
880 });
881 assert!(
882 !has_selinux.get(),
883 "{mode:?}: should have stripped all security.selinux xattrs"
884 );
885
886 let usr_dir = fs.root.get_directory(OsStr::new("usr"))?;
888 let bin_dir = usr_dir.get_directory(OsStr::new("bin"))?;
889 let leaf_id = bin_dir.leaf_id(OsStr::new("foo"))?;
890 let leaf = fs.leaf(leaf_id);
891
892 let cap_val = leaf
893 .stat
894 .xattrs
895 .get(OsStr::new("security.capability"))
896 .unwrap_or_else(|| panic!("{mode:?}: security.capability should be preserved"));
897 assert_eq!(cap_val.as_ref(), expected_cap, "{mode:?}");
898
899 assert_eq!(
900 leaf.stat.xattrs.contains_key(OsStr::new("user.testattr")),
901 keep_user_xattr,
902 "{mode:?}: user.testattr presence"
903 );
904 }
905
906 Ok(())
907 }
908}