Skip to main content

hd_oci/
unpack.rs

1// OCI layer unpacking into CAS.
2
3use std::io::Read;
4
5use hd_cas::ContentHash;
6use hd_cas::ContentStore;
7
8#[derive(Debug, Clone)]
9pub struct UnpackedEntry {
10    pub path: String,
11    pub manifest_hash: ContentHash,
12    pub mode: u32,
13}
14
15#[derive(Debug, thiserror::Error)]
16pub enum UnpackError {
17    #[error("tar error: {0}")]
18    Tar(String),
19    #[error("I/O error: {0}")]
20    Io(#[from] std::io::Error),
21    #[error("store error: {0}")]
22    Store(#[from] hd_cas::store::StoreError),
23}
24
25/// Unpack an OCI layer (tar or tar+gzip) into the CAS.
26/// Returns a list of unpacked file entries with their CAS manifest hashes.
27pub fn unpack_layer(
28    data: &[u8],
29    store: &ContentStore,
30    gzipped: bool,
31) -> Result<Vec<UnpackedEntry>, UnpackError> {
32    let reader: Box<dyn Read> = if gzipped {
33        Box::new(flate2::read::GzDecoder::new(data))
34    } else {
35        Box::new(data)
36    };
37
38    let mut archive = tar::Archive::new(reader);
39    let mut entries = Vec::new();
40
41    for entry_result in archive
42        .entries()
43        .map_err(|e| UnpackError::Tar(e.to_string()))?
44    {
45        let mut entry = entry_result.map_err(|e| UnpackError::Tar(e.to_string()))?;
46
47        // Skip directories and non-regular files
48        if entry.header().entry_type() != tar::EntryType::Regular {
49            continue;
50        }
51
52        let path = entry
53            .path()
54            .map_err(|e| UnpackError::Tar(e.to_string()))?
55            .to_string_lossy()
56            .to_string();
57
58        let mode = entry.header().mode().unwrap_or(0o644);
59
60        let mut content = Vec::new();
61        entry.read_to_end(&mut content)?;
62
63        let manifest_hash = store.put_file_from_bytes(&content, mode)?;
64
65        entries.push(UnpackedEntry {
66            path,
67            manifest_hash,
68            mode,
69        });
70    }
71
72    Ok(entries)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use hd_cas::ContentStore;
79    use std::io::Write;
80    use tempfile::TempDir;
81
82    fn create_test_tar() -> Vec<u8> {
83        let mut builder = tar::Builder::new(Vec::new());
84        let data = b"hello world";
85        let mut header = tar::Header::new_gnu();
86        header.set_path("test.txt").unwrap();
87        header.set_size(data.len() as u64);
88        header.set_mode(0o644);
89        header.set_cksum();
90        builder.append(&header, &data[..]).unwrap();
91        builder.into_inner().unwrap()
92    }
93
94    fn create_test_targz() -> Vec<u8> {
95        let tar_data = create_test_tar();
96        let mut encoder =
97            flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
98        encoder.write_all(&tar_data).unwrap();
99        encoder.finish().unwrap()
100    }
101
102    #[test]
103    fn unpack_tar_into_cas() {
104        let dir = TempDir::new().unwrap();
105        let store = ContentStore::open(dir.path()).unwrap();
106        let tar_data = create_test_tar();
107
108        let entries = unpack_layer(&tar_data, &store, false).unwrap();
109        assert_eq!(entries.len(), 1);
110        assert_eq!(entries[0].path, "test.txt");
111
112        // Verify content is in CAS
113        let out_path = dir.path().join("recovered.txt");
114        store
115            .get_file(&entries[0].manifest_hash, &out_path)
116            .unwrap();
117        assert_eq!(std::fs::read(&out_path).unwrap(), b"hello world");
118    }
119
120    #[test]
121    fn unpack_targz_into_cas() {
122        let dir = TempDir::new().unwrap();
123        let store = ContentStore::open(dir.path()).unwrap();
124        let targz_data = create_test_targz();
125
126        let entries = unpack_layer(&targz_data, &store, true).unwrap();
127        assert_eq!(entries.len(), 1);
128        assert_eq!(entries[0].path, "test.txt");
129    }
130
131    #[test]
132    fn unpack_multiple_files() {
133        let dir = TempDir::new().unwrap();
134        let store = ContentStore::open(dir.path()).unwrap();
135
136        let mut builder = tar::Builder::new(Vec::new());
137        for name in &["a.txt", "b.txt", "c.txt"] {
138            let data = name.as_bytes();
139            let mut header = tar::Header::new_gnu();
140            header.set_path(name).unwrap();
141            header.set_size(data.len() as u64);
142            header.set_mode(0o644);
143            header.set_cksum();
144            builder.append(&header, data).unwrap();
145        }
146        let tar_data = builder.into_inner().unwrap();
147
148        let entries = unpack_layer(&tar_data, &store, false).unwrap();
149        assert_eq!(entries.len(), 3);
150    }
151}