quillmark_core/quill/
tree.rs1use std::collections::HashMap;
3use std::error::Error as StdError;
4use std::path::Path;
5#[derive(Debug, Clone)]
7pub enum FileTreeNode {
8 File {
10 contents: Vec<u8>,
12 },
13 Directory {
15 files: HashMap<String, FileTreeNode>,
17 },
18}
19
20impl FileTreeNode {
21 pub fn get_node<P: AsRef<Path>>(&self, path: P) -> Option<&FileTreeNode> {
23 let path = path.as_ref();
24
25 if path == Path::new("") {
27 return Some(self);
28 }
29
30 let mut components: Vec<&str> = Vec::new();
36 for c in path.components() {
37 match c {
38 std::path::Component::Normal(s) => match s.to_str() {
39 Some(s) => components.push(s),
40 None => return None,
41 },
42 _ => return None,
43 }
44 }
45
46 if components.is_empty() {
47 return Some(self);
48 }
49
50 let mut current_node = self;
52 for component in components {
53 match current_node {
54 FileTreeNode::Directory { files } => {
55 current_node = files.get(component)?;
56 }
57 FileTreeNode::File { .. } => {
58 return None; }
60 }
61 }
62
63 Some(current_node)
64 }
65
66 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
68 match self.get_node(path)? {
69 FileTreeNode::File { contents } => Some(contents.as_slice()),
70 FileTreeNode::Directory { .. } => None,
71 }
72 }
73
74 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
76 matches!(self.get_node(path), Some(FileTreeNode::File { .. }))
77 }
78
79 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
81 matches!(self.get_node(path), Some(FileTreeNode::Directory { .. }))
82 }
83
84 pub fn list_files<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
86 match self.get_node(dir_path) {
87 Some(FileTreeNode::Directory { files }) => files
88 .iter()
89 .filter_map(|(name, node)| {
90 if matches!(node, FileTreeNode::File { .. }) {
91 Some(name.clone())
92 } else {
93 None
94 }
95 })
96 .collect(),
97 _ => Vec::new(),
98 }
99 }
100
101 pub fn list_subdirectories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
103 match self.get_node(dir_path) {
104 Some(FileTreeNode::Directory { files }) => files
105 .iter()
106 .filter_map(|(name, node)| {
107 if matches!(node, FileTreeNode::Directory { .. }) {
108 Some(name.clone())
109 } else {
110 None
111 }
112 })
113 .collect(),
114 _ => Vec::new(),
115 }
116 }
117
118 pub fn insert<P: AsRef<Path>>(
120 &mut self,
121 path: P,
122 node: FileTreeNode,
123 ) -> Result<(), Box<dyn StdError + Send + Sync>> {
124 let path = path.as_ref();
125
126 let mut components: Vec<String> = Vec::new();
129 for c in path.components() {
130 match c {
131 std::path::Component::Normal(s) => {
132 components.push(
133 s.to_str()
134 .ok_or("Path component is not valid UTF-8")?
135 .to_string(),
136 );
137 }
138 std::path::Component::ParentDir => {
139 return Err("Path traversal ('..') is not allowed".into());
140 }
141 std::path::Component::CurDir => {
142 return Err("Current-directory ('.') components are not allowed".into());
143 }
144 std::path::Component::RootDir | std::path::Component::Prefix(_) => {
145 return Err("Absolute paths are not allowed; use a relative path".into());
146 }
147 }
148 }
149
150 if components.is_empty() {
151 return Err("Cannot insert at root path".into());
152 }
153
154 let mut current_node = self;
156 for component in &components[..components.len() - 1] {
157 match current_node {
158 FileTreeNode::Directory { files } => {
159 current_node =
160 files
161 .entry(component.clone())
162 .or_insert_with(|| FileTreeNode::Directory {
163 files: HashMap::new(),
164 });
165 }
166 FileTreeNode::File { .. } => {
167 return Err("Cannot traverse into a file".into());
168 }
169 }
170 }
171
172 let filename = &components[components.len() - 1];
174 match current_node {
175 FileTreeNode::Directory { files } => {
176 files.insert(filename.clone(), node);
177 Ok(())
178 }
179 FileTreeNode::File { .. } => Err("Cannot insert into a file".into()),
180 }
181 }
182
183 pub fn flatten(&self) -> Vec<(String, Vec<u8>)> {
196 let mut out = Vec::new();
197 self.flatten_into(String::new(), &mut out);
198 out.sort_by(|(a, _), (b, _)| a.cmp(b));
199 out
200 }
201
202 fn flatten_into(&self, prefix: String, out: &mut Vec<(String, Vec<u8>)>) {
203 match self {
204 FileTreeNode::File { contents } => {
205 if !prefix.is_empty() {
208 out.push((prefix, contents.clone()));
209 }
210 }
211 FileTreeNode::Directory { files } => {
212 for (name, node) in files {
213 let path = if prefix.is_empty() {
214 name.clone()
215 } else {
216 format!("{}/{}", prefix, name)
217 };
218 node.flatten_into(path, out);
219 }
220 }
221 }
222 }
223
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 fn sample() -> FileTreeNode {
231 let mut root = FileTreeNode::Directory {
232 files: std::collections::HashMap::new(),
233 };
234 root.insert(
235 "a/b.txt",
236 FileTreeNode::File {
237 contents: b"hi".to_vec(),
238 },
239 )
240 .unwrap();
241 root
242 }
243
244 #[test]
245 fn get_node_rejects_traversal_components() {
246 let t = sample();
247 assert!(t.get_file("a/b.txt").is_some());
249 assert!(t.get_node("a/../b.txt").is_none());
252 assert!(t.get_node("./a/b.txt").is_none());
253 assert!(t.get_node("/a/b.txt").is_none());
254 }
255}