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 components: Vec<_> = path
32 .components()
33 .filter_map(|c| {
34 if let std::path::Component::Normal(s) = c {
35 s.to_str()
36 } else {
37 None
38 }
39 })
40 .collect();
41
42 if components.is_empty() {
43 return Some(self);
44 }
45
46 let mut current_node = self;
48 for component in components {
49 match current_node {
50 FileTreeNode::Directory { files } => {
51 current_node = files.get(component)?;
52 }
53 FileTreeNode::File { .. } => {
54 return None; }
56 }
57 }
58
59 Some(current_node)
60 }
61
62 pub fn get_file<P: AsRef<Path>>(&self, path: P) -> Option<&[u8]> {
64 match self.get_node(path)? {
65 FileTreeNode::File { contents } => Some(contents.as_slice()),
66 FileTreeNode::Directory { .. } => None,
67 }
68 }
69
70 pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
72 matches!(self.get_node(path), Some(FileTreeNode::File { .. }))
73 }
74
75 pub fn dir_exists<P: AsRef<Path>>(&self, path: P) -> bool {
77 matches!(self.get_node(path), Some(FileTreeNode::Directory { .. }))
78 }
79
80 pub fn list_files<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
82 match self.get_node(dir_path) {
83 Some(FileTreeNode::Directory { files }) => files
84 .iter()
85 .filter_map(|(name, node)| {
86 if matches!(node, FileTreeNode::File { .. }) {
87 Some(name.clone())
88 } else {
89 None
90 }
91 })
92 .collect(),
93 _ => Vec::new(),
94 }
95 }
96
97 pub fn list_subdirectories<P: AsRef<Path>>(&self, dir_path: P) -> Vec<String> {
99 match self.get_node(dir_path) {
100 Some(FileTreeNode::Directory { files }) => files
101 .iter()
102 .filter_map(|(name, node)| {
103 if matches!(node, FileTreeNode::Directory { .. }) {
104 Some(name.clone())
105 } else {
106 None
107 }
108 })
109 .collect(),
110 _ => Vec::new(),
111 }
112 }
113
114 pub fn insert<P: AsRef<Path>>(
116 &mut self,
117 path: P,
118 node: FileTreeNode,
119 ) -> Result<(), Box<dyn StdError + Send + Sync>> {
120 let path = path.as_ref();
121
122 let mut components: Vec<String> = Vec::new();
125 for c in path.components() {
126 match c {
127 std::path::Component::Normal(s) => {
128 components.push(
129 s.to_str()
130 .ok_or("Path component is not valid UTF-8")?
131 .to_string(),
132 );
133 }
134 std::path::Component::ParentDir => {
135 return Err("Path traversal ('..') is not allowed".into());
136 }
137 std::path::Component::CurDir => {
138 return Err("Current-directory ('.') components are not allowed".into());
139 }
140 std::path::Component::RootDir | std::path::Component::Prefix(_) => {
141 return Err("Absolute paths are not allowed; use a relative path".into());
142 }
143 }
144 }
145
146 if components.is_empty() {
147 return Err("Cannot insert at root path".into());
148 }
149
150 let mut current_node = self;
152 for component in &components[..components.len() - 1] {
153 match current_node {
154 FileTreeNode::Directory { files } => {
155 current_node =
156 files
157 .entry(component.clone())
158 .or_insert_with(|| FileTreeNode::Directory {
159 files: HashMap::new(),
160 });
161 }
162 FileTreeNode::File { .. } => {
163 return Err("Cannot traverse into a file".into());
164 }
165 }
166 }
167
168 let filename = &components[components.len() - 1];
170 match current_node {
171 FileTreeNode::Directory { files } => {
172 files.insert(filename.clone(), node);
173 Ok(())
174 }
175 FileTreeNode::File { .. } => Err("Cannot insert into a file".into()),
176 }
177 }
178
179 pub fn print_tree(&self) -> String {
180 self.print_tree_recursive("", "", true)
181 }
182
183 fn print_tree_recursive(&self, name: &str, prefix: &str, is_last: bool) -> String {
184 let mut result = String::new();
185
186 let connector = if is_last { "└── " } else { "├── " };
188 let extension = if is_last { " " } else { "│ " };
189
190 match self {
191 FileTreeNode::File { .. } => {
192 result.push_str(&format!("{}{}{}\n", prefix, connector, name));
193 }
194 FileTreeNode::Directory { files } => {
195 result.push_str(&format!("{}{}{}/\n", prefix, connector, name));
197
198 let child_prefix = format!("{}{}", prefix, extension);
199 let count = files.len();
200
201 for (i, (child_name, node)) in files.iter().enumerate() {
202 let is_last_child = i == count - 1;
203 result.push_str(&node.print_tree_recursive(
204 child_name,
205 &child_prefix,
206 is_last_child,
207 ));
208 }
209 }
210 }
211
212 result
213 }
214}