1use 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
25pub 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 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 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}