xtask_todo_lib/devshell/vfs/
tree.rs1use std::path::{Path, PathBuf};
4
5use super::copy_to_host::{copy_host_path_to_host_dir, copy_node_to_host};
6use super::error::VfsError;
7use super::node::Node;
8use super::path::resolve_path_with_cwd;
9
10pub struct Vfs {
11 root: Node,
12 cwd: String,
13 host_root: Option<PathBuf>,
16}
17
18impl Default for Vfs {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl Vfs {
25 #[must_use]
26 pub fn new() -> Self {
27 Self {
28 root: Node::Dir {
29 name: String::new(),
30 children: vec![],
31 },
32 cwd: "/".to_string(),
33 host_root: None,
34 }
35 }
36
37 pub fn new_host_root(root: impl AsRef<Path>) -> std::io::Result<Self> {
42 let root = root.as_ref();
43 std::fs::create_dir_all(root)?;
44 let root = root.canonicalize()?;
45 Ok(Self {
46 root: Node::Dir {
47 name: String::new(),
48 children: vec![],
49 },
50 cwd: "/".to_string(),
51 host_root: Some(root),
52 })
53 }
54
55 #[must_use]
57 pub const fn is_host_backed(&self) -> bool {
58 self.host_root.is_some()
59 }
60
61 #[must_use]
63 pub const fn from_parts(root: Node, cwd: String) -> Self {
64 Self {
65 root,
66 cwd,
67 host_root: None,
68 }
69 }
70
71 fn logical_to_host_path(&self, abs_logical: &str) -> PathBuf {
72 let root = self.host_root.as_ref().unwrap();
73 let abs_logical = abs_logical.trim_end_matches('/');
74 let mut p = root.clone();
75 if abs_logical.is_empty() || abs_logical == "/" {
76 return p;
77 }
78 for seg in abs_logical.split('/').filter(|s| !s.is_empty()) {
79 p.push(seg);
80 }
81 p
82 }
83 #[must_use]
84 pub fn cwd(&self) -> &str {
85 &self.cwd
86 }
87 #[must_use]
88 pub const fn root(&self) -> &Node {
89 &self.root
90 }
91
92 pub fn resolve_absolute(&self, path: &str) -> Result<Node, VfsError> {
97 if self.host_root.is_some() {
98 let path = path.trim_end_matches('/');
99 let p = self.logical_to_host_path(path);
100 let meta = std::fs::metadata(&p).map_err(|_| VfsError::InvalidPath)?;
101 if meta.is_file() {
102 let content = std::fs::read(&p).map_err(VfsError::Io)?;
103 let name = p
104 .file_name()
105 .map(|s| s.to_string_lossy().into_owned())
106 .unwrap_or_default();
107 return Ok(Node::File { name, content });
108 }
109 if meta.is_dir() {
110 let name = p
111 .file_name()
112 .map(|s| s.to_string_lossy().into_owned())
113 .unwrap_or_default();
114 return Ok(Node::Dir {
115 name,
116 children: vec![],
117 });
118 }
119 return Err(VfsError::InvalidPath);
120 }
121 let path = path.trim_end_matches('/');
122 if path.is_empty() || path == "/" {
123 return Ok(self.root.clone());
124 }
125 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
126 let mut current = &self.root;
127 for segment in segments {
128 current = current.child(segment).ok_or(VfsError::InvalidPath)?;
129 }
130 Ok(current.clone())
131 }
132
133 #[must_use]
136 pub fn resolve_to_absolute(&self, path: &str) -> String {
137 resolve_path_with_cwd(&self.cwd, path)
138 }
139
140 pub fn mkdir(&mut self, path: &str) -> Result<(), VfsError> {
145 if self.host_root.is_some() {
146 let abs = self.resolve_to_absolute(path);
147 let p = self.logical_to_host_path(&abs);
148 std::fs::create_dir_all(&p).map_err(VfsError::Io)?;
149 return Ok(());
150 }
151 let abs = self.resolve_to_absolute(path);
152 let segments: Vec<&str> = abs.split('/').filter(|s| !s.is_empty()).collect();
153 if segments.is_empty() {
154 return Ok(());
155 }
156 let mut indices: Vec<usize> = vec![];
157 for segment in segments {
158 let current = Self::get_mut_at(&mut self.root, &indices);
159 match current {
160 Node::Dir { children, .. } => {
161 let pos = children.iter().position(|c| c.name() == segment);
162 if let Some(i) = pos {
163 if !children[i].is_dir() {
164 return Err(VfsError::InvalidPath);
165 }
166 indices.push(i);
167 } else {
168 children.push(Node::Dir {
169 name: segment.to_string(),
170 children: vec![],
171 });
172 indices.push(children.len() - 1);
173 }
174 }
175 Node::File { .. } => return Err(VfsError::InvalidPath),
176 }
177 }
178 Ok(())
179 }
180
181 pub fn write_file(&mut self, path: &str, content: &[u8]) -> Result<(), VfsError> {
186 if self.host_root.is_some() {
187 let abs = self.resolve_to_absolute(path);
188 let p = self.logical_to_host_path(&abs);
189 if let Some(parent) = p.parent() {
190 std::fs::create_dir_all(parent).map_err(VfsError::Io)?;
191 }
192 std::fs::write(&p, content).map_err(VfsError::Io)?;
193 return Ok(());
194 }
195 let abs = self.resolve_to_absolute(path);
196 let segments: Vec<&str> = abs.split('/').filter(|s| !s.is_empty()).collect();
197 let (parent_segments, file_name) = match segments.split_last() {
198 Some((last, rest)) => (rest, *last),
199 None => return Err(VfsError::InvalidPath), };
201 let mut indices: Vec<usize> = vec![];
202 for segment in parent_segments {
203 let current = Self::get_mut_at(&mut self.root, &indices);
204 match current {
205 Node::Dir { children, .. } => {
206 let pos = children.iter().position(|c| c.name() == *segment);
207 match pos {
208 Some(i) => {
209 if !children[i].is_dir() {
210 return Err(VfsError::InvalidPath);
211 }
212 indices.push(i);
213 }
214 None => return Err(VfsError::InvalidPath), }
216 }
217 Node::File { .. } => return Err(VfsError::InvalidPath),
218 }
219 }
220 let parent = Self::get_mut_at(&mut self.root, &indices);
221 match parent {
222 Node::Dir { children, .. } => {
223 let pos = children.iter().position(|c| c.name() == file_name);
224 let node = Node::File {
225 name: file_name.to_string(),
226 content: content.to_vec(),
227 };
228 match pos {
229 Some(i) => children[i] = node,
230 None => children.push(node),
231 }
232 Ok(())
233 }
234 Node::File { .. } => Err(VfsError::InvalidPath),
235 }
236 }
237
238 pub fn touch(&mut self, path: &str) -> Result<(), VfsError> {
243 self.write_file(path, &[])
244 }
245
246 pub fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
251 if self.host_root.is_some() {
252 let abs = self.resolve_to_absolute(path);
253 let p = self.logical_to_host_path(&abs);
254 if !p.is_file() {
255 return Err(VfsError::InvalidPath);
256 }
257 return std::fs::read(&p).map_err(VfsError::Io);
258 }
259 let abs = self.resolve_to_absolute(path);
260 let n = self.resolve_absolute(&abs)?;
261 match n {
262 Node::File { content, .. } => Ok(content),
263 Node::Dir { .. } => Err(VfsError::InvalidPath),
264 }
265 }
266
267 pub fn list_dir(&self, path: &str) -> Result<Vec<String>, VfsError> {
272 if self.host_root.is_some() {
273 let abs = self.resolve_to_absolute(path);
274 let p = self.logical_to_host_path(&abs);
275 if !p.is_dir() {
276 return Err(VfsError::InvalidPath);
277 }
278 let mut out = Vec::new();
279 for e in std::fs::read_dir(&p).map_err(VfsError::Io)? {
280 let e = e.map_err(VfsError::Io)?;
281 out.push(e.file_name().to_string_lossy().into_owned());
282 }
283 out.sort();
284 return Ok(out);
285 }
286 let abs = self.resolve_to_absolute(path);
287 let n = self.resolve_absolute(&abs)?;
288 match n {
289 Node::Dir { children, .. } => {
290 Ok(children.iter().map(|c| c.name().to_string()).collect())
291 }
292 Node::File { .. } => Err(VfsError::InvalidPath),
293 }
294 }
295
296 pub fn set_cwd(&mut self, path: &str) -> Result<(), VfsError> {
301 if self.host_root.is_some() {
302 let abs = self.resolve_to_absolute(path);
303 let p = self.logical_to_host_path(&abs);
304 let meta = std::fs::metadata(&p).map_err(|_| VfsError::InvalidPath)?;
305 if !meta.is_dir() {
306 return Err(VfsError::InvalidPath);
307 }
308 self.cwd = if abs == "/" { "/".to_string() } else { abs };
309 return Ok(());
310 }
311 let abs = self.resolve_to_absolute(path);
312 let n = self.resolve_absolute(&abs)?;
313 if !n.is_dir() {
314 return Err(VfsError::InvalidPath);
315 }
316 self.cwd = if abs == "/" { "/".to_string() } else { abs };
317 Ok(())
318 }
319
320 fn get_mut_at<'a>(node: &'a mut Node, path: &[usize]) -> &'a mut Node {
322 if path.is_empty() {
323 return node;
324 }
325 match node {
326 Node::Dir { children, .. } => {
327 let i = path[0];
328 Self::get_mut_at(&mut children[i], &path[1..])
329 }
330 Node::File { .. } => unreachable!("path must follow dirs only"),
331 }
332 }
333
334 pub fn copy_tree_to_host(&self, vfs_path: &str, host_dir: &Path) -> Result<(), VfsError> {
340 let abs = self.resolve_to_absolute(vfs_path);
341 if self.host_root.is_some() {
342 let src = self.logical_to_host_path(&abs);
343 copy_host_path_to_host_dir(&src, host_dir)
344 } else {
345 let node = self.resolve_absolute(&abs)?;
346 copy_node_to_host(&node, host_dir)
347 }
348 }
349}