drawbridge_type/tree/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2mod 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    /// Returns an entry corresponding to the root of the tree
56    pub fn root(&self) -> &Entry<Content<F>> {
57        // SAFETY: A `Tree` can only be constructed via functionality
58        // in this module and therefore always has a root.
59        self.get(&Path::ROOT).unwrap()
60    }
61}
62
63impl<F: std::io::Read> Tree<F> {
64    /// Returns a file [Entry].
65    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    /// Returns a directory [Entry].
77    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                                    // TODO: Remove the need for a clone, we probably should have
172                                    // Path and PathBuf analogues for that
173                                    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}