Skip to main content

mars_schema/
tree.rs

1//! Host-independent package tree access.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5/// Read access to a package tree through normalized package-relative paths.
6///
7/// Paths use `/` separators, are never absolute, and contain no empty, `.` or
8/// `..` components. The empty path names the tree root for directory queries.
9pub trait TreeSource {
10    fn read(&self, path: &str) -> Option<Vec<u8>>;
11    fn is_file(&self, path: &str) -> bool;
12    fn is_dir(&self, path: &str) -> bool;
13
14    /// Return the direct child paths of `dir` in stable lexical order.
15    fn children(&self, dir: &str) -> Vec<String>;
16}
17
18/// An in-memory package tree for WASM callers and fixtures.
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct FileTree {
21    files: BTreeMap<String, Vec<u8>>,
22    children: BTreeMap<String, BTreeSet<String>>,
23}
24
25impl FileTree {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    pub fn from_files(
31        files: impl IntoIterator<Item = (String, Vec<u8>)>,
32    ) -> Result<Self, TreePathError> {
33        let mut tree = Self::new();
34        for (path, bytes) in files {
35            tree.insert(path, bytes)?;
36        }
37        Ok(tree)
38    }
39
40    pub fn insert(
41        &mut self,
42        path: impl Into<String>,
43        bytes: impl Into<Vec<u8>>,
44    ) -> Result<(), TreePathError> {
45        let path = path.into();
46        validate_file_path(&path)?;
47        let components = path.split('/').collect::<Vec<_>>();
48        let has_file_ancestor =
49            (1..components.len()).any(|end| self.files.contains_key(&components[..end].join("/")));
50        let descendant_prefix = format!("{path}/");
51        let has_file_descendant = self
52            .files
53            .range(path.clone()..)
54            .next()
55            .is_some_and(|(file, _)| file.starts_with(&descendant_prefix));
56        if has_file_ancestor || has_file_descendant {
57            return Err(TreePathError {
58                path,
59                reason: "a path cannot be both a file and a directory",
60            });
61        }
62        index_children(&mut self.children, &path);
63        self.files.insert(path, bytes.into());
64        Ok(())
65    }
66
67    pub fn files(&self) -> &BTreeMap<String, Vec<u8>> {
68        &self.files
69    }
70}
71
72impl TreeSource for FileTree {
73    fn read(&self, path: &str) -> Option<Vec<u8>> {
74        self.files.get(path).cloned()
75    }
76
77    fn is_file(&self, path: &str) -> bool {
78        self.files.contains_key(path)
79    }
80
81    fn is_dir(&self, path: &str) -> bool {
82        path.is_empty() || self.children.contains_key(path)
83    }
84
85    fn children(&self, dir: &str) -> Vec<String> {
86        self.children
87            .get(dir)
88            .map(|children| children.iter().cloned().collect())
89            .unwrap_or_default()
90    }
91}
92
93/// A view of a tree below one directory: `prefix` becomes the root.
94pub struct SubTree<'a, T: TreeSource> {
95    inner: &'a T,
96    prefix: String,
97}
98
99impl<'a, T: TreeSource> SubTree<'a, T> {
100    /// `prefix` is a normalized directory path of `inner`.
101    pub fn new(inner: &'a T, prefix: impl Into<String>) -> Self {
102        Self {
103            inner,
104            prefix: prefix.into(),
105        }
106    }
107
108    fn full(&self, path: &str) -> String {
109        match (self.prefix.is_empty(), path.is_empty()) {
110            (true, _) => path.to_owned(),
111            (false, true) => self.prefix.clone(),
112            (false, false) => format!("{}/{path}", self.prefix),
113        }
114    }
115}
116
117impl<T: TreeSource> TreeSource for SubTree<'_, T> {
118    fn read(&self, path: &str) -> Option<Vec<u8>> {
119        self.inner.read(&self.full(path))
120    }
121
122    fn is_file(&self, path: &str) -> bool {
123        self.inner.is_file(&self.full(path))
124    }
125
126    fn is_dir(&self, path: &str) -> bool {
127        self.inner.is_dir(&self.full(path))
128    }
129
130    fn children(&self, dir: &str) -> Vec<String> {
131        let skip = if self.prefix.is_empty() {
132            0
133        } else {
134            self.prefix.len() + 1
135        };
136        self.inner
137            .children(&self.full(dir))
138            .into_iter()
139            .map(|child| child[skip..].to_owned())
140            .collect()
141    }
142}
143
144fn index_children(index: &mut BTreeMap<String, BTreeSet<String>>, path: &str) {
145    let components = path.split('/').collect::<Vec<_>>();
146    for child_index in 0..components.len() {
147        let parent = components[..child_index].join("/");
148        let child = components[..=child_index].join("/");
149        index.entry(parent).or_default().insert(child);
150    }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
154#[error("invalid package tree path `{path}`: {reason}")]
155pub struct TreePathError {
156    pub path: String,
157    pub reason: &'static str,
158}
159
160impl TreePathError {
161    /// A stable identifier for callers that map failures to their own codes.
162    pub fn code(&self) -> &'static str {
163        "tree.invalid-path"
164    }
165}
166
167fn validate_file_path(path: &str) -> Result<(), TreePathError> {
168    let invalid = |reason| TreePathError {
169        path: path.to_owned(),
170        reason,
171    };
172    if path.is_empty() {
173        return Err(invalid("a file path cannot be empty"));
174    }
175    if path.starts_with('/') {
176        return Err(invalid("paths must be package-relative"));
177    }
178    if path.contains('\\') {
179        return Err(invalid("paths must use `/` separators"));
180    }
181    if path
182        .split('/')
183        .any(|component| component.is_empty() || matches!(component, "." | ".."))
184    {
185        return Err(invalid("paths must be normalized"));
186    }
187    Ok(())
188}