1use std::{ffi::OsStr, os::unix::ffi::OsStrExt, rc::Rc};
12
13use anyhow::{ensure, Context, Result};
14use sha2::{Digest, Sha256};
15
16use composefs::{
17 fsverity::FsVerityHashValue,
18 repository::Repository,
19 tree::{Directory, FileSystem, Inode, Leaf, Stat},
20};
21
22use crate::skopeo::TAR_LAYER_CONTENT_TYPE;
23use crate::tar::{TarEntry, TarItem};
24
25pub fn process_entry<ObjectID: FsVerityHashValue>(
33 filesystem: &mut FileSystem<ObjectID>,
34 entry: TarEntry<ObjectID>,
35) -> Result<()> {
36 if entry.path.file_name().is_none() {
37 ensure!(
39 matches!(entry.item, TarItem::Directory),
40 "Unpacking layer tar: filename {:?} must be a directory",
41 entry.path
42 );
43
44 filesystem.set_root_stat(entry.stat);
46 return Ok(());
47 }
48
49 let inode = match entry.item {
50 TarItem::Directory => Inode::Directory(Box::from(Directory::new(entry.stat))),
51 TarItem::Leaf(content) => Inode::Leaf(Rc::new(Leaf {
52 stat: entry.stat,
53 content,
54 })),
55 TarItem::Hardlink(target) => {
56 let (dir, filename) = filesystem.root.split(&target)?;
57 Inode::Leaf(dir.ref_leaf(filename)?)
58 }
59 };
60
61 let (dir, filename) = filesystem
62 .root
63 .split_mut(entry.path.as_os_str())
64 .with_context(|| {
65 format!(
66 "Error unpacking container layer file {:?} {:?}",
67 entry.path, inode
68 )
69 })?;
70
71 let bytes = filename.as_bytes();
72 if let Some(whiteout) = bytes.strip_prefix(b".wh.") {
73 if whiteout == b".wh..opq" {
74 dir.clear();
76 } else {
77 dir.remove(OsStr::from_bytes(whiteout));
78 }
79 } else {
80 dir.merge(filename, inode);
81 }
82
83 Ok(())
84}
85
86pub fn create_filesystem<ObjectID: FsVerityHashValue>(
99 repo: &Repository<ObjectID>,
100 config_name: &str,
101 config_verity: Option<&ObjectID>,
102) -> Result<FileSystem<ObjectID>> {
103 let mut filesystem = FileSystem::new(Stat::uninitialized());
104
105 let (config, map) = crate::open_config(repo, config_name, config_verity)?;
106
107 for diff_id in config.rootfs().diff_ids() {
108 let layer_verity = map
109 .get(diff_id.as_str())
110 .context("OCI config splitstream missing named ref to layer {diff_id}")?;
111
112 if config_verity.is_none() {
113 let mut layer_stream =
117 repo.open_stream("", Some(layer_verity), Some(TAR_LAYER_CONTENT_TYPE))?;
118 let mut context = Sha256::new();
119 layer_stream.cat(repo, &mut context)?;
120 let content_hash = format!("sha256:{}", hex::encode(context.finalize()));
121 ensure!(content_hash == *diff_id, "Layer has incorrect checksum");
122 }
123
124 let mut layer_stream =
125 repo.open_stream("", Some(layer_verity), Some(TAR_LAYER_CONTENT_TYPE))?;
126 while let Some(entry) = crate::tar::get_entry(&mut layer_stream)? {
127 process_entry(&mut filesystem, entry)?;
128 }
129 }
130
131 filesystem.transform_for_oci()?;
134
135 Ok(filesystem)
136}
137
138#[cfg(test)]
139mod test {
140 use composefs::{
141 dumpfile::write_dumpfile,
142 fsverity::Sha256HashValue,
143 tree::{LeafContent, RegularFile, Stat},
144 };
145 use std::{cell::RefCell, collections::BTreeMap, io::BufRead, path::PathBuf};
146
147 use super::*;
148
149 fn file_entry<ObjectID: FsVerityHashValue>(path: &str) -> TarEntry<ObjectID> {
150 TarEntry {
151 path: PathBuf::from(path),
152 stat: Stat {
153 st_mode: 0o644,
154 st_uid: 0,
155 st_gid: 0,
156 st_mtim_sec: 0,
157 xattrs: RefCell::new(BTreeMap::new()),
158 },
159 item: TarItem::Leaf(LeafContent::Regular(RegularFile::Inline([].into()))),
160 }
161 }
162
163 fn dir_entry<ObjectID: FsVerityHashValue>(path: &str) -> TarEntry<ObjectID> {
164 TarEntry {
165 path: PathBuf::from(path),
166 stat: Stat {
167 st_mode: 0o755,
168 st_uid: 0,
169 st_gid: 0,
170 st_mtim_sec: 0,
171 xattrs: RefCell::new(BTreeMap::new()),
172 },
173 item: TarItem::Directory,
174 }
175 }
176
177 fn assert_files(fs: &FileSystem<impl FsVerityHashValue>, expected: &[&str]) -> Result<()> {
178 let mut out = vec![];
179 write_dumpfile(&mut out, fs)?;
180 let actual: Vec<String> = out
181 .lines()
182 .map(|line| line.unwrap().split_once(' ').unwrap().0.into())
183 .collect();
184
185 similar_asserts::assert_eq!(actual, expected);
186 Ok(())
187 }
188
189 #[test]
190 fn test_process_entry() -> Result<()> {
191 let mut fs = FileSystem::<Sha256HashValue>::new(Stat::uninitialized());
192
193 process_entry(&mut fs, dir_entry("/a"))?;
195 process_entry(&mut fs, dir_entry("b"))?;
196 process_entry(&mut fs, dir_entry("c"))?;
197 assert_files(&fs, &["/", "/a", "/b", "/c"])?;
198
199 process_entry(&mut fs, file_entry("/a/b"))?;
201 process_entry(&mut fs, file_entry("/a/c"))?;
202 process_entry(&mut fs, file_entry("/b/a"))?;
203 process_entry(&mut fs, file_entry("/b/c"))?;
204 process_entry(&mut fs, file_entry("/c/a"))?;
205 process_entry(&mut fs, file_entry("/c/c"))?;
206 assert_files(
207 &fs,
208 &[
209 "/", "/a", "/a/b", "/a/c", "/b", "/b/a", "/b/c", "/c", "/c/a", "/c/c",
210 ],
211 )?;
212
213 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"])?;
218
219 Ok(())
220 }
221}