Skip to main content

rust_bash/vfs/
memory.rs

1use std::collections::BTreeMap;
2use std::path::{Component, Path, PathBuf};
3use std::sync::Arc;
4
5use crate::platform::SystemTime;
6
7use parking_lot::RwLock;
8
9use super::{DirEntry, FsNode, GlobOptions, Metadata, NodeType, VirtualFs};
10use crate::error::VfsError;
11
12const MAX_SYMLINK_DEPTH: u32 = 40;
13
14/// A fully in-memory filesystem implementation.
15///
16/// Thread-safe via `Arc<RwLock<...>>` — all `VirtualFs` methods take `&self`.
17/// Cloning is cheap (Arc increment) which is useful for subshell state cloning.
18#[derive(Debug, Clone)]
19pub struct InMemoryFs {
20    root: Arc<RwLock<FsNode>>,
21}
22
23impl Default for InMemoryFs {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl InMemoryFs {
30    pub fn new() -> Self {
31        Self {
32            root: Arc::new(RwLock::new(FsNode::Directory {
33                children: BTreeMap::new(),
34                mode: 0o755,
35                mtime: SystemTime::now(),
36            })),
37        }
38    }
39
40    /// Create a completely independent copy of this filesystem.
41    ///
42    /// Unlike `Clone` (which shares data via `Arc`), this recursively clones
43    /// the entire `FsNode` tree so the copy and original are fully independent.
44    /// Used for subshell isolation: `( cmds )`.
45    pub fn deep_clone(&self) -> Self {
46        let tree = self.root.read();
47        Self {
48            root: Arc::new(RwLock::new(tree.clone())),
49        }
50    }
51}
52
53// ---------------------------------------------------------------------------
54// Path utilities
55// ---------------------------------------------------------------------------
56
57/// Normalize an absolute path: resolve `.` and `..`, strip trailing slashes,
58/// reject empty paths.
59fn normalize(path: &Path) -> Result<PathBuf, VfsError> {
60    let s = path.to_str().unwrap_or("");
61    if s.is_empty() {
62        return Err(VfsError::InvalidPath("empty path".into()));
63    }
64    if !super::vfs_path_is_absolute(path) {
65        return Err(VfsError::InvalidPath(format!(
66            "path must be absolute: {}",
67            path.display()
68        )));
69    }
70
71    let mut parts: Vec<String> = Vec::new();
72    for comp in path.components() {
73        match comp {
74            Component::RootDir | Component::Prefix(_) => {}
75            Component::CurDir => {}
76            Component::ParentDir => {
77                parts.pop();
78            }
79            Component::Normal(seg) => {
80                if let Some(s) = seg.to_str() {
81                    parts.push(s.to_owned());
82                } else {
83                    return Err(VfsError::InvalidPath(format!(
84                        "non-UTF-8 component in: {}",
85                        path.display()
86                    )));
87                }
88            }
89        }
90    }
91
92    let mut result = PathBuf::from("/");
93    for p in &parts {
94        result.push(p);
95    }
96    Ok(result)
97}
98
99/// Split a normalized absolute path into its component names (excluding root).
100fn components(path: &Path) -> Vec<&str> {
101    path.components()
102        .filter_map(|c| match c {
103            Component::Normal(s) => s.to_str(),
104            _ => None,
105        })
106        .collect()
107}
108
109// ---------------------------------------------------------------------------
110// Internal node navigation helpers
111// ---------------------------------------------------------------------------
112
113impl InMemoryFs {
114    /// Read-lock the tree, navigate to a node (resolving symlinks), apply `f`.
115    fn with_node<F, T>(&self, path: &Path, f: F) -> Result<T, VfsError>
116    where
117        F: FnOnce(&FsNode) -> Result<T, VfsError>,
118    {
119        let norm = normalize(path)?;
120        let tree = self.root.read();
121        let node = navigate(&tree, &norm, true, MAX_SYMLINK_DEPTH, &tree)?;
122        f(node)
123    }
124
125    /// Read-lock the tree, navigate to a node **without** resolving the final
126    /// symlink component, apply `f`.
127    fn with_node_no_follow<F, T>(&self, path: &Path, f: F) -> Result<T, VfsError>
128    where
129        F: FnOnce(&FsNode) -> Result<T, VfsError>,
130    {
131        let norm = normalize(path)?;
132        let tree = self.root.read();
133        let node = navigate(&tree, &norm, false, MAX_SYMLINK_DEPTH, &tree)?;
134        f(node)
135    }
136
137    /// Write-lock, navigate to the **parent** of `path`, call `f(parent, child_name)`.
138    fn with_parent_mut<F, T>(&self, path: &Path, f: F) -> Result<T, VfsError>
139    where
140        F: FnOnce(&mut FsNode, &str) -> Result<T, VfsError>,
141    {
142        let norm = normalize(path)?;
143        let parts = components(&norm);
144        if parts.is_empty() {
145            return Err(VfsError::InvalidPath(
146                "cannot operate on root itself".into(),
147            ));
148        }
149        let child_name = parts.last().unwrap();
150        let parent_path: PathBuf = if parts.len() == 1 {
151            PathBuf::from("/")
152        } else {
153            let mut p = PathBuf::from("/");
154            for seg in &parts[..parts.len() - 1] {
155                p.push(seg);
156            }
157            p
158        };
159
160        let mut tree = self.root.write();
161        let parent = navigate_mut(&mut tree, &parent_path, true, MAX_SYMLINK_DEPTH)?;
162        f(parent, child_name)
163    }
164
165    /// Write-lock, navigate to a node (resolving symlinks), apply `f`.
166    fn with_node_mut<F, T>(&self, path: &Path, f: F) -> Result<T, VfsError>
167    where
168        F: FnOnce(&mut FsNode) -> Result<T, VfsError>,
169    {
170        let norm = normalize(path)?;
171        let mut tree = self.root.write();
172        let node = navigate_mut(&mut tree, &norm, true, MAX_SYMLINK_DEPTH)?;
173        f(node)
174    }
175
176    /// Resolve symlinks in a path, returning the canonical absolute path.
177    fn resolve_path(&self, path: &Path) -> Result<PathBuf, VfsError> {
178        let norm = normalize(path)?;
179        let tree = self.root.read();
180        resolve_canonical(&tree, &norm, MAX_SYMLINK_DEPTH, &tree)
181    }
182}
183
184// ---------------------------------------------------------------------------
185// Tree navigation (works on borrowed FsNode trees)
186// ---------------------------------------------------------------------------
187
188/// Navigate the tree from `root` to `path`, optionally following symlinks on
189/// the final component. Returns a reference to the target node.
190fn navigate<'a>(
191    root: &'a FsNode,
192    path: &Path,
193    follow_final: bool,
194    depth: u32,
195    tree_root: &'a FsNode,
196) -> Result<&'a FsNode, VfsError> {
197    if depth == 0 {
198        return Err(VfsError::SymlinkLoop(path.to_path_buf()));
199    }
200
201    let parts = components(path);
202    if parts.is_empty() {
203        return Ok(root);
204    }
205
206    let mut current = root;
207    for (i, name) in parts.iter().enumerate() {
208        let is_last = i == parts.len() - 1;
209        // Resolve current if it's a symlink (intermediate components always resolved)
210        current = resolve_if_symlink(current, path, depth, tree_root)?;
211
212        match current {
213            FsNode::Directory { children, .. } => {
214                let child = children
215                    .get(*name)
216                    .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))?;
217                if is_last && !follow_final {
218                    current = child;
219                } else {
220                    current = resolve_if_symlink(child, path, depth - 1, tree_root)?;
221                }
222            }
223            _ => return Err(VfsError::NotADirectory(path.to_path_buf())),
224        }
225    }
226    Ok(current)
227}
228
229/// Resolve a node if it is a symlink, following chains up to `depth`.
230fn resolve_if_symlink<'a>(
231    node: &'a FsNode,
232    original_path: &Path,
233    depth: u32,
234    tree_root: &'a FsNode,
235) -> Result<&'a FsNode, VfsError> {
236    if depth == 0 {
237        return Err(VfsError::SymlinkLoop(original_path.to_path_buf()));
238    }
239    match node {
240        FsNode::Symlink { target, .. } => {
241            let target_norm = normalize(target)?;
242            navigate(tree_root, &target_norm, true, depth - 1, tree_root)
243        }
244        other => Ok(other),
245    }
246}
247
248/// Mutable navigation. Symlinks on intermediate components are resolved by
249/// restarting from root (which requires dropping and re-borrowing).
250/// For simplicity, we first compute the canonical path, then navigate directly.
251fn navigate_mut<'a>(
252    root: &'a mut FsNode,
253    path: &Path,
254    follow_final: bool,
255    depth: u32,
256) -> Result<&'a mut FsNode, VfsError> {
257    if depth == 0 {
258        return Err(VfsError::SymlinkLoop(path.to_path_buf()));
259    }
260
261    let parts = components(path);
262    if parts.is_empty() {
263        return Ok(root);
264    }
265
266    // We need to handle symlinks during mutable traversal.
267    // Strategy: traverse step by step; if we hit a symlink, resolve it to get the
268    // canonical path of that prefix, then restart navigation from root with the
269    // resolved prefix + remaining components.
270    let canonical = resolve_canonical_from_root(root, path, follow_final, depth)?;
271    let canon_parts = components(&canonical);
272
273    let mut current = root as &mut FsNode;
274    for name in &canon_parts {
275        match current {
276            FsNode::Directory { children, .. } => {
277                current = children
278                    .get_mut(*name)
279                    .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))?;
280            }
281            _ => return Err(VfsError::NotADirectory(path.to_path_buf())),
282        }
283    }
284    Ok(current)
285}
286
287/// Resolve a path to its canonical form by walking the tree and resolving symlinks.
288fn resolve_canonical(
289    root: &FsNode,
290    path: &Path,
291    depth: u32,
292    tree_root: &FsNode,
293) -> Result<PathBuf, VfsError> {
294    if depth == 0 {
295        return Err(VfsError::SymlinkLoop(path.to_path_buf()));
296    }
297
298    let parts = components(path);
299    let mut resolved = PathBuf::from("/");
300    let mut current = root;
301
302    for name in &parts {
303        // current must be a directory (resolve symlinks)
304        current = resolve_if_symlink(current, path, depth, tree_root)?;
305        match current {
306            FsNode::Directory { children, .. } => {
307                let child = children
308                    .get(*name)
309                    .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))?;
310                match child {
311                    FsNode::Symlink { target, .. } => {
312                        let target_norm = normalize(target)?;
313                        // Resolve the symlink target recursively
314                        resolved =
315                            resolve_canonical(tree_root, &target_norm, depth - 1, tree_root)?;
316                        current = navigate(tree_root, &resolved, true, depth - 1, tree_root)?;
317                    }
318                    _ => {
319                        resolved.push(name);
320                        current = child;
321                    }
322                }
323            }
324            _ => return Err(VfsError::NotADirectory(path.to_path_buf())),
325        }
326    }
327    Ok(resolved)
328}
329
330/// Same as `resolve_canonical` but works from a mutable root reference
331/// (read-only traversal to compute the path).
332fn resolve_canonical_from_root(
333    root: &FsNode,
334    path: &Path,
335    follow_final: bool,
336    depth: u32,
337) -> Result<PathBuf, VfsError> {
338    if depth == 0 {
339        return Err(VfsError::SymlinkLoop(path.to_path_buf()));
340    }
341
342    let parts = components(path);
343    let mut resolved = PathBuf::from("/");
344    let mut current: &FsNode = root;
345
346    for (i, name) in parts.iter().enumerate() {
347        let is_last = i == parts.len() - 1;
348        // current must be a directory (resolve symlinks)
349        current = resolve_if_symlink_from_root(current, path, depth, root)?;
350        match current {
351            FsNode::Directory { children, .. } => {
352                let child = children
353                    .get(*name)
354                    .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))?;
355                if is_last && !follow_final {
356                    resolved.push(name);
357                    break;
358                }
359                match child {
360                    FsNode::Symlink { target, .. } => {
361                        let target_norm = normalize(target)?;
362                        resolved =
363                            resolve_canonical_from_root(root, &target_norm, true, depth - 1)?;
364                        current = navigate_readonly(root, &resolved, true, depth - 1, root)?;
365                    }
366                    _ => {
367                        resolved.push(name);
368                        current = child;
369                    }
370                }
371            }
372            _ => return Err(VfsError::NotADirectory(path.to_path_buf())),
373        }
374    }
375    Ok(resolved)
376}
377
378fn resolve_if_symlink_from_root<'a>(
379    node: &'a FsNode,
380    original_path: &Path,
381    depth: u32,
382    root: &'a FsNode,
383) -> Result<&'a FsNode, VfsError> {
384    if depth == 0 {
385        return Err(VfsError::SymlinkLoop(original_path.to_path_buf()));
386    }
387    match node {
388        FsNode::Symlink { target, .. } => {
389            let target_norm = normalize(target)?;
390            navigate_readonly(root, &target_norm, true, depth - 1, root)
391        }
392        other => Ok(other),
393    }
394}
395
396fn navigate_readonly<'a>(
397    root: &'a FsNode,
398    path: &Path,
399    follow_final: bool,
400    depth: u32,
401    tree_root: &'a FsNode,
402) -> Result<&'a FsNode, VfsError> {
403    navigate(root, path, follow_final, depth, tree_root)
404}
405
406/// Navigate a mutable tree by component names (no symlink resolution).
407/// Returns `None` if any component is missing or not a directory.
408fn navigate_to_mut<'a>(node: &'a mut FsNode, parts: &[&str]) -> Option<&'a mut FsNode> {
409    let mut current = node;
410    for name in parts {
411        match current {
412            FsNode::Directory { children, .. } => {
413                current = children.get_mut(*name)?;
414            }
415            _ => return None,
416        }
417    }
418    Some(current)
419}
420
421// ---------------------------------------------------------------------------
422// VirtualFs implementation
423// ---------------------------------------------------------------------------
424
425impl VirtualFs for InMemoryFs {
426    fn read_file(&self, path: &Path) -> Result<Vec<u8>, VfsError> {
427        self.with_node(path, |node| match node {
428            FsNode::File { content, .. } => Ok(content.clone()),
429            FsNode::Directory { .. } => Err(VfsError::IsADirectory(path.to_path_buf())),
430            FsNode::Symlink { .. } => Err(VfsError::IoError(
431                "unexpected symlink after resolution".into(),
432            )),
433        })
434    }
435
436    fn write_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError> {
437        let norm = normalize(path)?;
438
439        // Try to overwrite an existing file first
440        {
441            let mut tree = self.root.write();
442            let canon_result = resolve_canonical_from_root(&tree, &norm, true, MAX_SYMLINK_DEPTH);
443            if let Ok(canon) = canon_result {
444                let canon_parts = components(&canon);
445                let node = navigate_to_mut(&mut tree, &canon_parts);
446                if let Some(node) = node {
447                    match node {
448                        FsNode::File {
449                            content: c,
450                            mtime: m,
451                            ..
452                        } => {
453                            *c = content.to_vec();
454                            *m = SystemTime::now();
455                            return Ok(());
456                        }
457                        FsNode::Directory { .. } => {
458                            return Err(VfsError::IsADirectory(path.to_path_buf()));
459                        }
460                        FsNode::Symlink { .. } => {}
461                    }
462                }
463            }
464        }
465
466        // File doesn't exist — create it in the parent directory
467        self.with_parent_mut(path, |parent, child_name| match parent {
468            FsNode::Directory { children, .. } => {
469                children.insert(
470                    child_name.to_string(),
471                    FsNode::File {
472                        content: content.to_vec(),
473                        mode: 0o644,
474                        mtime: SystemTime::now(),
475                    },
476                );
477                Ok(())
478            }
479            _ => Err(VfsError::NotADirectory(path.to_path_buf())),
480        })
481    }
482
483    fn append_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError> {
484        self.with_node_mut(path, |node| match node {
485            FsNode::File {
486                content: c,
487                mtime: m,
488                ..
489            } => {
490                c.extend_from_slice(content);
491                *m = SystemTime::now();
492                Ok(())
493            }
494            FsNode::Directory { .. } => Err(VfsError::IsADirectory(path.to_path_buf())),
495            FsNode::Symlink { .. } => Err(VfsError::IoError(
496                "unexpected symlink after resolution".into(),
497            )),
498        })
499    }
500
501    fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
502        // Resolve the path to find the actual location of the file
503        let norm = normalize(path)?;
504
505        // Check if the final component is a symlink — remove_file should remove the link, not the target
506        self.with_parent_mut(path, |parent, child_name| match parent {
507            FsNode::Directory { children, .. } => match children.get(child_name) {
508                Some(FsNode::File { .. } | FsNode::Symlink { .. }) => {
509                    children.remove(child_name);
510                    Ok(())
511                }
512                Some(FsNode::Directory { .. }) => Err(VfsError::IsADirectory(norm.clone())),
513                None => Err(VfsError::NotFound(norm.clone())),
514            },
515            _ => Err(VfsError::NotADirectory(norm.clone())),
516        })
517    }
518
519    fn mkdir(&self, path: &Path) -> Result<(), VfsError> {
520        self.with_parent_mut(path, |parent, child_name| match parent {
521            FsNode::Directory { children, .. } => {
522                if children.contains_key(child_name) {
523                    return Err(VfsError::AlreadyExists(path.to_path_buf()));
524                }
525                children.insert(
526                    child_name.to_string(),
527                    FsNode::Directory {
528                        children: BTreeMap::new(),
529                        mode: 0o755,
530                        mtime: SystemTime::now(),
531                    },
532                );
533                Ok(())
534            }
535            _ => Err(VfsError::NotADirectory(path.to_path_buf())),
536        })
537    }
538
539    fn mkdir_p(&self, path: &Path) -> Result<(), VfsError> {
540        let norm = normalize(path)?;
541        let parts = components(&norm);
542        if parts.is_empty() {
543            return Ok(()); // root already exists
544        }
545
546        let mut tree = self.root.write();
547        let mut current: &mut FsNode = &mut tree;
548        for name in &parts {
549            match current {
550                FsNode::Directory { children, .. } => {
551                    current =
552                        children
553                            .entry((*name).to_string())
554                            .or_insert_with(|| FsNode::Directory {
555                                children: BTreeMap::new(),
556                                mode: 0o755,
557                                mtime: SystemTime::now(),
558                            });
559                    // If it already exists as a dir, that's fine. If it's a file, error.
560                    match current {
561                        FsNode::Directory { .. } => {}
562                        FsNode::File { .. } => {
563                            return Err(VfsError::NotADirectory(path.to_path_buf()));
564                        }
565                        FsNode::Symlink { .. } => {
566                            // For simplicity, don't follow symlinks in mkdir_p path creation
567                            return Err(VfsError::NotADirectory(path.to_path_buf()));
568                        }
569                    }
570                }
571                _ => return Err(VfsError::NotADirectory(path.to_path_buf())),
572            }
573        }
574        Ok(())
575    }
576
577    fn readdir(&self, path: &Path) -> Result<Vec<DirEntry>, VfsError> {
578        self.with_node(path, |node| match node {
579            FsNode::Directory { children, .. } => {
580                let entries = children
581                    .iter()
582                    .map(|(name, child)| DirEntry {
583                        name: name.clone(),
584                        node_type: match child {
585                            FsNode::File { .. } => NodeType::File,
586                            FsNode::Directory { .. } => NodeType::Directory,
587                            FsNode::Symlink { .. } => NodeType::Symlink,
588                        },
589                    })
590                    .collect();
591                Ok(entries)
592            }
593            _ => Err(VfsError::NotADirectory(path.to_path_buf())),
594        })
595    }
596
597    fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
598        self.with_parent_mut(path, |parent, child_name| match parent {
599            FsNode::Directory { children, .. } => match children.get(child_name) {
600                Some(FsNode::Directory { children: ch, .. }) => {
601                    if ch.is_empty() {
602                        children.remove(child_name);
603                        Ok(())
604                    } else {
605                        Err(VfsError::DirectoryNotEmpty(path.to_path_buf()))
606                    }
607                }
608                Some(FsNode::File { .. }) => Err(VfsError::NotADirectory(path.to_path_buf())),
609                Some(FsNode::Symlink { .. }) => Err(VfsError::NotADirectory(path.to_path_buf())),
610                None => Err(VfsError::NotFound(path.to_path_buf())),
611            },
612            _ => Err(VfsError::NotADirectory(path.to_path_buf())),
613        })
614    }
615
616    fn remove_dir_all(&self, path: &Path) -> Result<(), VfsError> {
617        self.with_parent_mut(path, |parent, child_name| match parent {
618            FsNode::Directory { children, .. } => match children.get(child_name) {
619                Some(FsNode::Directory { .. }) => {
620                    children.remove(child_name);
621                    Ok(())
622                }
623                Some(FsNode::File { .. }) => Err(VfsError::NotADirectory(path.to_path_buf())),
624                Some(FsNode::Symlink { .. }) => Err(VfsError::NotADirectory(path.to_path_buf())),
625                None => Err(VfsError::NotFound(path.to_path_buf())),
626            },
627            _ => Err(VfsError::NotADirectory(path.to_path_buf())),
628        })
629    }
630
631    fn exists(&self, path: &Path) -> bool {
632        let norm = match normalize(path) {
633            Ok(p) => p,
634            Err(_) => return false,
635        };
636        let tree = self.root.read();
637        navigate(&tree, &norm, true, MAX_SYMLINK_DEPTH, &tree).is_ok()
638    }
639
640    fn stat(&self, path: &Path) -> Result<Metadata, VfsError> {
641        self.with_node(path, |node| Ok(node_metadata(node)))
642    }
643
644    fn lstat(&self, path: &Path) -> Result<Metadata, VfsError> {
645        self.with_node_no_follow(path, |node| Ok(node_metadata(node)))
646    }
647
648    fn chmod(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
649        self.with_node_mut(path, |node| {
650            match node {
651                FsNode::File { mode: m, .. } | FsNode::Directory { mode: m, .. } => {
652                    *m = mode;
653                }
654                FsNode::Symlink { .. } => {
655                    // chmod on a symlink (after resolution) shouldn't hit this
656                    return Err(VfsError::IoError("cannot chmod a symlink directly".into()));
657                }
658            }
659            Ok(())
660        })
661    }
662
663    fn utimes(&self, path: &Path, mtime: SystemTime) -> Result<(), VfsError> {
664        self.with_node_mut(path, |node| {
665            match node {
666                FsNode::File { mtime: m, .. }
667                | FsNode::Directory { mtime: m, .. }
668                | FsNode::Symlink { mtime: m, .. } => {
669                    *m = mtime;
670                }
671            }
672            Ok(())
673        })
674    }
675
676    fn symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError> {
677        self.with_parent_mut(link, |parent, child_name| match parent {
678            FsNode::Directory { children, .. } => {
679                if children.contains_key(child_name) {
680                    return Err(VfsError::AlreadyExists(link.to_path_buf()));
681                }
682                children.insert(
683                    child_name.to_string(),
684                    FsNode::Symlink {
685                        target: target.to_path_buf(),
686                        mtime: SystemTime::now(),
687                    },
688                );
689                Ok(())
690            }
691            _ => Err(VfsError::NotADirectory(link.to_path_buf())),
692        })
693    }
694
695    fn hardlink(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
696        // Hard links in an in-memory FS: clone the node data.
697        let content = self.read_file(src)?;
698        let meta = self.stat(src)?;
699        self.with_parent_mut(dst, |parent, child_name| match parent {
700            FsNode::Directory { children, .. } => {
701                if children.contains_key(child_name) {
702                    return Err(VfsError::AlreadyExists(dst.to_path_buf()));
703                }
704                children.insert(
705                    child_name.to_string(),
706                    FsNode::File {
707                        content: content.clone(),
708                        mode: meta.mode,
709                        mtime: meta.mtime,
710                    },
711                );
712                Ok(())
713            }
714            _ => Err(VfsError::NotADirectory(dst.to_path_buf())),
715        })
716    }
717
718    fn readlink(&self, path: &Path) -> Result<PathBuf, VfsError> {
719        self.with_node_no_follow(path, |node| match node {
720            FsNode::Symlink { target, .. } => Ok(target.clone()),
721            _ => Err(VfsError::InvalidPath(format!(
722                "not a symlink: {}",
723                path.display()
724            ))),
725        })
726    }
727
728    fn canonicalize(&self, path: &Path) -> Result<PathBuf, VfsError> {
729        self.resolve_path(path)
730    }
731
732    fn copy(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
733        let content = self.read_file(src)?;
734        let meta = self.stat(src)?;
735        self.write_file(dst, &content)?;
736        self.chmod(dst, meta.mode)?;
737        Ok(())
738    }
739
740    fn rename(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
741        let src_norm = normalize(src)?;
742        let dst_norm = normalize(dst)?;
743
744        let src_parts = components(&src_norm);
745        let dst_parts = components(&dst_norm);
746        if src_parts.is_empty() || dst_parts.is_empty() {
747            return Err(VfsError::InvalidPath("cannot rename root".into()));
748        }
749
750        let mut tree = self.root.write();
751
752        // Extract the source node from the tree
753        let node = {
754            let src_parent_parts = &src_parts[..src_parts.len() - 1];
755            let src_child = src_parts.last().unwrap();
756
757            let mut parent: &mut FsNode = &mut tree;
758            for name in src_parent_parts {
759                match parent {
760                    FsNode::Directory { children, .. } => {
761                        parent = children
762                            .get_mut(*name)
763                            .ok_or_else(|| VfsError::NotFound(src.to_path_buf()))?;
764                    }
765                    _ => return Err(VfsError::NotADirectory(src.to_path_buf())),
766                }
767            }
768            match parent {
769                FsNode::Directory { children, .. } => children
770                    .remove(*src_child)
771                    .ok_or_else(|| VfsError::NotFound(src.to_path_buf()))?,
772                _ => return Err(VfsError::NotADirectory(src.to_path_buf())),
773            }
774        };
775
776        // Insert at destination
777        {
778            let dst_parent_parts = &dst_parts[..dst_parts.len() - 1];
779            let dst_child = dst_parts.last().unwrap();
780
781            let mut parent: &mut FsNode = &mut tree;
782            for name in dst_parent_parts {
783                match parent {
784                    FsNode::Directory { children, .. } => {
785                        parent = children
786                            .get_mut(*name)
787                            .ok_or_else(|| VfsError::NotFound(dst.to_path_buf()))?;
788                    }
789                    _ => return Err(VfsError::NotADirectory(dst.to_path_buf())),
790                }
791            }
792            match parent {
793                FsNode::Directory { children, .. } => {
794                    children.insert((*dst_child).to_string(), node);
795                }
796                _ => return Err(VfsError::NotADirectory(dst.to_path_buf())),
797            }
798        }
799
800        Ok(())
801    }
802
803    fn glob(&self, pattern: &str, cwd: &Path) -> Result<Vec<PathBuf>, VfsError> {
804        // VFS-level glob always supports ** recursion; the shell expansion layer
805        // controls whether ** is sent as a pattern based on `shopt -s globstar`.
806        self.glob_with_opts(
807            pattern,
808            cwd,
809            &GlobOptions {
810                globstar: true,
811                ..GlobOptions::default()
812            },
813        )
814    }
815
816    fn glob_with_opts(
817        &self,
818        pattern: &str,
819        cwd: &Path,
820        opts: &GlobOptions,
821    ) -> Result<Vec<PathBuf>, VfsError> {
822        let is_absolute = pattern.starts_with('/');
823        let abs_pattern = if is_absolute {
824            pattern.to_string()
825        } else {
826            let cwd_str = cwd.to_str().unwrap_or("/").trim_end_matches('/');
827            format!("{cwd_str}/{pattern}")
828        };
829
830        let components: Vec<&str> = abs_pattern.split('/').filter(|s| !s.is_empty()).collect();
831        let tree = self.root.read();
832        let mut results = Vec::new();
833        let max = 100_000;
834        glob_collect(
835            &tree,
836            &components,
837            PathBuf::from("/"),
838            &mut results,
839            &tree,
840            max,
841            opts,
842        );
843        results.sort();
844        results.dedup();
845
846        if !is_absolute {
847            results = results
848                .into_iter()
849                .filter_map(|p| p.strip_prefix(cwd).ok().map(|r| r.to_path_buf()))
850                .collect();
851        }
852
853        Ok(results)
854    }
855
856    fn deep_clone(&self) -> Arc<dyn VirtualFs> {
857        Arc::new(InMemoryFs::deep_clone(self))
858    }
859}
860
861// ---------------------------------------------------------------------------
862// Glob tree walk
863// ---------------------------------------------------------------------------
864
865use crate::interpreter::pattern::{
866    extglob_match, extglob_match_nocase, glob_match, glob_match_nocase,
867};
868
869/// Recursively collect filesystem paths matching a glob pattern.
870///
871/// `node` is the current tree position, `components` the remaining pattern
872/// segments, and `current_path` the path assembled so far. `tree_root` is
873/// used for resolving symlinks, and `max` caps the result count.
874fn glob_collect(
875    node: &FsNode,
876    components: &[&str],
877    current_path: PathBuf,
878    results: &mut Vec<PathBuf>,
879    tree_root: &FsNode,
880    max: usize,
881    opts: &GlobOptions,
882) {
883    if results.len() >= max {
884        return;
885    }
886
887    if components.is_empty() {
888        results.push(current_path);
889        return;
890    }
891
892    // Resolve symlinks so we can see through them to the target directory.
893    let resolved =
894        resolve_if_symlink(node, &current_path, MAX_SYMLINK_DEPTH, tree_root).unwrap_or(node);
895
896    let pattern = components[0];
897    let rest = &components[1..];
898
899    if pattern == "**" && opts.globstar {
900        // Zero directories — advance past **
901        glob_collect(
902            resolved,
903            rest,
904            current_path.clone(),
905            results,
906            tree_root,
907            max,
908            opts,
909        );
910
911        // One or more directories — recurse into children
912        if let FsNode::Directory { children, .. } = resolved {
913            for (name, child) in children {
914                if results.len() >= max {
915                    return;
916                }
917                if name.starts_with('.') && !opts.dotglob {
918                    continue;
919                }
920                let child_path = current_path.join(name);
921                glob_collect(child, components, child_path, results, tree_root, max, opts);
922            }
923        }
924    } else {
925        // When globstar is off, treat ** as *
926        let effective_pattern = if pattern == "**" { "*" } else { pattern };
927
928        if let FsNode::Directory { children, .. } = resolved {
929            for (name, child) in children {
930                if results.len() >= max {
931                    return;
932                }
933                // Skip hidden files unless dotglob is on or pattern explicitly starts with '.'
934                if name.starts_with('.') && !effective_pattern.starts_with('.') && !opts.dotglob {
935                    continue;
936                }
937                let matched = if opts.extglob && opts.nocaseglob {
938                    extglob_match_nocase(effective_pattern, name)
939                } else if opts.extglob {
940                    extglob_match(effective_pattern, name)
941                } else if opts.nocaseglob {
942                    glob_match_nocase(effective_pattern, name)
943                } else {
944                    glob_match(effective_pattern, name)
945                };
946                if matched {
947                    let child_path = current_path.join(name);
948                    if rest.is_empty() {
949                        results.push(child_path);
950                    } else {
951                        glob_collect(child, rest, child_path, results, tree_root, max, opts);
952                    }
953                }
954            }
955        }
956    }
957}
958
959/// Extract metadata from a node.
960fn node_metadata(node: &FsNode) -> Metadata {
961    match node {
962        FsNode::File {
963            content,
964            mode,
965            mtime,
966            ..
967        } => Metadata {
968            node_type: NodeType::File,
969            size: content.len() as u64,
970            mode: *mode,
971            mtime: *mtime,
972        },
973        FsNode::Directory { mode, mtime, .. } => Metadata {
974            node_type: NodeType::Directory,
975            size: 0,
976            mode: *mode,
977            mtime: *mtime,
978        },
979        FsNode::Symlink { target, mtime, .. } => Metadata {
980            node_type: NodeType::Symlink,
981            size: target.to_string_lossy().len() as u64,
982            mode: 0o777,
983            mtime: *mtime,
984        },
985    }
986}