1use std::io::Write;
13use std::os::unix::fs::PermissionsExt;
14use std::path::{Component, Path, PathBuf};
15use std::sync::Arc;
16
17use crate::platform::SystemTime;
18
19use crate::error::VfsError;
20use crate::interpreter::pattern::glob_match;
21
22use super::{DirEntry, Metadata, NodeType, VirtualFs};
23
24pub struct ReadWriteFs {
46 root: Option<PathBuf>,
47}
48
49impl ReadWriteFs {
50 pub fn new() -> Self {
52 Self { root: None }
53 }
54
55 pub fn with_root(root: impl Into<PathBuf>) -> std::io::Result<Self> {
61 let root = root.into().canonicalize()?;
62 Ok(Self { root: Some(root) })
63 }
64
65 fn resolve(&self, path: &Path) -> Result<PathBuf, VfsError> {
77 let Some(root) = &self.root else {
78 return Ok(path.to_path_buf());
79 };
80
81 let lossy = path.to_string_lossy();
83 let rel_str = lossy.trim_start_matches('/');
84 let joined = if rel_str.is_empty() {
85 root.clone()
86 } else {
87 root.join(rel_str)
88 };
89
90 let normalized = logical_normalize(&joined);
92
93 if !normalized.starts_with(root) {
95 return Err(VfsError::PermissionDenied(path.to_path_buf()));
96 }
97
98 if normalized == *root {
100 return Ok(root.clone());
101 }
102
103 let name = normalized
105 .file_name()
106 .expect("normalized path has a filename")
107 .to_owned();
108 let parent = normalized.parent().unwrap_or(root);
109
110 let canonical_parent = canonicalize_existing(parent, path, root)?;
112
113 if !canonical_parent.starts_with(root) {
115 return Err(VfsError::PermissionDenied(path.to_path_buf()));
116 }
117
118 Ok(canonical_parent.join(name))
119 }
120
121 fn resolve_follow(&self, path: &Path) -> Result<PathBuf, VfsError> {
128 let resolved = self.resolve(path)?;
129 if let Some(root) = &self.root {
130 match std::fs::symlink_metadata(&resolved) {
131 Ok(meta) if meta.is_symlink() => {
132 let canonical =
133 std::fs::canonicalize(&resolved).map_err(|e| map_io_error(e, path))?;
134 if !canonical.starts_with(root) {
135 return Err(VfsError::PermissionDenied(path.to_path_buf()));
136 }
137 return Ok(canonical);
138 }
139 Ok(_) => {}
140 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
141 Err(e) => return Err(map_io_error(e, path)),
142 }
143 }
144 Ok(resolved)
145 }
146
147 fn is_within_root(&self, real_path: &Path) -> bool {
149 let Some(root) = &self.root else {
150 return true;
151 };
152 match std::fs::canonicalize(real_path) {
153 Ok(canonical) => canonical.starts_with(root),
154 Err(_) => real_path.starts_with(root),
155 }
156 }
157
158 fn glob_walk(
160 &self,
161 real_dir: &Path,
162 components: &[&str],
163 virtual_path: PathBuf,
164 results: &mut Vec<PathBuf>,
165 max: usize,
166 ) {
167 if results.len() >= max || components.is_empty() {
168 if components.is_empty() {
169 results.push(virtual_path);
170 }
171 return;
172 }
173
174 let pattern = components[0];
175 let rest = &components[1..];
176
177 if pattern == "**" {
178 self.glob_walk(real_dir, rest, virtual_path.clone(), results, max);
180
181 let Ok(entries) = std::fs::read_dir(real_dir) else {
183 return;
184 };
185 for entry in entries.flatten() {
186 if results.len() >= max {
187 return;
188 }
189 let name = entry.file_name().to_string_lossy().into_owned();
190 if name.starts_with('.') {
191 continue;
192 }
193 let child_real = real_dir.join(&name);
194 let child_virtual = virtual_path.join(&name);
195
196 let is_dir = entry
197 .file_type()
198 .is_ok_and(|ft| ft.is_dir() || ft.is_symlink());
199 if is_dir && self.is_within_root(&child_real) {
200 self.glob_walk(&child_real, components, child_virtual, results, max);
202 }
203 }
204 } else {
205 let Ok(entries) = std::fs::read_dir(real_dir) else {
206 return;
207 };
208 for entry in entries.flatten() {
209 if results.len() >= max {
210 return;
211 }
212 let name = entry.file_name().to_string_lossy().into_owned();
213 if name.starts_with('.') && !pattern.starts_with('.') {
215 continue;
216 }
217 if glob_match(pattern, &name) {
218 let child_real = real_dir.join(&name);
219 let child_virtual = virtual_path.join(&name);
220 if rest.is_empty() {
221 results.push(child_virtual);
222 } else {
223 let is_dir = entry
224 .file_type()
225 .is_ok_and(|ft| ft.is_dir() || ft.is_symlink());
226 if is_dir && self.is_within_root(&child_real) {
227 self.glob_walk(&child_real, rest, child_virtual, results, max);
228 }
229 }
230 }
231 }
232 }
233 }
234}
235
236impl Default for ReadWriteFs {
237 fn default() -> Self {
238 Self::new()
239 }
240}
241
242impl VirtualFs for ReadWriteFs {
247 fn read_file(&self, path: &Path) -> Result<Vec<u8>, VfsError> {
248 let resolved = self.resolve_follow(path)?;
249 std::fs::read(&resolved).map_err(|e| map_io_error(e, path))
250 }
251
252 fn write_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError> {
253 let resolved = self.resolve_follow(path)?;
254 std::fs::write(&resolved, content).map_err(|e| map_io_error(e, path))
255 }
256
257 fn append_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError> {
258 let resolved = self.resolve_follow(path)?;
259 let mut file = std::fs::OpenOptions::new()
260 .append(true)
261 .open(&resolved)
262 .map_err(|e| map_io_error(e, path))?;
263 file.write_all(content).map_err(|e| map_io_error(e, path))
264 }
265
266 fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
267 let resolved = self.resolve(path)?;
268 std::fs::remove_file(&resolved).map_err(|e| map_io_error(e, path))
269 }
270
271 fn mkdir(&self, path: &Path) -> Result<(), VfsError> {
272 let resolved = self.resolve(path)?;
273 std::fs::create_dir(&resolved).map_err(|e| map_io_error(e, path))
274 }
275
276 fn mkdir_p(&self, path: &Path) -> Result<(), VfsError> {
277 let resolved = self.resolve(path)?;
278 std::fs::create_dir_all(&resolved).map_err(|e| map_io_error(e, path))
279 }
280
281 fn readdir(&self, path: &Path) -> Result<Vec<DirEntry>, VfsError> {
282 let resolved = self.resolve_follow(path)?;
283 let entries = std::fs::read_dir(&resolved).map_err(|e| map_io_error(e, path))?;
284 let mut result = Vec::new();
285 for entry in entries {
286 let entry = entry.map_err(|e| map_io_error(e, path))?;
287 let ft = entry.file_type().map_err(|e| map_io_error(e, path))?;
288 let node_type = if ft.is_dir() {
289 NodeType::Directory
290 } else if ft.is_symlink() {
291 NodeType::Symlink
292 } else {
293 NodeType::File
294 };
295 result.push(DirEntry {
296 name: entry.file_name().to_string_lossy().into_owned(),
297 node_type,
298 });
299 }
300 Ok(result)
301 }
302
303 fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
304 let resolved = self.resolve(path)?;
305 std::fs::remove_dir(&resolved).map_err(|e| map_io_error(e, path))
306 }
307
308 fn remove_dir_all(&self, path: &Path) -> Result<(), VfsError> {
309 let resolved = self.resolve(path)?;
310 std::fs::remove_dir_all(&resolved).map_err(|e| map_io_error(e, path))
311 }
312
313 fn exists(&self, path: &Path) -> bool {
314 match self.resolve(path) {
315 Ok(resolved) => resolved.exists(),
316 Err(_) => false,
317 }
318 }
319
320 fn stat(&self, path: &Path) -> Result<Metadata, VfsError> {
321 let resolved = self.resolve_follow(path)?;
322 let meta = std::fs::metadata(&resolved).map_err(|e| map_io_error(e, path))?;
323 Ok(map_metadata(&meta))
324 }
325
326 fn lstat(&self, path: &Path) -> Result<Metadata, VfsError> {
327 let resolved = self.resolve(path)?;
328 let meta = std::fs::symlink_metadata(&resolved).map_err(|e| map_io_error(e, path))?;
329 Ok(map_metadata(&meta))
330 }
331
332 fn chmod(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
333 let resolved = self.resolve_follow(path)?;
334 let perms = std::fs::Permissions::from_mode(mode);
335 std::fs::set_permissions(&resolved, perms).map_err(|e| map_io_error(e, path))
336 }
337
338 fn utimes(&self, path: &Path, mtime: SystemTime) -> Result<(), VfsError> {
339 let resolved = self.resolve_follow(path)?;
340 let file = std::fs::File::options()
341 .write(true)
342 .open(&resolved)
343 .map_err(|e| map_io_error(e, path))?;
344 file.set_times(std::fs::FileTimes::new().set_modified(mtime))
345 .map_err(|e| map_io_error(e, path))
346 }
347
348 fn symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError> {
349 let resolved_link = self.resolve(link)?;
350 let actual_target = if target.is_absolute() && self.root.is_some() {
353 self.resolve(target)?
354 } else {
355 target.to_path_buf()
356 };
357 std::os::unix::fs::symlink(&actual_target, &resolved_link)
358 .map_err(|e| map_io_error(e, link))
359 }
360
361 fn hardlink(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
362 let resolved_src = self.resolve_follow(src)?;
363 let resolved_dst = self.resolve(dst)?;
364 std::fs::hard_link(&resolved_src, &resolved_dst).map_err(|e| {
365 if e.kind() == std::io::ErrorKind::NotFound {
366 map_io_error(e, src)
367 } else {
368 map_io_error(e, dst)
369 }
370 })
371 }
372
373 fn readlink(&self, path: &Path) -> Result<PathBuf, VfsError> {
374 let resolved = self.resolve(path)?;
375 let target = std::fs::read_link(&resolved).map_err(|e| map_io_error(e, path))?;
376 if let Some(root) = &self.root
378 && target.is_absolute()
379 && let Ok(rel) = target.strip_prefix(root)
380 {
381 return Ok(PathBuf::from("/").join(rel));
382 }
383 Ok(target)
384 }
385
386 fn canonicalize(&self, path: &Path) -> Result<PathBuf, VfsError> {
387 let resolved = self.resolve(path)?;
388 let canonical = std::fs::canonicalize(&resolved).map_err(|e| map_io_error(e, path))?;
389 if let Some(root) = &self.root {
390 if !canonical.starts_with(root) {
391 return Err(VfsError::PermissionDenied(path.to_path_buf()));
392 }
393 let rel = canonical.strip_prefix(root).unwrap();
394 Ok(PathBuf::from("/").join(rel))
395 } else {
396 Ok(canonical)
397 }
398 }
399
400 fn copy(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
401 let resolved_src = self.resolve_follow(src)?;
402 let resolved_dst = self.resolve(dst)?;
403 std::fs::copy(&resolved_src, &resolved_dst).map_err(|e| {
404 if e.kind() == std::io::ErrorKind::NotFound {
405 map_io_error(e, src)
406 } else {
407 map_io_error(e, dst)
408 }
409 })?;
410 Ok(())
411 }
412
413 fn rename(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
414 let resolved_src = self.resolve(src)?;
415 let resolved_dst = self.resolve(dst)?;
416 std::fs::rename(&resolved_src, &resolved_dst).map_err(|e| {
417 if e.kind() == std::io::ErrorKind::NotFound {
418 map_io_error(e, src)
419 } else {
420 map_io_error(e, dst)
421 }
422 })
423 }
424
425 fn glob(&self, pattern: &str, cwd: &Path) -> Result<Vec<PathBuf>, VfsError> {
426 let is_absolute = pattern.starts_with('/');
427 let abs_pattern = if is_absolute {
428 pattern.to_string()
429 } else {
430 let cwd_str = cwd.to_str().unwrap_or("/").trim_end_matches('/');
431 format!("{cwd_str}/{pattern}")
432 };
433
434 let components: Vec<&str> = abs_pattern.split('/').filter(|s| !s.is_empty()).collect();
435
436 let real_root = self.resolve(Path::new("/"))?;
438
439 let mut results = Vec::new();
440 let max = 100_000;
441 self.glob_walk(
442 &real_root,
443 &components,
444 PathBuf::from("/"),
445 &mut results,
446 max,
447 );
448
449 results.sort();
450 results.dedup();
451
452 if !is_absolute {
453 results = results
454 .into_iter()
455 .filter_map(|p| p.strip_prefix(cwd).ok().map(|r| r.to_path_buf()))
456 .collect();
457 }
458
459 Ok(results)
460 }
461
462 fn deep_clone(&self) -> Arc<dyn VirtualFs> {
463 Arc::new(Self {
466 root: self.root.clone(),
467 })
468 }
469}
470
471fn logical_normalize(path: &Path) -> PathBuf {
477 let mut parts: Vec<&std::ffi::OsStr> = Vec::new();
478 for comp in path.components() {
479 match comp {
480 Component::RootDir | Component::Prefix(_) => {
481 parts.clear();
482 }
483 Component::CurDir => {}
484 Component::ParentDir => {
485 parts.pop();
486 }
487 Component::Normal(c) => parts.push(c),
488 }
489 }
490 let mut result = PathBuf::from("/");
491 for part in parts {
492 result.push(part);
493 }
494 result
495}
496
497fn canonicalize_existing(path: &Path, original: &Path, root: &Path) -> Result<PathBuf, VfsError> {
501 let mut existing = path.to_path_buf();
502 let mut tail: Vec<std::ffi::OsString> = Vec::new();
503 while !existing.exists() {
504 match existing.file_name() {
505 Some(name) => {
506 tail.push(name.to_owned());
507 existing.pop();
508 }
509 None => break,
510 }
511 }
512 let canonical = if existing.exists() {
513 std::fs::canonicalize(&existing).map_err(|e| map_io_error(e, original))?
514 } else {
515 existing
516 };
517
518 if !canonical.starts_with(root) {
520 return Err(VfsError::PermissionDenied(original.to_path_buf()));
521 }
522
523 let mut result = canonical;
524 for component in tail.into_iter().rev() {
525 result.push(component);
526 }
527 Ok(result)
528}
529
530fn map_io_error(err: std::io::Error, path: &Path) -> VfsError {
532 let p = path.to_path_buf();
533 match err.kind() {
534 std::io::ErrorKind::NotFound => VfsError::NotFound(p),
535 std::io::ErrorKind::AlreadyExists => VfsError::AlreadyExists(p),
536 std::io::ErrorKind::PermissionDenied => VfsError::PermissionDenied(p),
537 std::io::ErrorKind::DirectoryNotEmpty => VfsError::DirectoryNotEmpty(p),
538 std::io::ErrorKind::NotADirectory => VfsError::NotADirectory(p),
539 std::io::ErrorKind::IsADirectory => VfsError::IsADirectory(p),
540 _ => VfsError::IoError(err.to_string()),
541 }
542}
543
544fn map_metadata(meta: &std::fs::Metadata) -> Metadata {
546 let node_type = if meta.is_symlink() {
547 NodeType::Symlink
548 } else if meta.is_dir() {
549 NodeType::Directory
550 } else {
551 NodeType::File
552 };
553 Metadata {
554 node_type,
555 size: meta.len(),
556 mode: meta.permissions().mode(),
557 mtime: meta.modified().unwrap_or(SystemTime::UNIX_EPOCH),
558 }
559}