Skip to main content

rust_bash/vfs/
overlay.rs

1//! OverlayFs — copy-on-write filesystem backed by a real directory (lower)
2//! and an in-memory write layer (upper).
3//!
4//! Reads resolve through: whiteouts → upper → lower.
5//! Writes always go to the upper layer. The lower directory is never modified.
6//! Deletions insert a "whiteout" entry so the file appears removed even though
7//! it still exists on disk.
8
9use std::collections::HashSet;
10use std::os::unix::fs::PermissionsExt;
11use std::path::{Component, Path, PathBuf};
12use std::sync::Arc;
13
14use crate::platform::SystemTime;
15
16use parking_lot::RwLock;
17
18use super::{DirEntry, InMemoryFs, Metadata, NodeType, VirtualFs};
19use crate::error::VfsError;
20use crate::interpreter::pattern::glob_match;
21
22const MAX_SYMLINK_DEPTH: u32 = 40;
23
24/// A copy-on-write filesystem: reads from a real directory, writes to memory.
25///
26/// The lower layer (a real directory on disk) is treated as read-only.
27/// All mutations go to the upper `InMemoryFs` layer. Deletions are tracked
28/// via a whiteout set so deleted lower-layer entries appear as removed.
29///
30/// # Example
31///
32/// ```ignore
33/// use rust_bash::{RustBashBuilder, OverlayFs};
34/// use std::sync::Arc;
35///
36/// let overlay = OverlayFs::new("./my_project").unwrap();
37/// let mut shell = RustBashBuilder::new()
38///     .fs(Arc::new(overlay))
39///     .cwd("/")
40///     .build()
41///     .unwrap();
42///
43/// let result = shell.exec("cat /src/main.rs").unwrap(); // reads from disk
44/// shell.exec("echo new > /src/main.rs").unwrap();       // writes to memory only
45/// ```
46pub struct OverlayFs {
47    lower: PathBuf,
48    upper: InMemoryFs,
49    whiteouts: Arc<RwLock<HashSet<PathBuf>>>,
50}
51
52/// Where a path resolved to during layer lookup.
53enum LayerResult {
54    Whiteout,
55    Upper,
56    Lower,
57    NotFound,
58}
59
60impl OverlayFs {
61    /// Create an overlay filesystem with `lower` as the read-only base.
62    ///
63    /// The lower directory must exist and be a directory. It is canonicalized
64    /// on construction so symlinks in the lower path itself are resolved once.
65    pub fn new(lower: impl Into<PathBuf>) -> std::io::Result<Self> {
66        let lower = lower.into();
67        if !lower.is_dir() {
68            return Err(std::io::Error::new(
69                std::io::ErrorKind::NotADirectory,
70                format!("{} is not a directory", lower.display()),
71            ));
72        }
73        let lower = lower.canonicalize()?;
74        Ok(Self {
75            lower,
76            upper: InMemoryFs::new(),
77            whiteouts: Arc::new(RwLock::new(HashSet::new())),
78        })
79    }
80
81    // ------------------------------------------------------------------
82    // Whiteout helpers
83    // ------------------------------------------------------------------
84
85    /// Check if `path` or any ancestor is whiteout-ed.
86    fn is_whiteout(&self, path: &Path) -> bool {
87        let whiteouts = self.whiteouts.read();
88        let mut current = path.to_path_buf();
89        loop {
90            if whiteouts.contains(&current) {
91                return true;
92            }
93            if !current.pop() {
94                return false;
95            }
96        }
97    }
98
99    /// Insert a whiteout for `path`.
100    fn add_whiteout(&self, path: &Path) {
101        self.whiteouts.write().insert(path.to_path_buf());
102    }
103
104    /// Remove a whiteout for exactly `path` (not ancestors).
105    fn remove_whiteout(&self, path: &Path) {
106        self.whiteouts.write().remove(path);
107    }
108
109    // ------------------------------------------------------------------
110    // Layer entry checks (no symlink following)
111    // ------------------------------------------------------------------
112
113    /// Check if a node exists in the upper layer at `path` without
114    /// following symlinks. This is critical because `InMemoryFs::exists`
115    /// follows symlinks — a symlink whose target is only in lower would
116    /// return false.
117    fn upper_has_entry(&self, path: &Path) -> bool {
118        self.upper.lstat(path).is_ok()
119    }
120
121    // ------------------------------------------------------------------
122    // Layer resolution
123    // ------------------------------------------------------------------
124
125    /// Determine which layer `path` lives in (after normalization).
126    fn resolve_layer(&self, path: &Path) -> LayerResult {
127        if self.is_whiteout(path) {
128            return LayerResult::Whiteout;
129        }
130        if self.upper_has_entry(path) {
131            return LayerResult::Upper;
132        }
133        if self.lower_exists(path) {
134            return LayerResult::Lower;
135        }
136        LayerResult::NotFound
137    }
138
139    // ------------------------------------------------------------------
140    // Lower-layer reading helpers (3j)
141    // ------------------------------------------------------------------
142
143    /// Map a VFS absolute path to the corresponding real path under `lower`.
144    fn lower_path(&self, vfs_path: &Path) -> PathBuf {
145        let rel = vfs_path.strip_prefix("/").unwrap_or(vfs_path.as_ref());
146        self.lower.join(rel)
147    }
148
149    /// Read a file from the lower layer.
150    fn read_lower_file(&self, path: &Path) -> Result<Vec<u8>, VfsError> {
151        let real = self.lower_path(path);
152        std::fs::read(&real).map_err(|e| map_io_error(e, path))
153    }
154
155    /// Get metadata for a path in the lower layer (follows symlinks).
156    fn stat_lower(&self, path: &Path) -> Result<Metadata, VfsError> {
157        let real = self.lower_path(path);
158        let meta = std::fs::metadata(&real).map_err(|e| map_io_error(e, path))?;
159        Ok(map_std_metadata(&meta))
160    }
161
162    /// Get metadata for a path in the lower layer (does NOT follow symlinks).
163    fn lstat_lower(&self, path: &Path) -> Result<Metadata, VfsError> {
164        let real = self.lower_path(path);
165        let meta = std::fs::symlink_metadata(&real).map_err(|e| map_io_error(e, path))?;
166        Ok(map_std_metadata(&meta))
167    }
168
169    /// List entries in a lower-layer directory.
170    fn readdir_lower(&self, path: &Path) -> Result<Vec<DirEntry>, VfsError> {
171        let real = self.lower_path(path);
172        let entries = std::fs::read_dir(&real).map_err(|e| map_io_error(e, path))?;
173        let mut result = Vec::new();
174        for entry in entries {
175            let entry = entry.map_err(|e| map_io_error(e, path))?;
176            let ft = entry.file_type().map_err(|e| map_io_error(e, path))?;
177            let node_type = if ft.is_dir() {
178                NodeType::Directory
179            } else if ft.is_symlink() {
180                NodeType::Symlink
181            } else {
182                NodeType::File
183            };
184            result.push(DirEntry {
185                name: entry.file_name().to_string_lossy().into_owned(),
186                node_type,
187            });
188        }
189        Ok(result)
190    }
191
192    /// Read a symlink target from the lower layer.
193    fn readlink_lower(&self, path: &Path) -> Result<PathBuf, VfsError> {
194        let real = self.lower_path(path);
195        std::fs::read_link(&real).map_err(|e| map_io_error(e, path))
196    }
197
198    /// Check whether a path exists in the lower layer (symlink_metadata).
199    fn lower_exists(&self, path: &Path) -> bool {
200        let real = self.lower_path(path);
201        real.symlink_metadata().is_ok()
202    }
203
204    // ------------------------------------------------------------------
205    // Copy-up helpers (3c)
206    // ------------------------------------------------------------------
207
208    /// Ensure a file is present in the upper layer. If it only exists in the
209    /// lower layer, copy its content and metadata up.
210    fn copy_up_if_needed(&self, path: &Path) -> Result<(), VfsError> {
211        if self.upper_has_entry(path) {
212            return Ok(());
213        }
214        debug_assert!(
215            !self.is_whiteout(path),
216            "copy_up_if_needed called on whiteout-ed path"
217        );
218        // Ensure the parent directory exists in upper
219        if let Some(parent) = path.parent()
220            && parent != Path::new("/")
221        {
222            self.ensure_upper_dir_path(parent)?;
223        }
224        let content = self.read_lower_file(path)?;
225        let meta = self.stat_lower(path)?;
226        self.upper.write_file(path, &content)?;
227        self.upper.chmod(path, meta.mode)?;
228        self.upper.utimes(path, meta.mtime)?;
229        Ok(())
230    }
231
232    /// Ensure all components of `path` exist as directories in the upper layer,
233    /// creating them if they only exist in the lower layer. Also clears any
234    /// whiteouts on each component so previously-deleted paths become visible
235    /// again.
236    fn ensure_upper_dir_path(&self, path: &Path) -> Result<(), VfsError> {
237        let norm = normalize(path)?;
238        let parts = path_components(&norm);
239        let mut built = PathBuf::from("/");
240        for name in parts {
241            built.push(name);
242            self.remove_whiteout(&built);
243            if self.upper_has_entry(&built) {
244                continue;
245            }
246            // Try to pick up metadata from lower; fallback to defaults
247            let mode = if let Ok(m) = self.stat_lower(&built) {
248                m.mode
249            } else {
250                0o755
251            };
252            self.upper.mkdir_p(&built)?;
253            self.upper.chmod(&built, mode)?;
254        }
255        Ok(())
256    }
257
258    // ------------------------------------------------------------------
259    // Recursive whiteout for remove_dir_all (3d)
260    // ------------------------------------------------------------------
261
262    /// Collect all visible paths under `dir` from both layers, then whiteout them.
263    fn whiteout_recursive(&self, dir: &Path) -> Result<(), VfsError> {
264        // Gather all visible children (merged from upper + lower, minus whiteouts)
265        let entries = self.readdir_merged(dir)?;
266        for entry in &entries {
267            let child = dir.join(&entry.name);
268            if entry.node_type == NodeType::Directory {
269                self.whiteout_recursive(&child)?;
270            }
271            self.add_whiteout(&child);
272        }
273        Ok(())
274    }
275
276    // ------------------------------------------------------------------
277    // Merged readdir helper (3e)
278    // ------------------------------------------------------------------
279
280    /// Merge directory listings from upper and lower, excluding whiteouts.
281    fn readdir_merged(&self, path: &Path) -> Result<Vec<DirEntry>, VfsError> {
282        let mut entries: std::collections::BTreeMap<String, DirEntry> =
283            std::collections::BTreeMap::new();
284
285        // Lower entries first
286        if self.lower_exists(path)
287            && let Ok(lower_entries) = self.readdir_lower(path)
288        {
289            for e in lower_entries {
290                let child_path = path.join(&e.name);
291                if !self.is_whiteout(&child_path) {
292                    entries.insert(e.name.clone(), e);
293                }
294            }
295        }
296
297        // Upper entries override lower entries (dedup by name)
298        if self.upper_has_entry(path)
299            && let Ok(upper_entries) = self.upper.readdir(path)
300        {
301            for e in upper_entries {
302                entries.insert(e.name.clone(), e);
303            }
304        }
305
306        Ok(entries.into_values().collect())
307    }
308
309    // ------------------------------------------------------------------
310    // Canonicalize helper (3g)
311    // ------------------------------------------------------------------
312
313    /// Step-by-step path resolution through both layers with symlink following.
314    fn resolve_path(&self, path: &Path, follow_final: bool) -> Result<PathBuf, VfsError> {
315        self.resolve_path_depth(path, follow_final, MAX_SYMLINK_DEPTH)
316    }
317
318    fn resolve_path_depth(
319        &self,
320        path: &Path,
321        follow_final: bool,
322        depth: u32,
323    ) -> Result<PathBuf, VfsError> {
324        if depth == 0 {
325            return Err(VfsError::SymlinkLoop(path.to_path_buf()));
326        }
327
328        let norm = normalize(path)?;
329        let parts = path_components(&norm);
330        let mut resolved = PathBuf::from("/");
331
332        for (i, name) in parts.iter().enumerate() {
333            let is_last = i == parts.len() - 1;
334            let candidate = resolved.join(name);
335
336            if self.is_whiteout(&candidate) {
337                return Err(VfsError::NotFound(path.to_path_buf()));
338            }
339
340            // Check if this component is a symlink (upper takes precedence)
341            let is_symlink_in_upper = self
342                .upper
343                .lstat(&candidate)
344                .is_ok_and(|m| m.node_type == NodeType::Symlink);
345            let is_symlink_in_lower = !is_symlink_in_upper
346                && self
347                    .lstat_lower(&candidate)
348                    .is_ok_and(|m| m.node_type == NodeType::Symlink);
349
350            if is_symlink_in_upper || is_symlink_in_lower {
351                if is_last && !follow_final {
352                    resolved = candidate;
353                } else {
354                    // Read the symlink target
355                    let target = if is_symlink_in_upper {
356                        self.upper.readlink(&candidate)?
357                    } else {
358                        self.readlink_lower(&candidate)?
359                    };
360                    // Resolve target (absolute or relative to parent)
361                    let abs_target = if target.is_absolute() {
362                        target
363                    } else {
364                        resolved.join(&target)
365                    };
366                    resolved = self.resolve_path_depth(&abs_target, true, depth - 1)?;
367                }
368            } else {
369                resolved = candidate;
370            }
371        }
372        Ok(resolved)
373    }
374
375    // ------------------------------------------------------------------
376    // Glob helpers (3h)
377    // ------------------------------------------------------------------
378
379    /// Walk directories in both layers for glob matching.
380    fn glob_walk(
381        &self,
382        dir: &Path,
383        components: &[&str],
384        current_path: PathBuf,
385        results: &mut Vec<PathBuf>,
386        max: usize,
387    ) {
388        if results.len() >= max || components.is_empty() {
389            if components.is_empty() {
390                results.push(current_path);
391            }
392            return;
393        }
394
395        let pattern = components[0];
396        let rest = &components[1..];
397
398        if pattern == "**" {
399            // Zero directories — advance past **
400            self.glob_walk(dir, rest, current_path.clone(), results, max);
401
402            // One or more directories — recurse
403            if let Ok(entries) = self.readdir_merged(dir) {
404                for entry in entries {
405                    if results.len() >= max {
406                        return;
407                    }
408                    if entry.name.starts_with('.') {
409                        continue;
410                    }
411                    let child_path = current_path.join(&entry.name);
412                    let child_dir = dir.join(&entry.name);
413                    if entry.node_type == NodeType::Directory
414                        || entry.node_type == NodeType::Symlink
415                    {
416                        self.glob_walk(&child_dir, components, child_path, results, max);
417                    }
418                }
419            }
420        } else if let Ok(entries) = self.readdir_merged(dir) {
421            for entry in entries {
422                if results.len() >= max {
423                    return;
424                }
425                if entry.name.starts_with('.') && !pattern.starts_with('.') {
426                    continue;
427                }
428                if glob_match(pattern, &entry.name) {
429                    let child_path = current_path.join(&entry.name);
430                    let child_dir = dir.join(&entry.name);
431                    if rest.is_empty() {
432                        results.push(child_path);
433                    } else if entry.node_type == NodeType::Directory
434                        || entry.node_type == NodeType::Symlink
435                    {
436                        self.glob_walk(&child_dir, rest, child_path, results, max);
437                    }
438                }
439            }
440        }
441    }
442}
443
444// ---------------------------------------------------------------------------
445// VirtualFs implementation
446// ---------------------------------------------------------------------------
447
448impl VirtualFs for OverlayFs {
449    fn read_file(&self, path: &Path) -> Result<Vec<u8>, VfsError> {
450        let norm = normalize(path)?;
451        let resolved = self.resolve_path(&norm, true)?;
452        match self.resolve_layer(&resolved) {
453            LayerResult::Whiteout => Err(VfsError::NotFound(path.to_path_buf())),
454            LayerResult::Upper => self.upper.read_file(&resolved),
455            LayerResult::Lower => self.read_lower_file(&resolved),
456            LayerResult::NotFound => Err(VfsError::NotFound(path.to_path_buf())),
457        }
458    }
459
460    fn write_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError> {
461        let norm = normalize(path)?;
462        // Ensure parent directories exist in upper
463        if let Some(parent) = norm.parent()
464            && parent != Path::new("/")
465        {
466            self.ensure_upper_dir_path(parent)?;
467        }
468        // Remove whiteout if any (we're creating/overwriting the file)
469        self.remove_whiteout(&norm);
470        self.upper.write_file(&norm, content)
471    }
472
473    fn append_file(&self, path: &Path, content: &[u8]) -> Result<(), VfsError> {
474        let norm = normalize(path)?;
475        let resolved = self.resolve_path(&norm, true)?;
476        match self.resolve_layer(&resolved) {
477            LayerResult::Whiteout => Err(VfsError::NotFound(path.to_path_buf())),
478            LayerResult::Upper => self.upper.append_file(&resolved, content),
479            LayerResult::Lower => {
480                self.copy_up_if_needed(&resolved)?;
481                self.upper.append_file(&resolved, content)
482            }
483            LayerResult::NotFound => Err(VfsError::NotFound(path.to_path_buf())),
484        }
485    }
486
487    fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
488        let norm = normalize(path)?;
489        if self.is_whiteout(&norm) {
490            return Err(VfsError::NotFound(path.to_path_buf()));
491        }
492        let in_upper = self.upper_has_entry(&norm);
493        let in_lower = self.lower_exists(&norm);
494        if !in_upper && !in_lower {
495            return Err(VfsError::NotFound(path.to_path_buf()));
496        }
497        // Verify it's not a directory
498        if in_upper {
499            if let Ok(m) = self.upper.lstat(&norm)
500                && m.node_type == NodeType::Directory
501            {
502                return Err(VfsError::IsADirectory(path.to_path_buf()));
503            }
504        } else if let Ok(m) = self.lstat_lower(&norm)
505            && m.node_type == NodeType::Directory
506        {
507            return Err(VfsError::IsADirectory(path.to_path_buf()));
508        }
509        if in_upper {
510            self.upper.remove_file(&norm)?;
511        }
512        self.add_whiteout(&norm);
513        Ok(())
514    }
515
516    fn mkdir(&self, path: &Path) -> Result<(), VfsError> {
517        let norm = normalize(path)?;
518        if self.is_whiteout(&norm) {
519            // Path was deleted — we can re-create it
520            self.remove_whiteout(&norm);
521        } else {
522            // Check if it already exists in either layer
523            let in_upper = self.upper_has_entry(&norm);
524            let in_lower = self.lower_exists(&norm);
525            if in_upper || in_lower {
526                return Err(VfsError::AlreadyExists(path.to_path_buf()));
527            }
528        }
529        // Ensure parents exist in upper
530        if let Some(parent) = norm.parent()
531            && parent != Path::new("/")
532        {
533            self.ensure_upper_dir_path(parent)?;
534        }
535        // Check if it now exists in upper (ensure_upper_dir_path might have created it)
536        if self.upper_has_entry(&norm) {
537            return Err(VfsError::AlreadyExists(path.to_path_buf()));
538        }
539        self.upper.mkdir(&norm)
540    }
541
542    fn mkdir_p(&self, path: &Path) -> Result<(), VfsError> {
543        let norm = normalize(path)?;
544        let parts = path_components(&norm);
545        if parts.is_empty() {
546            return Ok(());
547        }
548
549        let mut built = PathBuf::from("/");
550        for name in parts {
551            built.push(name);
552
553            // Skip if the whiteout was for this exact path but we want to recreate
554            if self.is_whiteout(&built) {
555                self.remove_whiteout(&built);
556                // Need to create this component in upper
557                self.ensure_single_dir_in_upper(&built)?;
558                continue;
559            }
560
561            // If it exists in upper, verify it's a directory
562            if self.upper_has_entry(&built) {
563                let m = self.upper.lstat(&built)?;
564                if m.node_type != NodeType::Directory {
565                    return Err(VfsError::NotADirectory(path.to_path_buf()));
566                }
567                continue;
568            }
569
570            // If it exists in lower, verify it's a directory — no need to copy up
571            if self.lower_exists(&built) {
572                let m = self.lstat_lower(&built)?;
573                if m.node_type != NodeType::Directory {
574                    return Err(VfsError::NotADirectory(path.to_path_buf()));
575                }
576                continue;
577            }
578
579            // Doesn't exist anywhere — create in upper
580            self.ensure_single_dir_in_upper(&built)?;
581        }
582        Ok(())
583    }
584
585    fn readdir(&self, path: &Path) -> Result<Vec<DirEntry>, VfsError> {
586        let norm = normalize(path)?;
587        if self.is_whiteout(&norm) {
588            return Err(VfsError::NotFound(path.to_path_buf()));
589        }
590
591        let in_upper = self.upper_has_entry(&norm);
592        let in_lower = self.lower_exists(&norm);
593
594        if !in_upper && !in_lower {
595            return Err(VfsError::NotFound(path.to_path_buf()));
596        }
597
598        // Validate directory through overlay (handles symlinks across layers)
599        let m = self.stat(&norm)?;
600        if m.node_type != NodeType::Directory {
601            return Err(VfsError::NotADirectory(path.to_path_buf()));
602        }
603
604        self.readdir_merged(&norm)
605    }
606
607    fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
608        let norm = normalize(path)?;
609        if self.is_whiteout(&norm) {
610            return Err(VfsError::NotFound(path.to_path_buf()));
611        }
612
613        // Check that it exists and is a directory
614        let m = self.lstat_overlay(&norm, path)?;
615        if m.node_type != NodeType::Directory {
616            return Err(VfsError::NotADirectory(path.to_path_buf()));
617        }
618
619        // Check that it's empty (merged view)
620        let entries = self.readdir_merged(&norm)?;
621        if !entries.is_empty() {
622            return Err(VfsError::DirectoryNotEmpty(path.to_path_buf()));
623        }
624
625        // Remove from upper if present
626        if self.upper_has_entry(&norm) {
627            self.upper.remove_dir(&norm).ok();
628        }
629        self.add_whiteout(&norm);
630        Ok(())
631    }
632
633    fn remove_dir_all(&self, path: &Path) -> Result<(), VfsError> {
634        let norm = normalize(path)?;
635        if self.is_whiteout(&norm) {
636            return Err(VfsError::NotFound(path.to_path_buf()));
637        }
638
639        // Check that it exists
640        let m = self.lstat_overlay(&norm, path)?;
641        if m.node_type != NodeType::Directory {
642            return Err(VfsError::NotADirectory(path.to_path_buf()));
643        }
644
645        // Recursively whiteout all children
646        self.whiteout_recursive(&norm)?;
647
648        // Remove the directory subtree from upper if present
649        if self.upper_has_entry(&norm) {
650            self.upper.remove_dir_all(&norm).ok();
651        }
652
653        // Whiteout the directory itself
654        self.add_whiteout(&norm);
655        Ok(())
656    }
657
658    fn exists(&self, path: &Path) -> bool {
659        let norm = match normalize(path) {
660            Ok(p) => p,
661            Err(_) => return false,
662        };
663        if self.is_whiteout(&norm) {
664            return false;
665        }
666        // Check if entry exists in either layer without following symlinks first
667        if !self.upper_has_entry(&norm) && !self.lower_exists(&norm) {
668            return false;
669        }
670        // If it's a symlink, verify the target exists through the overlay
671        let meta = match self.lstat_overlay(&norm, &norm) {
672            Ok(m) => m,
673            Err(_) => return false,
674        };
675        if meta.node_type == NodeType::Symlink {
676            return self.stat(&norm).is_ok();
677        }
678        true
679    }
680
681    fn stat(&self, path: &Path) -> Result<Metadata, VfsError> {
682        let norm = normalize(path)?;
683        // Try to resolve symlinks
684        let resolved = self.resolve_path(&norm, true)?;
685        match self.resolve_layer(&resolved) {
686            LayerResult::Whiteout => Err(VfsError::NotFound(path.to_path_buf())),
687            LayerResult::Upper => self.upper.stat(&resolved),
688            LayerResult::Lower => self.stat_lower(&resolved),
689            LayerResult::NotFound => Err(VfsError::NotFound(path.to_path_buf())),
690        }
691    }
692
693    fn lstat(&self, path: &Path) -> Result<Metadata, VfsError> {
694        let norm = normalize(path)?;
695        self.lstat_overlay(&norm, path)
696    }
697
698    fn chmod(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
699        let norm = normalize(path)?;
700        let resolved = self.resolve_path(&norm, true)?;
701        match self.resolve_layer(&resolved) {
702            LayerResult::Whiteout => Err(VfsError::NotFound(path.to_path_buf())),
703            LayerResult::Upper => self.upper.chmod(&resolved, mode),
704            LayerResult::Lower => {
705                // Need to copy up the file/dir to apply chmod
706                let meta = self.lstat_lower(&resolved)?;
707                match meta.node_type {
708                    NodeType::File => {
709                        self.copy_up_if_needed(&resolved)?;
710                        self.upper.chmod(&resolved, mode)
711                    }
712                    NodeType::Directory => {
713                        self.ensure_upper_dir_path(&resolved)?;
714                        self.upper.chmod(&resolved, mode)
715                    }
716                    NodeType::Symlink => {
717                        Err(VfsError::IoError("cannot chmod a symlink directly".into()))
718                    }
719                }
720            }
721            LayerResult::NotFound => Err(VfsError::NotFound(path.to_path_buf())),
722        }
723    }
724
725    fn utimes(&self, path: &Path, mtime: SystemTime) -> Result<(), VfsError> {
726        let norm = normalize(path)?;
727        let resolved = self.resolve_path(&norm, true)?;
728        match self.resolve_layer(&resolved) {
729            LayerResult::Whiteout => Err(VfsError::NotFound(path.to_path_buf())),
730            LayerResult::Upper => self.upper.utimes(&resolved, mtime),
731            LayerResult::Lower => {
732                let meta = self.lstat_lower(&resolved)?;
733                match meta.node_type {
734                    NodeType::File => {
735                        self.copy_up_if_needed(&resolved)?;
736                        self.upper.utimes(&resolved, mtime)
737                    }
738                    NodeType::Directory => {
739                        self.ensure_upper_dir_path(&resolved)?;
740                        self.upper.utimes(&resolved, mtime)
741                    }
742                    NodeType::Symlink => {
743                        self.copy_up_symlink_if_needed(&resolved)?;
744                        self.upper.utimes(&resolved, mtime)
745                    }
746                }
747            }
748            LayerResult::NotFound => Err(VfsError::NotFound(path.to_path_buf())),
749        }
750    }
751
752    fn symlink(&self, target: &Path, link: &Path) -> Result<(), VfsError> {
753        let norm_link = normalize(link)?;
754        // Ensure parent in upper
755        if let Some(parent) = norm_link.parent()
756            && parent != Path::new("/")
757        {
758            self.ensure_upper_dir_path(parent)?;
759        }
760        // If there's a whiteout, remove it to allow re-creation
761        self.remove_whiteout(&norm_link);
762        self.upper.symlink(target, &norm_link)
763    }
764
765    fn hardlink(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
766        let norm_src = normalize(src)?;
767        let norm_dst = normalize(dst)?;
768        // Read source from whichever layer has it
769        let content = self.read_file(&norm_src)?;
770        let meta = self.stat(&norm_src)?;
771        // Ensure parent for dst in upper
772        if let Some(parent) = norm_dst.parent()
773            && parent != Path::new("/")
774        {
775            self.ensure_upper_dir_path(parent)?;
776        }
777        self.remove_whiteout(&norm_dst);
778        self.upper.write_file(&norm_dst, &content)?;
779        self.upper.chmod(&norm_dst, meta.mode)?;
780        self.upper.utimes(&norm_dst, meta.mtime)?;
781        Ok(())
782    }
783
784    fn readlink(&self, path: &Path) -> Result<PathBuf, VfsError> {
785        let norm = normalize(path)?;
786        if self.is_whiteout(&norm) {
787            return Err(VfsError::NotFound(path.to_path_buf()));
788        }
789        if self.upper_has_entry(&norm) {
790            return self.upper.readlink(&norm);
791        }
792        if self.lower_exists(&norm) {
793            return self.readlink_lower(&norm);
794        }
795        Err(VfsError::NotFound(path.to_path_buf()))
796    }
797
798    fn canonicalize(&self, path: &Path) -> Result<PathBuf, VfsError> {
799        let norm = normalize(path)?;
800        let resolved = self.resolve_path(&norm, true)?;
801        // Make sure the resolved path actually exists
802        if self.is_whiteout(&resolved) {
803            return Err(VfsError::NotFound(path.to_path_buf()));
804        }
805        if !self.upper_has_entry(&resolved) && !self.lower_exists(&resolved) {
806            return Err(VfsError::NotFound(path.to_path_buf()));
807        }
808        Ok(resolved)
809    }
810
811    fn copy(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
812        let norm_src = normalize(src)?;
813        let norm_dst = normalize(dst)?;
814        // Read from resolved layer
815        let content = self.read_file(&norm_src)?;
816        let meta = self.stat(&norm_src)?;
817        // Write to upper
818        self.write_file(&norm_dst, &content)?;
819        self.chmod(&norm_dst, meta.mode)?;
820        Ok(())
821    }
822
823    fn rename(&self, src: &Path, dst: &Path) -> Result<(), VfsError> {
824        let norm_src = normalize(src)?;
825        let norm_dst = normalize(dst)?;
826
827        if self.is_whiteout(&norm_src) {
828            return Err(VfsError::NotFound(src.to_path_buf()));
829        }
830
831        // Copy-up src if only in lower
832        let in_upper = self.upper_has_entry(&norm_src);
833        let in_lower = self.lower_exists(&norm_src);
834        if !in_upper && !in_lower {
835            return Err(VfsError::NotFound(src.to_path_buf()));
836        }
837
838        // Read content and metadata
839        let meta = self.lstat_overlay(&norm_src, src)?;
840        match meta.node_type {
841            NodeType::File => {
842                let content = self.read_file(&norm_src)?;
843                // Ensure dst parent exists in upper
844                if let Some(parent) = norm_dst.parent()
845                    && parent != Path::new("/")
846                {
847                    self.ensure_upper_dir_path(parent)?;
848                }
849                self.remove_whiteout(&norm_dst);
850                self.upper.write_file(&norm_dst, &content)?;
851                self.upper.chmod(&norm_dst, meta.mode)?;
852            }
853            NodeType::Symlink => {
854                let target = self.readlink(&norm_src)?;
855                if let Some(parent) = norm_dst.parent()
856                    && parent != Path::new("/")
857                {
858                    self.ensure_upper_dir_path(parent)?;
859                }
860                self.remove_whiteout(&norm_dst);
861                self.upper.symlink(&target, &norm_dst)?;
862            }
863            NodeType::Directory => {
864                // For directory rename, copy all children recursively
865                if let Some(parent) = norm_dst.parent()
866                    && parent != Path::new("/")
867                {
868                    self.ensure_upper_dir_path(parent)?;
869                }
870                self.remove_whiteout(&norm_dst);
871                self.upper.mkdir_p(&norm_dst)?;
872                let entries = self.readdir_merged(&norm_src)?;
873                for entry in entries {
874                    let child_src = norm_src.join(&entry.name);
875                    let child_dst = norm_dst.join(&entry.name);
876                    self.rename(&child_src, &child_dst)?;
877                }
878            }
879        }
880
881        // Remove from upper if it was there
882        if in_upper {
883            match meta.node_type {
884                NodeType::Directory => {
885                    self.upper.remove_dir_all(&norm_src).ok();
886                }
887                _ => {
888                    self.upper.remove_file(&norm_src).ok();
889                }
890            }
891        }
892        // Whiteout the source to hide from lower
893        self.add_whiteout(&norm_src);
894        Ok(())
895    }
896
897    fn glob(&self, pattern: &str, cwd: &Path) -> Result<Vec<PathBuf>, VfsError> {
898        let is_absolute = pattern.starts_with('/');
899        let abs_pattern = if is_absolute {
900            pattern.to_string()
901        } else {
902            let cwd_str = cwd.to_str().unwrap_or("/").trim_end_matches('/');
903            format!("{cwd_str}/{pattern}")
904        };
905
906        let components: Vec<&str> = abs_pattern.split('/').filter(|s| !s.is_empty()).collect();
907        let mut results = Vec::new();
908        let max = 100_000;
909        self.glob_walk(
910            Path::new("/"),
911            &components,
912            PathBuf::from("/"),
913            &mut results,
914            max,
915        );
916
917        results.sort();
918        results.dedup();
919
920        if !is_absolute {
921            results = results
922                .into_iter()
923                .filter_map(|p| p.strip_prefix(cwd).ok().map(|r| r.to_path_buf()))
924                .collect();
925        }
926
927        Ok(results)
928    }
929
930    fn deep_clone(&self) -> Arc<dyn VirtualFs> {
931        Arc::new(OverlayFs {
932            lower: self.lower.clone(),
933            upper: self.upper.deep_clone(),
934            whiteouts: Arc::new(RwLock::new(self.whiteouts.read().clone())),
935        })
936    }
937}
938
939// ---------------------------------------------------------------------------
940// Private OverlayFs helpers
941// ---------------------------------------------------------------------------
942
943impl OverlayFs {
944    /// lstat through the overlay (no symlink following on final component).
945    fn lstat_overlay(&self, norm: &Path, orig: &Path) -> Result<Metadata, VfsError> {
946        if self.is_whiteout(norm) {
947            return Err(VfsError::NotFound(orig.to_path_buf()));
948        }
949        if self.upper_has_entry(norm) {
950            return self.upper.lstat(norm);
951        }
952        if self.lower_exists(norm) {
953            return self.lstat_lower(norm);
954        }
955        Err(VfsError::NotFound(orig.to_path_buf()))
956    }
957
958    /// Ensure a single directory exists in the upper layer at `path`.
959    fn ensure_single_dir_in_upper(&self, path: &Path) -> Result<(), VfsError> {
960        if self.upper_has_entry(path) {
961            return Ok(());
962        }
963        // Ensure parent first
964        if let Some(parent) = path.parent()
965            && parent != Path::new("/")
966            && !self.upper_has_entry(parent)
967        {
968            self.ensure_single_dir_in_upper(parent)?;
969        }
970        self.upper.mkdir(path)
971    }
972
973    /// Copy-up a symlink from lower to upper.
974    fn copy_up_symlink_if_needed(&self, path: &Path) -> Result<(), VfsError> {
975        if self.upper_has_entry(path) {
976            return Ok(());
977        }
978        if let Some(parent) = path.parent()
979            && parent != Path::new("/")
980        {
981            self.ensure_upper_dir_path(parent)?;
982        }
983        let target = self.readlink_lower(path)?;
984        self.upper.symlink(&target, path)
985    }
986}
987
988// ---------------------------------------------------------------------------
989// Shared helpers
990// ---------------------------------------------------------------------------
991
992/// Normalize an absolute path: resolve `.` and `..`.
993fn normalize(path: &Path) -> Result<PathBuf, VfsError> {
994    let s = path.to_str().unwrap_or("");
995    if s.is_empty() {
996        return Err(VfsError::InvalidPath("empty path".into()));
997    }
998    if !super::vfs_path_is_absolute(path) {
999        return Err(VfsError::InvalidPath(format!(
1000            "path must be absolute: {}",
1001            path.display()
1002        )));
1003    }
1004    let mut parts: Vec<String> = Vec::new();
1005    for comp in path.components() {
1006        match comp {
1007            Component::RootDir | Component::Prefix(_) => {}
1008            Component::CurDir => {}
1009            Component::ParentDir => {
1010                parts.pop();
1011            }
1012            Component::Normal(seg) => {
1013                if let Some(s) = seg.to_str() {
1014                    parts.push(s.to_owned());
1015                } else {
1016                    return Err(VfsError::InvalidPath(format!(
1017                        "non-UTF-8 component in: {}",
1018                        path.display()
1019                    )));
1020                }
1021            }
1022        }
1023    }
1024    let mut result = PathBuf::from("/");
1025    for p in &parts {
1026        result.push(p);
1027    }
1028    Ok(result)
1029}
1030
1031/// Split a normalized absolute path into component names.
1032fn path_components(path: &Path) -> Vec<&str> {
1033    path.components()
1034        .filter_map(|c| match c {
1035            Component::Normal(s) => s.to_str(),
1036            _ => None,
1037        })
1038        .collect()
1039}
1040
1041/// Map `std::io::Error` to `VfsError`.
1042fn map_io_error(err: std::io::Error, path: &Path) -> VfsError {
1043    let p = path.to_path_buf();
1044    match err.kind() {
1045        std::io::ErrorKind::NotFound => VfsError::NotFound(p),
1046        std::io::ErrorKind::AlreadyExists => VfsError::AlreadyExists(p),
1047        std::io::ErrorKind::PermissionDenied => VfsError::PermissionDenied(p),
1048        std::io::ErrorKind::DirectoryNotEmpty => VfsError::DirectoryNotEmpty(p),
1049        std::io::ErrorKind::NotADirectory => VfsError::NotADirectory(p),
1050        std::io::ErrorKind::IsADirectory => VfsError::IsADirectory(p),
1051        _ => VfsError::IoError(err.to_string()),
1052    }
1053}
1054
1055/// Map `std::fs::Metadata` to our `vfs::Metadata`.
1056fn map_std_metadata(meta: &std::fs::Metadata) -> Metadata {
1057    let node_type = if meta.is_symlink() {
1058        NodeType::Symlink
1059    } else if meta.is_dir() {
1060        NodeType::Directory
1061    } else {
1062        NodeType::File
1063    };
1064    Metadata {
1065        node_type,
1066        size: meta.len(),
1067        mode: meta.permissions().mode(),
1068        mtime: meta.modified().unwrap_or(SystemTime::UNIX_EPOCH),
1069    }
1070}