1use std::{ffi::OsStr, os::unix::ffi::OsStrExt, rc::Rc};
2
3use anyhow::{ensure, Context, Result};
4use oci_spec::image::ImageConfiguration;
5
6use composefs::{
7 fsverity::FsVerityHashValue,
8 repository::Repository,
9 tree::{Directory, FileSystem, Inode, Leaf},
10};
11
12use crate::tar::{TarEntry, TarItem};
13
14pub fn process_entry<ObjectID: FsVerityHashValue>(
15 filesystem: &mut FileSystem<ObjectID>,
16 entry: TarEntry<ObjectID>,
17) -> Result<()> {
18 if entry.path.file_name().is_none() {
19 ensure!(
21 matches!(entry.item, TarItem::Directory),
22 "Unpacking layer tar: filename {:?} must be a directory",
23 entry.path
24 );
25
26 filesystem.set_root_stat(entry.stat);
28 return Ok(());
29 }
30
31 let inode = match entry.item {
32 TarItem::Directory => Inode::Directory(Box::from(Directory::new(entry.stat))),
33 TarItem::Leaf(content) => Inode::Leaf(Rc::new(Leaf {
34 stat: entry.stat,
35 content,
36 })),
37 TarItem::Hardlink(target) => {
38 let (dir, filename) = filesystem.root.split(&target)?;
39 Inode::Leaf(dir.ref_leaf(filename)?)
40 }
41 };
42
43 let (dir, filename) = filesystem
44 .root
45 .split_mut(entry.path.as_os_str())
46 .with_context(|| {
47 format!(
48 "Error unpacking container layer file {:?} {:?}",
49 entry.path, inode
50 )
51 })?;
52
53 let bytes = filename.as_bytes();
54 if let Some(whiteout) = bytes.strip_prefix(b".wh.") {
55 if whiteout == b".wh.opq" {
56 dir.clear();
58 } else {
59 dir.remove(OsStr::from_bytes(whiteout));
60 }
61 } else {
62 dir.merge(filename, inode);
63 }
64
65 Ok(())
66}
67
68pub fn create_filesystem<ObjectID: FsVerityHashValue>(
71 repo: &Repository<ObjectID>,
72 config_name: &str,
73 config_verity: Option<&ObjectID>,
74) -> Result<FileSystem<ObjectID>> {
75 let mut filesystem = FileSystem::default();
76
77 let mut config_stream = repo.open_stream(config_name, config_verity)?;
78 let config = ImageConfiguration::from_reader(&mut config_stream)?;
79
80 for diff_id in config.rootfs().diff_ids() {
81 let layer_sha256 = super::sha256_from_digest(diff_id)?;
82 let layer_verity = config_stream.lookup(&layer_sha256)?;
83
84 let mut layer_stream = repo.open_stream(&hex::encode(layer_sha256), Some(layer_verity))?;
85 while let Some(entry) = crate::tar::get_entry(&mut layer_stream)? {
86 process_entry(&mut filesystem, entry)?;
87 }
88 }
89
90 Ok(filesystem)
91}
92
93#[cfg(test)]
94mod test {
95 use composefs::{
96 dumpfile::write_dumpfile,
97 fsverity::Sha256HashValue,
98 tree::{LeafContent, RegularFile, Stat},
99 };
100 use std::{cell::RefCell, collections::BTreeMap, io::BufRead, path::PathBuf};
101
102 use super::*;
103
104 fn file_entry<ObjectID: FsVerityHashValue>(path: &str) -> TarEntry<ObjectID> {
105 TarEntry {
106 path: PathBuf::from(path),
107 stat: Stat {
108 st_mode: 0o644,
109 st_uid: 0,
110 st_gid: 0,
111 st_mtim_sec: 0,
112 xattrs: RefCell::new(BTreeMap::new()),
113 },
114 item: TarItem::Leaf(LeafContent::Regular(RegularFile::Inline([].into()))),
115 }
116 }
117
118 fn dir_entry<ObjectID: FsVerityHashValue>(path: &str) -> TarEntry<ObjectID> {
119 TarEntry {
120 path: PathBuf::from(path),
121 stat: Stat {
122 st_mode: 0o755,
123 st_uid: 0,
124 st_gid: 0,
125 st_mtim_sec: 0,
126 xattrs: RefCell::new(BTreeMap::new()),
127 },
128 item: TarItem::Directory,
129 }
130 }
131
132 fn assert_files(fs: &FileSystem<impl FsVerityHashValue>, expected: &[&str]) -> Result<()> {
133 let mut out = vec![];
134 write_dumpfile(&mut out, fs)?;
135 let actual: Vec<String> = out
136 .lines()
137 .map(|line| line.unwrap().split_once(' ').unwrap().0.into())
138 .collect();
139
140 similar_asserts::assert_eq!(actual, expected);
141 Ok(())
142 }
143
144 #[test]
145 fn test_process_entry() -> Result<()> {
146 let mut fs = FileSystem::<Sha256HashValue>::default();
147
148 process_entry(&mut fs, dir_entry("/a"))?;
150 process_entry(&mut fs, dir_entry("b"))?;
151 process_entry(&mut fs, dir_entry("c"))?;
152 assert_files(&fs, &["/", "/a", "/b", "/c"])?;
153
154 process_entry(&mut fs, file_entry("/a/b"))?;
156 process_entry(&mut fs, file_entry("/a/c"))?;
157 process_entry(&mut fs, file_entry("/b/a"))?;
158 process_entry(&mut fs, file_entry("/b/c"))?;
159 process_entry(&mut fs, file_entry("/c/a"))?;
160 process_entry(&mut fs, file_entry("/c/c"))?;
161 assert_files(
162 &fs,
163 &[
164 "/", "/a", "/a/b", "/a/c", "/b", "/b/a", "/b/c", "/c", "/c/a", "/c/c",
165 ],
166 )?;
167
168 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"])?;
173
174 Ok(())
175 }
176}