tiedcrossing_type/tree/
mod.rs

1// SPDX-FileCopyrightText: 2022 Profian Inc. <opensource@profian.com>
2// SPDX-License-Identifier: AGPL-3.0-only
3
4mod context;
5mod directory;
6mod entry;
7mod name;
8mod path;
9
10pub use context::*;
11pub use directory::*;
12pub use entry::*;
13pub use name::*;
14pub use path::*;
15
16use super::digest::Algorithms;
17use super::Meta;
18
19use std::collections::BTreeMap;
20use std::ffi::OsStr;
21use std::io::Seek;
22use std::ops::Bound::{Excluded, Unbounded};
23use std::ops::Deref;
24
25use mime::APPLICATION_OCTET_STREAM;
26use walkdir::WalkDir;
27
28#[derive(Debug, Clone)]
29pub enum Content<F> {
30    File(F),
31    Directory(Vec<u8>),
32}
33
34#[repr(transparent)]
35#[derive(Debug, Clone)]
36pub struct Tree<F>(BTreeMap<Path, Entry<Content<F>>>);
37
38impl<F> Deref for Tree<F> {
39    type Target = BTreeMap<Path, Entry<Content<F>>>;
40
41    fn deref(&self) -> &Self::Target {
42        &self.0
43    }
44}
45
46impl<F> IntoIterator for Tree<F> {
47    type Item = (Path, Entry<Content<F>>);
48    type IntoIter = std::collections::btree_map::IntoIter<Path, Entry<Content<F>>>;
49
50    fn into_iter(self) -> Self::IntoIter {
51        self.0.into_iter()
52    }
53}
54
55impl<F> Tree<F> {
56    /// Returns an entry corresponding to the root of the tree
57    pub fn root(&self) -> &Entry<Content<F>> {
58        // SAFETY: A `Tree` can only be constructed via functionality
59        // in this module and therefore always has a root.
60        self.get(&Path::ROOT).unwrap()
61    }
62}
63
64impl Tree<std::fs::File> {
65    fn invalid_data_error(
66        error: impl Into<Box<dyn std::error::Error + Send + Sync>>,
67    ) -> std::io::Error {
68        use std::io;
69
70        io::Error::new(io::ErrorKind::InvalidData, error)
71    }
72
73    pub fn from_path_sync(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
74        let mut tree: BTreeMap<Path, Entry<Content<std::fs::File>>> = BTreeMap::new();
75        WalkDir::new(&path)
76            .contents_first(true)
77            .follow_links(true)
78            .into_iter()
79            .try_for_each(|r| {
80                let e = r?;
81
82                let path = e.path().strip_prefix(&path).map_err(|e| {
83                    Self::invalid_data_error(format!("failed to trim tree root path prefix: {e}",))
84                })?;
85                let path = path.to_str().ok_or_else(|| {
86                    Self::invalid_data_error(format!(
87                        "failed to convert tree path `{}` to Unicode",
88                        path.to_string_lossy(),
89                    ))
90                })?;
91                let path = path.parse().map_err(|err| {
92                    Self::invalid_data_error(format!("failed to parse tree path `{path}`: {err}",))
93                })?;
94
95                let entry = match e.file_type() {
96                    t if t.is_file() => {
97                        let mut file = std::fs::File::open(e.path())?;
98                        let (size, hash) = Algorithms::default().read_sync(&mut file)?;
99                        file.rewind()?;
100                        Entry {
101                            meta: Meta {
102                                hash,
103                                size,
104                                mime: match e.path().extension().and_then(OsStr::to_str) {
105                                    Some("wasm") => "application/wasm".parse().unwrap(),
106                                    Some("toml") => "application/toml".parse().unwrap(),
107                                    _ => APPLICATION_OCTET_STREAM,
108                                },
109                            },
110                            custom: Default::default(),
111                            content: Content::File(file),
112                        }
113                    }
114                    t if t.is_dir() => {
115                        let dir: Directory<_> = tree
116                            .range((Excluded(&path), Unbounded))
117                            .map_while(|(p, e)| match p.split_last() {
118                                Some((base, dir)) if dir == path.as_slice() => {
119                                    // TODO: Remove the need for a clone, we probably should have
120                                    // Path and PathBuf analogues for that
121                                    Some((base.clone(), e))
122                                }
123                                _ => None,
124                            })
125                            .collect();
126                        let buf = serde_json::to_vec(&dir).map_err(|e| {
127                            std::io::Error::new(
128                                std::io::ErrorKind::Other,
129                                format!("failed to encode directory to JSON: {e}",),
130                            )
131                        })?;
132                        let (size, hash) = Algorithms::default().read_sync(&buf[..])?;
133                        Entry {
134                            meta: Meta {
135                                hash,
136                                size,
137                                mime: Directory::<()>::TYPE.parse().unwrap(),
138                            },
139                            custom: Default::default(),
140                            content: Content::Directory(buf),
141                        }
142                    }
143                    _ => {
144                        return Err(Self::invalid_data_error(format!(
145                            "unsupported file type encountered at `{path}`",
146                        )))
147                    }
148                };
149                if tree.insert(path, entry).is_some() {
150                    Err(Self::invalid_data_error("duplicate file name {name}"))
151                } else {
152                    Ok(())
153                }
154            })?;
155        Ok(Self(tree))
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    use std::fs::{create_dir, write};
164    use std::io::Read;
165
166    use tempfile::tempdir;
167
168    #[test]
169    fn from_path_sync() {
170        let root = tempdir().expect("failed to create temporary root directory");
171        write(root.path().join("test-file-foo"), "foo").unwrap();
172
173        create_dir(root.path().join("test-dir")).unwrap();
174        write(root.path().join("test-dir").join("test-file-bar"), "bar").unwrap();
175
176        let foo_meta = Algorithms::default()
177            .read_sync("foo".as_bytes())
178            .map(|(size, hash)| Meta {
179                hash,
180                size,
181                mime: APPLICATION_OCTET_STREAM,
182            })
183            .unwrap();
184
185        let bar_meta = Algorithms::default()
186            .read_sync("bar".as_bytes())
187            .map(|(size, hash)| Meta {
188                hash,
189                size,
190                mime: APPLICATION_OCTET_STREAM,
191            })
192            .unwrap();
193
194        let test_dir_json = serde_json::to_vec(&Directory::from({
195            let mut m = BTreeMap::new();
196            m.insert(
197                "test-file-bar".parse().unwrap(),
198                Entry {
199                    meta: bar_meta.clone(),
200                    custom: Default::default(),
201                    content: (),
202                },
203            );
204            m
205        }))
206        .unwrap();
207        let test_dir_meta = Algorithms::default()
208            .read_sync(&test_dir_json[..])
209            .map(|(size, hash)| Meta {
210                hash,
211                size,
212                mime: Directory::<()>::TYPE.parse().unwrap(),
213            })
214            .unwrap();
215
216        let root_json = serde_json::to_vec(&Directory::from({
217            let mut m = BTreeMap::new();
218            m.insert(
219                "test-dir".parse().unwrap(),
220                Entry {
221                    meta: test_dir_meta.clone(),
222                    custom: Default::default(),
223                    content: (),
224                },
225            );
226            m
227        }))
228        .unwrap();
229        let root_meta = Algorithms::default()
230            .read_sync(&root_json[..])
231            .map(|(size, hash)| Meta {
232                hash,
233                size,
234                mime: Directory::<()>::TYPE.parse().unwrap(),
235            })
236            .unwrap();
237
238        let tree = Tree::from_path_sync(root.path()).expect("failed to construct a tree");
239
240        assert_eq!(tree.root().meta, root_meta);
241        assert!(tree.root().custom.is_empty());
242        assert!(matches!(tree.root().content, Content::Directory(ref json) if json == &root_json));
243
244        let mut tree = tree.into_iter();
245
246        let (path, entry) = tree.next().unwrap();
247        assert_eq!(path, Path::ROOT);
248        assert_eq!(entry.meta, root_meta);
249        assert!(entry.custom.is_empty());
250        assert!(matches!(entry.content, Content::Directory(json) if json == root_json));
251
252        let (path, entry) = tree.next().unwrap();
253        assert_eq!(path, "test-dir".parse().unwrap());
254        assert_eq!(entry.meta, test_dir_meta);
255        assert!(entry.custom.is_empty());
256        assert!(matches!(entry.content, Content::Directory(json) if json == test_dir_json));
257
258        let (path, entry) = tree.next().unwrap();
259        assert_eq!(path, "test-dir/test-file-bar".parse().unwrap());
260        assert_eq!(entry.meta, bar_meta);
261        assert!(entry.custom.is_empty());
262        assert!(matches!(entry.content, Content::File(_)));
263        if let Content::File(mut file) = entry.content {
264            let mut buf = vec![];
265            file.read_to_end(&mut buf).unwrap();
266            assert_eq!(buf, "bar".as_bytes());
267        } else {
268            panic!("invalid content type")
269        }
270
271        let (path, entry) = tree.next().unwrap();
272        assert_eq!(path, "test-file-foo".parse().unwrap());
273        assert_eq!(entry.meta, foo_meta);
274        assert!(entry.custom.is_empty());
275        assert!(matches!(entry.content, Content::File(_)));
276        if let Content::File(mut file) = entry.content {
277            let mut buf = vec![];
278            file.read_to_end(&mut buf).unwrap();
279            assert_eq!(buf, "foo".as_bytes());
280        } else {
281            panic!("invalid content type")
282        }
283
284        assert!(tree.next().is_none());
285    }
286}