1mod 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 pub fn root(&self) -> &Entry<Content<F>> {
58 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 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}