1mod context;
3mod directory;
4mod entry;
5mod name;
6mod path;
7
8pub use context::*;
9pub use directory::*;
10pub use entry::*;
11pub use name::*;
12pub use path::*;
13
14use super::digest::Algorithms;
15use super::Meta;
16
17use std::borrow::Borrow;
18use std::collections::BTreeMap;
19use std::ffi::OsStr;
20use std::ops::Bound::{Excluded, Unbounded};
21use std::ops::Deref;
22
23use mime::{Mime, APPLICATION_OCTET_STREAM};
24use serde::Serialize;
25use walkdir::WalkDir;
26
27#[derive(Debug, Clone)]
28pub enum Content<F> {
29 File(F),
30 Directory(Vec<u8>),
31}
32
33#[repr(transparent)]
34#[derive(Debug, Clone)]
35pub struct Tree<F>(BTreeMap<Path, Entry<Content<F>>>);
36
37impl<F> Deref for Tree<F> {
38 type Target = BTreeMap<Path, Entry<Content<F>>>;
39
40 fn deref(&self) -> &Self::Target {
41 &self.0
42 }
43}
44
45impl<F> IntoIterator for Tree<F> {
46 type Item = (Path, Entry<Content<F>>);
47 type IntoIter = std::collections::btree_map::IntoIter<Path, Entry<Content<F>>>;
48
49 fn into_iter(self) -> Self::IntoIter {
50 self.0.into_iter()
51 }
52}
53
54impl<F> Tree<F> {
55 pub fn root(&self) -> &Entry<Content<F>> {
57 self.get(&Path::ROOT).unwrap()
60 }
61}
62
63impl<F: std::io::Read> Tree<F> {
64 pub fn file_entry_sync(mut content: F, mime: Mime) -> std::io::Result<Entry<Content<F>>> {
66 let (size, hash) = Algorithms::default().read_sync(&mut content)?;
67 Ok(Entry {
68 meta: Meta { hash, size, mime },
69 custom: Default::default(),
70 content: Content::File(content),
71 })
72 }
73}
74
75impl<F> Tree<F> {
76 pub fn dir_entry_sync<FI, E: Borrow<Entry<Content<FI>>> + Serialize>(
78 dir: impl Borrow<Directory<E>>,
79 ) -> std::io::Result<Entry<Content<F>>> {
80 let buf = serde_json::to_vec(dir.borrow()).map_err(|e| {
81 std::io::Error::new(
82 std::io::ErrorKind::Other,
83 format!("failed to encode directory to JSON: {e}",),
84 )
85 })?;
86 let (size, hash) = Algorithms::default().read_sync(&buf[..])?;
87 Ok(Entry {
88 meta: Meta {
89 hash,
90 size,
91 mime: Directory::<()>::TYPE.parse().unwrap(),
92 },
93 custom: Default::default(),
94 content: Content::Directory(buf),
95 })
96 }
97}
98
99impl<F> TryFrom<Directory<Entry<Content<F>>>> for Tree<F> {
100 type Error = std::io::Error;
101
102 fn try_from(dir: Directory<Entry<Content<F>>>) -> std::io::Result<Self> {
103 let mut tree: BTreeMap<Path, Entry<Content<F>>> = BTreeMap::new();
104 let root = Self::dir_entry_sync(&dir)?;
105 assert!(tree.insert(Path::ROOT, root).is_none());
106 for (name, entry) in dir {
107 assert!(tree.insert(name.into(), entry).is_none());
108 }
109 Ok(Self(tree))
110 }
111}
112
113impl<F> TryFrom<BTreeMap<Name, Entry<Content<F>>>> for Tree<F> {
114 type Error = std::io::Error;
115
116 fn try_from(dir: BTreeMap<Name, Entry<Content<F>>>) -> std::io::Result<Self> {
117 let dir: Directory<_> = dir.into();
118 dir.try_into()
119 }
120}
121
122impl Tree<std::fs::File> {
123 fn invalid_data_error(
124 error: impl Into<Box<dyn std::error::Error + Send + Sync>>,
125 ) -> std::io::Error {
126 use std::io;
127
128 io::Error::new(io::ErrorKind::InvalidData, error)
129 }
130
131 pub fn from_path_sync(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
132 let mut tree: BTreeMap<Path, Entry<Content<std::fs::File>>> = BTreeMap::new();
133 WalkDir::new(&path)
134 .contents_first(true)
135 .follow_links(true)
136 .into_iter()
137 .try_for_each(|r| {
138 let e = r?;
139
140 let path = e.path().strip_prefix(&path).map_err(|e| {
141 Self::invalid_data_error(format!("failed to trim tree root path prefix: {e}",))
142 })?;
143 let path = path.to_str().ok_or_else(|| {
144 Self::invalid_data_error(format!(
145 "failed to convert tree path `{}` to Unicode",
146 path.to_string_lossy(),
147 ))
148 })?;
149 let path = path.parse().map_err(|err| {
150 Self::invalid_data_error(format!("failed to parse tree path `{path}`: {err}",))
151 })?;
152
153 let entry = match e.file_type() {
154 t if t.is_file() => {
155 let path = e.path();
156 let file = std::fs::File::open(path)?;
157 Self::file_entry_sync(
158 file,
159 match path.extension().and_then(OsStr::to_str) {
160 Some("wasm") => "application/wasm".parse().unwrap(),
161 Some("toml") => "application/toml".parse().unwrap(),
162 _ => APPLICATION_OCTET_STREAM,
163 },
164 )?
165 }
166 t if t.is_dir() => {
167 let dir: Directory<_> = tree
168 .range((Excluded(&path), Unbounded))
169 .map_while(|(p, e)| match p.split_last() {
170 Some((base, dir)) if dir == path.as_slice() => {
171 Some((base.clone(), e))
174 }
175 _ => None,
176 })
177 .collect();
178 Self::dir_entry_sync(dir)?
179 }
180 _ => {
181 return Err(Self::invalid_data_error(format!(
182 "unsupported file type encountered at `{path}`",
183 )))
184 }
185 };
186 if tree.insert(path, entry).is_some() {
187 Err(Self::invalid_data_error("duplicate file name {name}"))
188 } else {
189 Ok(())
190 }
191 })?;
192 Ok(Self(tree))
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 use std::fs::{create_dir, write};
201 use std::io::{Read, Seek};
202
203 use tempfile::tempdir;
204
205 #[test]
206 fn try_from_btree_map() {
207 let bin = Tree::file_entry_sync(
208 [0xde, 0xad, 0xbe, 0xef].as_slice(),
209 APPLICATION_OCTET_STREAM,
210 )
211 .unwrap();
212
213 let conf = Tree::file_entry_sync(
214 r#"steward = "example.com"
215args = ["foo", "bar"]"#
216 .as_bytes(),
217 "application/toml".parse().unwrap(),
218 )
219 .unwrap();
220
221 let tree = Tree::try_from(BTreeMap::from([
222 ("main.wasm".parse().unwrap(), bin.clone()),
223 ("Enarx.toml".parse().unwrap(), conf.clone()),
224 ]))
225 .unwrap();
226 let root = tree.root().clone();
227
228 let mut tree = tree.into_iter();
229 let (path, entry) = tree.next().unwrap();
230 assert_eq!(path, Path::ROOT);
231 assert_eq!(entry.meta, root.meta);
232
233 let (path, entry) = tree.next().unwrap();
234 assert_eq!(path, "/Enarx.toml".parse().unwrap());
235 assert_eq!(entry.meta, conf.meta);
236
237 let (path, entry) = tree.next().unwrap();
238 assert_eq!(path, "/main.wasm".parse().unwrap());
239 assert_eq!(entry.meta, bin.meta);
240 }
241
242 #[test]
243 fn from_path_sync() {
244 let root = tempdir().expect("failed to create temporary root directory");
245 write(root.path().join("test-file-foo"), "foo").unwrap();
246
247 create_dir(root.path().join("test-dir")).unwrap();
248 write(root.path().join("test-dir").join("test-file-bar"), "bar").unwrap();
249
250 let foo_meta = Algorithms::default()
251 .read_sync("foo".as_bytes())
252 .map(|(size, hash)| Meta {
253 hash,
254 size,
255 mime: APPLICATION_OCTET_STREAM,
256 })
257 .unwrap();
258
259 let bar_meta = Algorithms::default()
260 .read_sync("bar".as_bytes())
261 .map(|(size, hash)| Meta {
262 hash,
263 size,
264 mime: APPLICATION_OCTET_STREAM,
265 })
266 .unwrap();
267
268 let test_dir_json = serde_json::to_vec(&Directory::from({
269 let mut m = BTreeMap::new();
270 assert_eq!(
271 m.insert(
272 "test-file-bar".parse().unwrap(),
273 Entry {
274 meta: bar_meta.clone(),
275 custom: Default::default(),
276 content: (),
277 },
278 ),
279 None
280 );
281 m
282 }))
283 .unwrap();
284 let test_dir_meta = Algorithms::default()
285 .read_sync(&test_dir_json[..])
286 .map(|(size, hash)| Meta {
287 hash,
288 size,
289 mime: Directory::<()>::TYPE.parse().unwrap(),
290 })
291 .unwrap();
292
293 let root_json = serde_json::to_vec(&Directory::from({
294 let mut m = BTreeMap::new();
295 assert_eq!(
296 m.insert(
297 "test-dir".parse().unwrap(),
298 Entry {
299 meta: test_dir_meta.clone(),
300 custom: Default::default(),
301 content: (),
302 },
303 ),
304 None
305 );
306 m
307 }))
308 .unwrap();
309 let root_meta = Algorithms::default()
310 .read_sync(&root_json[..])
311 .map(|(size, hash)| Meta {
312 hash,
313 size,
314 mime: Directory::<()>::TYPE.parse().unwrap(),
315 })
316 .unwrap();
317
318 let tree = Tree::from_path_sync(root.path()).expect("failed to construct a tree");
319
320 assert_eq!(tree.root().meta, root_meta);
321 assert!(tree.root().custom.is_empty());
322 assert!(matches!(tree.root().content, Content::Directory(ref json) if json == &root_json));
323
324 let mut tree = tree.into_iter();
325
326 let (path, entry) = tree.next().unwrap();
327 assert_eq!(path, Path::ROOT);
328 assert_eq!(entry.meta, root_meta);
329 assert!(entry.custom.is_empty());
330 assert!(matches!(entry.content, Content::Directory(json) if json == root_json));
331
332 let (path, entry) = tree.next().unwrap();
333 assert_eq!(path, "test-dir".parse().unwrap());
334 assert_eq!(entry.meta, test_dir_meta);
335 assert!(entry.custom.is_empty());
336 assert!(matches!(entry.content, Content::Directory(json) if json == test_dir_json));
337
338 let (path, entry) = tree.next().unwrap();
339 assert_eq!(path, "test-dir/test-file-bar".parse().unwrap());
340 assert_eq!(entry.meta, bar_meta);
341 assert!(entry.custom.is_empty());
342 assert!(matches!(entry.content, Content::File(_)));
343 if let Content::File(mut file) = entry.content {
344 file.rewind().unwrap();
345 let mut buf = vec![];
346 assert_eq!(file.read_to_end(&mut buf).unwrap(), "bar".len());
347 assert_eq!(buf, "bar".as_bytes());
348 } else {
349 panic!("invalid content type")
350 }
351
352 let (path, entry) = tree.next().unwrap();
353 assert_eq!(path, "test-file-foo".parse().unwrap());
354 assert_eq!(entry.meta, foo_meta);
355 assert!(entry.custom.is_empty());
356 assert!(matches!(entry.content, Content::File(_)));
357 if let Content::File(mut file) = entry.content {
358 file.rewind().unwrap();
359 let mut buf = vec![];
360 assert_eq!(file.read_to_end(&mut buf).unwrap(), "foo".len());
361 assert_eq!(buf, "foo".as_bytes());
362 } else {
363 panic!("invalid content type")
364 }
365
366 assert!(tree.next().is_none());
367 }
368}