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 pub fn print_tree(&self) -> String {
225 self.print_tree_recursive("", "", true)
226 }
227
228 fn print_tree_recursive(&self, name: &str, prefix: &str, is_last: bool) -> String {
229 let mut result = String::new();
230
231 let connector = if is_last { "└── " } else { "├── " };
233 let extension = if is_last { " " } else { "│ " };
234
235 match self {
236 FileTreeNode::File { .. } => {
237 result.push_str(&format!("{}{}{}\n", prefix, connector, name));
238 }
239 FileTreeNode::Directory { files } => {
240 result.push_str(&format!("{}{}{}/\n", prefix, connector, name));
242
243 let child_prefix = format!("{}{}", prefix, extension);
244 let count = files.len();
245
246 for (i, (child_name, node)) in files.iter().enumerate() {
247 let is_last_child = i == count - 1;
248 result.push_str(&node.print_tree_recursive(
249 child_name,
250 &child_prefix,
251 is_last_child,
252 ));
253 }
254 }
255 }
256
257 result
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 fn sample() -> FileTreeNode {
266 let mut root = FileTreeNode::Directory {
267 files: std::collections::HashMap::new(),
268 };
269 root.insert(
270 "a/b.txt",
271 FileTreeNode::File {
272 contents: b"hi".to_vec(),
273 },
274 )
275 .unwrap();
276 root
277 }
278
279 #[test]
280 fn get_node_rejects_traversal_components() {
281 let t = sample();
282 assert!(t.get_file("a/b.txt").is_some());
284 assert!(t.get_node("a/../b.txt").is_none());
287 assert!(t.get_node("./a/b.txt").is_none());
288 assert!(t.get_node("/a/b.txt").is_none());
289 }
290}