Skip to main content

composefs_oci/
image.rs

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        // special handling for the root directory
20        ensure!(
21            matches!(entry.item, TarItem::Directory),
22            "Unpacking layer tar: filename {:?} must be a directory",
23            entry.path
24        );
25
26        // Update the stat, but don't do anything else
27        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            // complete name is '.wh..wh.opq'
57            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
68/// Creates a filesystem from the given OCI container.  No special transformations are performed to
69/// make the filesystem bootable.
70pub 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        // both with and without leading slash should be supported
149        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        // add some files
155        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        // try some whiteouts
169        process_entry(&mut fs, file_entry(".wh.a"))?; // entire dir
170        process_entry(&mut fs, file_entry("/b/.wh..wh.opq"))?; // opaque dir
171        process_entry(&mut fs, file_entry("/c/.wh.c"))?; // single file
172        assert_files(&fs, &["/", "/b", "/c", "/c/a"])?;
173
174        Ok(())
175    }
176}