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