Skip to main content

agent_os_kernel/
overlay_fs.rs

1use crate::vfs::{
2    normalize_path, MemoryFileSystem, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem,
3    VirtualStat, VirtualUtimeSpec,
4};
5use base64::Engine;
6use std::collections::BTreeSet;
7
8const MAX_SNAPSHOT_DEPTH: usize = 1024;
9const OVERLAY_METADATA_ROOT: &str = "/.agent-os-overlay";
10const OVERLAY_WHITEOUT_DIR: &str = "/.agent-os-overlay/whiteouts";
11const OVERLAY_OPAQUE_DIR: &str = "/.agent-os-overlay/opaque";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OverlayMode {
15    Ephemeral,
16    ReadOnly,
17}
18
19#[derive(Debug)]
20pub struct OverlayFileSystem {
21    lowers: Vec<MemoryFileSystem>,
22    upper: Option<MemoryFileSystem>,
23    writes_locked: bool,
24}
25
26#[derive(Debug, Clone, Copy)]
27enum OverlayMarkerKind {
28    Whiteout,
29    Opaque,
30}
31
32#[derive(Debug)]
33enum OverlaySnapshotKind {
34    Directory,
35    File(Vec<u8>),
36    Symlink(String),
37}
38
39#[derive(Debug)]
40struct OverlaySnapshotEntry {
41    path: String,
42    stat: VirtualStat,
43    kind: OverlaySnapshotKind,
44}
45
46impl OverlayFileSystem {
47    pub fn new(lowers: Vec<MemoryFileSystem>, mode: OverlayMode) -> Self {
48        let mut effective_lowers = lowers;
49        if effective_lowers.is_empty() {
50            effective_lowers.push(MemoryFileSystem::new());
51        }
52
53        let mut upper = match mode {
54            OverlayMode::Ephemeral => Some(MemoryFileSystem::new()),
55            OverlayMode::ReadOnly => None,
56        };
57        if let Some(upper_filesystem) = upper.as_mut() {
58            sync_upper_root_metadata(upper_filesystem, &effective_lowers);
59        }
60
61        Self {
62            lowers: effective_lowers,
63            upper,
64            writes_locked: matches!(mode, OverlayMode::ReadOnly),
65        }
66    }
67
68    pub fn with_upper(lowers: Vec<MemoryFileSystem>, upper: MemoryFileSystem) -> Self {
69        let mut effective_lowers = lowers;
70        if effective_lowers.is_empty() {
71            effective_lowers.push(MemoryFileSystem::new());
72        }
73
74        Self {
75            lowers: effective_lowers,
76            upper: Some(upper),
77            writes_locked: false,
78        }
79    }
80
81    pub fn lock_writes(&mut self) {
82        self.writes_locked = true;
83    }
84
85    fn normalized(path: &str) -> String {
86        normalize_path(path)
87    }
88
89    fn encode_marker_path(path: &str) -> String {
90        base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(path)
91    }
92
93    fn marker_directory(kind: OverlayMarkerKind) -> &'static str {
94        match kind {
95            OverlayMarkerKind::Whiteout => OVERLAY_WHITEOUT_DIR,
96            OverlayMarkerKind::Opaque => OVERLAY_OPAQUE_DIR,
97        }
98    }
99
100    fn marker_path(kind: OverlayMarkerKind, path: &str) -> String {
101        format!(
102            "{}/{}",
103            Self::marker_directory(kind),
104            Self::encode_marker_path(&Self::normalized(path))
105        )
106    }
107
108    fn is_internal_metadata_path(path: &str) -> bool {
109        let normalized = Self::normalized(path);
110        normalized == OVERLAY_METADATA_ROOT
111            || normalized.starts_with(&(String::from(OVERLAY_METADATA_ROOT) + "/"))
112    }
113
114    fn hidden_root_entry_name() -> &'static str {
115        ".agent-os-overlay"
116    }
117
118    fn should_hide_directory_entry(path: &str, entry: &str) -> bool {
119        let normalized = Self::normalized(path);
120        normalized == "/" && entry == Self::hidden_root_entry_name()
121    }
122
123    fn should_ignore_raw_directory_entry(
124        upper: Option<&MemoryFileSystem>,
125        path: &str,
126        entry: &str,
127    ) -> bool {
128        if entry == "." || entry == ".." || Self::should_hide_directory_entry(path, entry) {
129            return true;
130        }
131
132        let entry_path = Self::join_path(path, entry);
133        Self::marker_exists_in_upper(upper, OverlayMarkerKind::Whiteout, &entry_path)
134    }
135
136    fn marker_exists(&self, kind: OverlayMarkerKind, path: &str) -> bool {
137        Self::marker_exists_in_upper(self.upper.as_ref(), kind, path)
138    }
139
140    fn marker_exists_in_upper(
141        upper: Option<&MemoryFileSystem>,
142        kind: OverlayMarkerKind,
143        path: &str,
144    ) -> bool {
145        upper.is_some_and(|filesystem| filesystem.exists(&Self::marker_path(kind, path)))
146    }
147
148    fn is_whited_out(&self, path: &str) -> bool {
149        self.marker_exists(OverlayMarkerKind::Whiteout, path)
150    }
151
152    fn ensure_metadata_directories_in_upper(&mut self, path: &str) -> VfsResult<()> {
153        let upper = self.writable_upper(path)?;
154        upper.mkdir(OVERLAY_METADATA_ROOT, true)?;
155        upper.mkdir(OVERLAY_WHITEOUT_DIR, true)?;
156        upper.mkdir(OVERLAY_OPAQUE_DIR, true)?;
157        Ok(())
158    }
159
160    fn set_marker(&mut self, kind: OverlayMarkerKind, path: &str, present: bool) -> VfsResult<()> {
161        let marker_path = Self::marker_path(kind, path);
162        if present {
163            self.ensure_metadata_directories_in_upper(path)?;
164            self.writable_upper(path)?
165                .write_file(&marker_path, Self::normalized(path).into_bytes())?;
166            return Ok(());
167        }
168
169        if self
170            .upper
171            .as_ref()
172            .is_some_and(|upper| upper.exists(&marker_path))
173        {
174            self.writable_upper(path)?.remove_file(&marker_path)?;
175        }
176        Ok(())
177    }
178
179    fn add_whiteout(&mut self, path: &str) -> VfsResult<()> {
180        self.set_marker(OverlayMarkerKind::Whiteout, path, true)
181    }
182
183    fn remove_whiteout(&mut self, path: &str) -> VfsResult<()> {
184        self.set_marker(OverlayMarkerKind::Whiteout, path, false)
185    }
186
187    fn mark_opaque_directory(&mut self, path: &str) -> VfsResult<()> {
188        self.set_marker(OverlayMarkerKind::Opaque, path, true)
189    }
190
191    fn clear_opaque_directory(&mut self, path: &str) -> VfsResult<()> {
192        self.set_marker(OverlayMarkerKind::Opaque, path, false)
193    }
194
195    fn clear_path_metadata(&mut self, path: &str) -> VfsResult<()> {
196        self.remove_whiteout(path)?;
197        self.clear_opaque_directory(path)
198    }
199
200    fn join_path(base: &str, name: &str) -> String {
201        if base == "/" {
202            format!("/{name}")
203        } else {
204            format!("{base}/{name}")
205        }
206    }
207
208    fn rebase_path(path: &str, old_root: &str, new_root: &str) -> String {
209        if path == old_root {
210            return String::from(new_root);
211        }
212
213        format!("{new_root}{}", &path[old_root.len()..])
214    }
215
216    fn read_only_error(path: &str) -> VfsError {
217        VfsError::new("EROFS", format!("read-only filesystem: {path}"))
218    }
219
220    fn entry_not_found(path: &str) -> VfsError {
221        VfsError::new("ENOENT", format!("no such file: {path}"))
222    }
223
224    fn directory_not_found(path: &str) -> VfsError {
225        VfsError::new("ENOENT", format!("no such directory: {path}"))
226    }
227
228    fn already_exists(path: &str) -> VfsError {
229        VfsError::new("EEXIST", format!("file exists: {path}"))
230    }
231
232    fn not_directory(path: &str) -> VfsError {
233        VfsError::new("ENOTDIR", format!("not a directory: {path}"))
234    }
235
236    fn writable_upper(&mut self, path: &str) -> VfsResult<&mut MemoryFileSystem> {
237        if self.writes_locked {
238            return Err(Self::read_only_error(path));
239        }
240        self.upper
241            .as_mut()
242            .ok_or_else(|| Self::read_only_error(path))
243    }
244
245    fn path_exists_in_filesystem(filesystem: &MemoryFileSystem, path: &str) -> bool {
246        filesystem.exists(path)
247    }
248
249    fn has_entry_in_filesystem(filesystem: &MemoryFileSystem, path: &str) -> bool {
250        filesystem.lstat(path).is_ok()
251    }
252
253    fn exists_in_upper(&self, path: &str) -> bool {
254        self.upper
255            .as_ref()
256            .is_some_and(|upper| Self::path_exists_in_filesystem(upper, path))
257    }
258
259    fn has_entry_in_upper(&self, path: &str) -> bool {
260        self.upper
261            .as_ref()
262            .is_some_and(|upper| Self::has_entry_in_filesystem(upper, path))
263    }
264
265    fn find_lower_by_exists(&self, path: &str) -> Option<usize> {
266        self.lowers
267            .iter()
268            .position(|lower| Self::path_exists_in_filesystem(lower, path))
269    }
270
271    fn find_lower_by_entry(&self, path: &str) -> Option<(usize, VirtualStat)> {
272        self.lowers
273            .iter()
274            .enumerate()
275            .find_map(|(index, lower)| lower.lstat(path).ok().map(|stat| (index, stat)))
276    }
277
278    fn merged_lstat(&self, path: &str) -> VfsResult<VirtualStat> {
279        if Self::is_internal_metadata_path(path) {
280            return Err(Self::entry_not_found(path));
281        }
282        if self.is_whited_out(path) {
283            return Err(Self::entry_not_found(path));
284        }
285        if self.has_entry_in_upper(path) {
286            return self
287                .upper
288                .as_ref()
289                .expect("upper must exist when entry exists")
290                .lstat(path);
291        }
292        self.find_lower_by_entry(path)
293            .map(|(_, stat)| stat)
294            .ok_or_else(|| Self::entry_not_found(path))
295    }
296
297    fn ensure_ancestor_directories_in_upper(&mut self, path: &str) -> VfsResult<()> {
298        if Self::is_internal_metadata_path(path) {
299            return Err(VfsError::permission_denied("mkdir", path));
300        }
301        let normalized = Self::normalized(path);
302        let parts = normalized
303            .split('/')
304            .filter(|part| !part.is_empty())
305            .collect::<Vec<_>>();
306
307        let mut current = String::new();
308        for part in parts.iter().take(parts.len().saturating_sub(1)) {
309            current.push('/');
310            current.push_str(part);
311
312            if self.exists_in_upper(&current) {
313                continue;
314            }
315
316            if let Some(index) = self.find_lower_by_exists(&current) {
317                let stat = self.lowers[index].stat(&current)?;
318                if !stat.is_directory {
319                    return Err(Self::not_directory(&current));
320                }
321
322                let upper = self.writable_upper(&current)?;
323                upper.mkdir(&current, false)?;
324                upper.chmod(&current, stat.mode)?;
325                upper.chown(&current, stat.uid, stat.gid)?;
326                continue;
327            }
328
329            let upper = self.writable_upper(&current)?;
330            upper.mkdir(&current, false)?;
331        }
332
333        Ok(())
334    }
335
336    fn copy_up_path(&mut self, path: &str) -> VfsResult<()> {
337        if self.has_entry_in_upper(path) {
338            return Ok(());
339        }
340
341        self.ensure_ancestor_directories_in_upper(path)?;
342
343        let (lower_index, stat) = self
344            .find_lower_by_entry(path)
345            .ok_or_else(|| Self::entry_not_found(path))?;
346
347        if stat.is_symbolic_link {
348            let target = self.lowers[lower_index].read_link(path)?;
349            let upper = self.writable_upper(path)?;
350            upper.symlink(&target, path)?;
351            return Ok(());
352        }
353
354        if stat.is_directory {
355            let upper = self.writable_upper(path)?;
356            upper.mkdir(path, false)?;
357            upper.chmod(path, stat.mode)?;
358            upper.chown(path, stat.uid, stat.gid)?;
359            self.mark_opaque_directory(path)?;
360            return Ok(());
361        }
362
363        let data = self.lowers[lower_index].read_file(path)?;
364        let upper = self.writable_upper(path)?;
365        upper.write_file(path, data)?;
366        upper.chmod(path, stat.mode)?;
367        upper.chown(path, stat.uid, stat.gid)?;
368        Ok(())
369    }
370
371    fn path_exists_in_merged_view(&self, path: &str) -> bool {
372        if self.is_whited_out(path) {
373            return false;
374        }
375        if self.has_entry_in_upper(path) {
376            return true;
377        }
378        self.find_lower_by_entry(path).is_some()
379    }
380
381    fn not_empty(path: &str) -> VfsError {
382        VfsError::new("ENOTEMPTY", format!("directory not empty, rmdir '{path}'"))
383    }
384
385    fn collect_snapshot_entries(
386        &mut self,
387        path: &str,
388        entries: &mut Vec<OverlaySnapshotEntry>,
389    ) -> VfsResult<()> {
390        let mut pending = vec![(Self::normalized(path), 0usize)];
391        while let Some((current_path, depth)) = pending.pop() {
392            if depth > MAX_SNAPSHOT_DEPTH {
393                return Err(VfsError::new(
394                    "EINVAL",
395                    format!("overlay snapshot depth limit exceeded at '{current_path}'"),
396                ));
397            }
398
399            let stat = self.lstat(&current_path)?;
400
401            if stat.is_symbolic_link {
402                entries.push(OverlaySnapshotEntry {
403                    path: current_path.clone(),
404                    stat,
405                    kind: OverlaySnapshotKind::Symlink(self.read_link(&current_path)?),
406                });
407                continue;
408            }
409
410            if stat.is_directory {
411                entries.push(OverlaySnapshotEntry {
412                    path: current_path.clone(),
413                    stat,
414                    kind: OverlaySnapshotKind::Directory,
415                });
416
417                let children = self.read_dir_with_types(&current_path)?;
418                for entry in children.into_iter().rev() {
419                    pending.push((Self::join_path(&current_path, &entry.name), depth + 1));
420                }
421                continue;
422            }
423
424            entries.push(OverlaySnapshotEntry {
425                path: current_path.clone(),
426                stat,
427                kind: OverlaySnapshotKind::File(self.read_file(&current_path)?),
428            });
429        }
430        Ok(())
431    }
432
433    fn remove_snapshot_entries(&mut self, entries: &[OverlaySnapshotEntry]) -> VfsResult<()> {
434        for entry in entries.iter().rev() {
435            if self.has_entry_in_upper(&entry.path) {
436                match entry.kind {
437                    OverlaySnapshotKind::Directory => {
438                        self.writable_upper(&entry.path)?.remove_dir(&entry.path)?;
439                    }
440                    OverlaySnapshotKind::File(_) | OverlaySnapshotKind::Symlink(_) => {
441                        self.writable_upper(&entry.path)?.remove_file(&entry.path)?;
442                    }
443                }
444            }
445
446            if self.find_lower_by_entry(&entry.path).is_some() {
447                self.clear_opaque_directory(&entry.path)?;
448                self.add_whiteout(&entry.path)?;
449            } else {
450                self.clear_path_metadata(&entry.path)?;
451            }
452        }
453
454        Ok(())
455    }
456
457    fn directory_has_raw_children(&mut self, path: &str) -> VfsResult<bool> {
458        let normalized = Self::normalized(path);
459        let mut directory_exists = false;
460
461        if let Some(upper) = self.upper.as_mut() {
462            if let Ok(entries) = upper.read_dir(&normalized) {
463                directory_exists = true;
464                if entries.into_iter().any(|entry| {
465                    !Self::should_ignore_raw_directory_entry(Some(&*upper), &normalized, &entry)
466                }) {
467                    return Ok(true);
468                }
469            }
470        }
471
472        let upper = self.upper.as_ref();
473        for lower in self.lowers.iter_mut().rev() {
474            if let Ok(entries) = lower.read_dir(&normalized) {
475                directory_exists = true;
476                if entries.into_iter().any(|entry| {
477                    !Self::should_ignore_raw_directory_entry(upper, &normalized, &entry)
478                }) {
479                    return Ok(true);
480                }
481            }
482        }
483
484        if !directory_exists {
485            return Err(Self::directory_not_found(path));
486        }
487
488        Ok(false)
489    }
490
491    fn marker_paths_in_upper(&mut self, kind: OverlayMarkerKind) -> VfsResult<Vec<String>> {
492        let Some(upper) = self.upper.as_mut() else {
493            return Ok(Vec::new());
494        };
495
496        let marker_dir = Self::marker_directory(kind);
497        let entries = match upper.read_dir(marker_dir) {
498            Ok(entries) => entries,
499            Err(error) if error.code() == "ENOENT" => return Ok(Vec::new()),
500            Err(error) => return Err(error),
501        };
502
503        let mut marker_paths = Vec::new();
504        for entry in entries {
505            if entry == "." || entry == ".." {
506                continue;
507            }
508
509            let marker_file = Self::join_path(marker_dir, &entry);
510            let marker_path =
511                String::from_utf8(upper.read_file(&marker_file).map_err(|_| {
512                    VfsError::io(format!("invalid overlay marker '{marker_file}'"))
513                })?)
514                .map_err(|_| VfsError::io(format!("invalid overlay marker '{marker_file}'")))?;
515            marker_paths.push(Self::normalized(&marker_path));
516        }
517
518        Ok(marker_paths)
519    }
520
521    fn path_in_subtree(path: &str, root: &str) -> bool {
522        path == root || path.starts_with(&(String::from(root) + "/"))
523    }
524
525    fn clear_subtree_metadata(&mut self, path: &str) -> VfsResult<()> {
526        let normalized = Self::normalized(path);
527        for kind in [OverlayMarkerKind::Whiteout, OverlayMarkerKind::Opaque] {
528            for marker_path in self.marker_paths_in_upper(kind)? {
529                if Self::path_in_subtree(&marker_path, &normalized) {
530                    self.set_marker(kind, &marker_path, false)?;
531                }
532            }
533        }
534        Ok(())
535    }
536
537    fn copy_subtree_metadata(&mut self, old_root: &str, new_root: &str) -> VfsResult<()> {
538        let old_normalized = Self::normalized(old_root);
539        let new_normalized = Self::normalized(new_root);
540
541        for kind in [OverlayMarkerKind::Whiteout, OverlayMarkerKind::Opaque] {
542            for marker_path in self.marker_paths_in_upper(kind)? {
543                if Self::path_in_subtree(&marker_path, &old_normalized) {
544                    let destination =
545                        Self::rebase_path(&marker_path, &old_normalized, &new_normalized);
546                    self.set_marker(kind, &destination, true)?;
547                }
548            }
549        }
550
551        Ok(())
552    }
553
554    fn stage_snapshot_entries_in_upper(
555        &mut self,
556        entries: &[OverlaySnapshotEntry],
557    ) -> VfsResult<()> {
558        for entry in entries {
559            match &entry.kind {
560                OverlaySnapshotKind::Directory => {
561                    if !self.has_entry_in_upper(&entry.path) {
562                        self.ensure_ancestor_directories_in_upper(&entry.path)?;
563                        self.writable_upper(&entry.path)?.create_dir(&entry.path)?;
564                    }
565                    self.writable_upper(&entry.path)?
566                        .chmod(&entry.path, entry.stat.mode)?;
567                    self.writable_upper(&entry.path)?.chown(
568                        &entry.path,
569                        entry.stat.uid,
570                        entry.stat.gid,
571                    )?;
572                    self.mark_opaque_directory(&entry.path)?;
573                }
574                OverlaySnapshotKind::File(data) => {
575                    if self.has_entry_in_upper(&entry.path) {
576                        continue;
577                    }
578                    self.ensure_ancestor_directories_in_upper(&entry.path)?;
579                    self.writable_upper(&entry.path)?
580                        .write_file(&entry.path, data.clone())?;
581                    self.writable_upper(&entry.path)?
582                        .chmod(&entry.path, entry.stat.mode)?;
583                    self.writable_upper(&entry.path)?.chown(
584                        &entry.path,
585                        entry.stat.uid,
586                        entry.stat.gid,
587                    )?;
588                }
589                OverlaySnapshotKind::Symlink(target) => {
590                    if self.has_entry_in_upper(&entry.path) {
591                        continue;
592                    }
593                    self.ensure_ancestor_directories_in_upper(&entry.path)?;
594                    self.writable_upper(&entry.path)?
595                        .symlink(target, &entry.path)?;
596                }
597            }
598        }
599
600        Ok(())
601    }
602}
603
604fn sync_upper_root_metadata(upper: &mut MemoryFileSystem, lowers: &[MemoryFileSystem]) {
605    let Some(root_stat) = lowers.iter().find_map(|lower| lower.lstat("/").ok()) else {
606        return;
607    };
608
609    upper
610        .chmod("/", root_stat.mode)
611        .expect("overlay upper root should exist");
612    upper
613        .chown("/", root_stat.uid, root_stat.gid)
614        .expect("overlay upper root should exist");
615}
616
617impl VirtualFileSystem for OverlayFileSystem {
618    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
619        if Self::is_internal_metadata_path(path) {
620            return Err(Self::entry_not_found(path));
621        }
622        if self.is_whited_out(path) {
623            return Err(Self::entry_not_found(path));
624        }
625        if self.exists_in_upper(path) {
626            return self
627                .upper
628                .as_mut()
629                .expect("upper must exist when path exists")
630                .read_file(path);
631        }
632        let Some(index) = self.find_lower_by_exists(path) else {
633            return Err(Self::entry_not_found(path));
634        };
635        self.lowers[index].read_file(path)
636    }
637
638    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
639        if Self::is_internal_metadata_path(path) {
640            return Err(Self::directory_not_found(path));
641        }
642        if self.is_whited_out(path) {
643            return Err(Self::directory_not_found(path));
644        }
645
646        let normalized = Self::normalized(path);
647        let mut directory_exists = false;
648        let mut entries = BTreeSet::new();
649        let upper = self.upper.as_ref();
650        let include_lowers = !Self::marker_exists_in_upper(upper, OverlayMarkerKind::Opaque, path);
651
652        if include_lowers {
653            for lower in self.lowers.iter_mut().rev() {
654                if let Ok(lower_entries) = lower.read_dir(path) {
655                    directory_exists = true;
656                    for entry in lower_entries {
657                        if entry == "."
658                            || entry == ".."
659                            || Self::should_hide_directory_entry(path, &entry)
660                        {
661                            continue;
662                        }
663                        let child_path = if normalized == "/" {
664                            format!("/{entry}")
665                        } else {
666                            format!("{normalized}/{entry}")
667                        };
668                        if !Self::marker_exists_in_upper(
669                            upper,
670                            OverlayMarkerKind::Whiteout,
671                            &child_path,
672                        ) {
673                            entries.insert(entry);
674                        }
675                    }
676                }
677            }
678        }
679
680        if let Some(upper) = self.upper.as_mut() {
681            if let Ok(upper_entries) = upper.read_dir(path) {
682                directory_exists = true;
683                for entry in upper_entries {
684                    if entry == "."
685                        || entry == ".."
686                        || Self::should_hide_directory_entry(path, &entry)
687                    {
688                        continue;
689                    }
690                    entries.insert(entry);
691                }
692            }
693        }
694
695        if !directory_exists {
696            return Err(Self::directory_not_found(path));
697        }
698
699        Ok(entries.into_iter().collect())
700    }
701
702    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
703        if Self::is_internal_metadata_path(path) {
704            return Err(Self::directory_not_found(path));
705        }
706        if self.is_whited_out(path) {
707            return Err(Self::directory_not_found(path));
708        }
709
710        let normalized = Self::normalized(path);
711        let mut directory_exists = false;
712        let mut entries = BTreeSet::new();
713        let upper = self.upper.as_ref();
714        let include_lowers = !Self::marker_exists_in_upper(upper, OverlayMarkerKind::Opaque, path);
715
716        if include_lowers {
717            for lower in self.lowers.iter_mut().rev() {
718                if let Ok(lower_entries) = lower.read_dir(path) {
719                    directory_exists = true;
720                    for entry in lower_entries {
721                        if entry == "."
722                            || entry == ".."
723                            || Self::should_hide_directory_entry(path, &entry)
724                        {
725                            continue;
726                        }
727                        let child_path = if normalized == "/" {
728                            format!("/{entry}")
729                        } else {
730                            format!("{normalized}/{entry}")
731                        };
732                        if !Self::marker_exists_in_upper(
733                            upper,
734                            OverlayMarkerKind::Whiteout,
735                            &child_path,
736                        ) {
737                            entries.insert(entry);
738                            if entries.len() > max_entries {
739                                return Err(VfsError::new(
740                                    "ENOMEM",
741                                    format!(
742                                        "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
743                                    ),
744                                ));
745                            }
746                        }
747                    }
748                }
749            }
750        }
751
752        if let Some(upper) = self.upper.as_mut() {
753            if let Ok(upper_entries) = upper.read_dir(path) {
754                directory_exists = true;
755                for entry in upper_entries {
756                    if entry == "."
757                        || entry == ".."
758                        || Self::should_hide_directory_entry(path, &entry)
759                    {
760                        continue;
761                    }
762                    entries.insert(entry);
763                    if entries.len() > max_entries {
764                        return Err(VfsError::new(
765                            "ENOMEM",
766                            format!(
767                                "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
768                            ),
769                        ));
770                    }
771                }
772            }
773        }
774
775        if !directory_exists {
776            return Err(Self::directory_not_found(path));
777        }
778
779        Ok(entries.into_iter().collect())
780    }
781
782    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
783        if Self::is_internal_metadata_path(path) {
784            return Err(Self::directory_not_found(path));
785        }
786        if self.is_whited_out(path) {
787            return Err(Self::directory_not_found(path));
788        }
789
790        let normalized = Self::normalized(path);
791        let mut directory_exists = false;
792        let mut entries = Vec::<VirtualDirEntry>::new();
793        let mut seen = BTreeSet::<String>::new();
794        let upper = self.upper.as_ref();
795        let include_lowers = !Self::marker_exists_in_upper(upper, OverlayMarkerKind::Opaque, path);
796
797        if include_lowers {
798            for lower in self.lowers.iter_mut().rev() {
799                if let Ok(lower_entries) = lower.read_dir_with_types(path) {
800                    directory_exists = true;
801                    for entry in lower_entries {
802                        if entry.name == "."
803                            || entry.name == ".."
804                            || Self::should_hide_directory_entry(path, &entry.name)
805                        {
806                            continue;
807                        }
808                        let child_path = if normalized == "/" {
809                            format!("/{}", entry.name)
810                        } else {
811                            format!("{normalized}/{}", entry.name)
812                        };
813                        if Self::marker_exists_in_upper(
814                            upper,
815                            OverlayMarkerKind::Whiteout,
816                            &child_path,
817                        ) || seen.contains(&entry.name)
818                        {
819                            continue;
820                        }
821                        seen.insert(entry.name.clone());
822                        entries.push(entry);
823                    }
824                }
825            }
826        }
827
828        if let Some(upper) = self.upper.as_mut() {
829            if let Ok(upper_entries) = upper.read_dir_with_types(path) {
830                directory_exists = true;
831                for entry in upper_entries {
832                    if entry.name == "."
833                        || entry.name == ".."
834                        || Self::should_hide_directory_entry(path, &entry.name)
835                    {
836                        continue;
837                    }
838                    if let Some(index) = entries
839                        .iter()
840                        .position(|existing| existing.name == entry.name)
841                    {
842                        entries[index] = entry;
843                    } else {
844                        seen.insert(entry.name.clone());
845                        entries.push(entry);
846                    }
847                }
848            }
849        }
850
851        if !directory_exists {
852            return Err(Self::directory_not_found(path));
853        }
854
855        Ok(entries)
856    }
857
858    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
859        if Self::is_internal_metadata_path(path) {
860            return Err(VfsError::permission_denied("open", path));
861        }
862        self.clear_path_metadata(path)?;
863        if self.find_lower_by_entry(path).is_some() {
864            self.copy_up_path(path)?;
865        } else {
866            self.ensure_ancestor_directories_in_upper(path)?;
867        }
868        self.writable_upper(path)?.write_file(path, content.into())
869    }
870
871    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
872        if Self::is_internal_metadata_path(path) {
873            return Err(VfsError::permission_denied("open", path));
874        }
875        self.clear_path_metadata(path)?;
876        if self.path_exists_in_merged_view(path) {
877            return Err(Self::already_exists(path));
878        }
879        self.ensure_ancestor_directories_in_upper(path)?;
880        self.writable_upper(path)?
881            .create_file_exclusive(path, content.into())
882    }
883
884    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
885        if Self::is_internal_metadata_path(path) {
886            return Err(VfsError::permission_denied("open", path));
887        }
888        self.clear_path_metadata(path)?;
889        if self.find_lower_by_entry(path).is_some() {
890            self.copy_up_path(path)?;
891        } else {
892            self.ensure_ancestor_directories_in_upper(path)?;
893        }
894        self.writable_upper(path)?.append_file(path, content.into())
895    }
896
897    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
898        if Self::is_internal_metadata_path(path) {
899            return Err(VfsError::permission_denied("mkdir", path));
900        }
901        self.clear_path_metadata(path)?;
902        if self.path_exists_in_merged_view(path) {
903            return Err(Self::already_exists(path));
904        }
905        self.ensure_ancestor_directories_in_upper(path)?;
906        self.writable_upper(path)?.create_dir(path)
907    }
908
909    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
910        if Self::is_internal_metadata_path(path) {
911            return Err(VfsError::permission_denied("mkdir", path));
912        }
913        self.clear_path_metadata(path)?;
914        if self.path_exists_in_merged_view(path) {
915            let stat = self.merged_lstat(path)?;
916            if recursive && stat.is_directory && !stat.is_symbolic_link {
917                return Ok(());
918            }
919            return Err(Self::already_exists(path));
920        }
921        self.ensure_ancestor_directories_in_upper(path)?;
922        self.writable_upper(path)?.mkdir(path, recursive)
923    }
924
925    fn exists(&self, path: &str) -> bool {
926        if Self::is_internal_metadata_path(path) {
927            return false;
928        }
929        self.path_exists_in_merged_view(path)
930    }
931
932    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
933        if Self::is_internal_metadata_path(path) {
934            return Err(Self::entry_not_found(path));
935        }
936        if self.is_whited_out(path) {
937            return Err(Self::entry_not_found(path));
938        }
939        if self.exists_in_upper(path) {
940            return self
941                .upper
942                .as_mut()
943                .expect("upper must exist when path exists")
944                .stat(path);
945        }
946        let Some(index) = self.find_lower_by_exists(path) else {
947            return Err(Self::entry_not_found(path));
948        };
949        self.lowers[index].stat(path)
950    }
951
952    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
953        if Self::is_internal_metadata_path(path) {
954            return Err(VfsError::permission_denied("unlink", path));
955        }
956        if self.is_whited_out(path) {
957            return Err(Self::entry_not_found(path));
958        }
959        let lower_exists = self.find_lower_by_exists(path).is_some();
960        let upper_exists = self.exists_in_upper(path);
961        if !lower_exists && !upper_exists {
962            return Err(Self::entry_not_found(path));
963        }
964        if upper_exists {
965            self.writable_upper(path)?.remove_file(path)?;
966        } else {
967            self.writable_upper(path)?;
968        }
969        self.clear_opaque_directory(path)?;
970        self.add_whiteout(path)?;
971        Ok(())
972    }
973
974    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
975        let normalized = Self::normalized(path);
976        if Self::is_internal_metadata_path(&normalized) {
977            return Err(VfsError::permission_denied("rmdir", path));
978        }
979        if normalized == "/" {
980            return Err(VfsError::permission_denied("rmdir", path));
981        }
982
983        let stat = match self.merged_lstat(path) {
984            Ok(stat) => stat,
985            Err(error) if error.code() == "ENOENT" => return Err(Self::directory_not_found(path)),
986            Err(error) => return Err(error),
987        };
988
989        if !stat.is_directory || stat.is_symbolic_link {
990            return Err(Self::not_directory(path));
991        }
992
993        if self.directory_has_raw_children(path)? {
994            return Err(Self::not_empty(path));
995        }
996
997        let lower_exists = self.find_lower_by_entry(path).is_some();
998        let upper_exists = self.has_entry_in_upper(path);
999        if upper_exists {
1000            self.writable_upper(path)?.remove_dir(&normalized)?;
1001        } else {
1002            self.writable_upper(path)?;
1003        }
1004        if lower_exists {
1005            self.clear_opaque_directory(path)?;
1006            self.add_whiteout(path)?;
1007        } else {
1008            self.clear_path_metadata(path)?;
1009        }
1010        Ok(())
1011    }
1012
1013    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1014        let old_normalized = Self::normalized(old_path);
1015        let new_normalized = Self::normalized(new_path);
1016        if Self::is_internal_metadata_path(&old_normalized)
1017            || Self::is_internal_metadata_path(&new_normalized)
1018        {
1019            return Err(VfsError::permission_denied("rename", old_path));
1020        }
1021
1022        if old_normalized == "/" {
1023            return Err(VfsError::permission_denied("rename", old_path));
1024        }
1025
1026        if old_normalized == new_normalized {
1027            return Ok(());
1028        }
1029
1030        let source_stat = self.merged_lstat(old_path)?;
1031        if source_stat.is_directory && new_normalized.starts_with(&(old_normalized.clone() + "/")) {
1032            return Err(VfsError::new(
1033                "EINVAL",
1034                format!(
1035                    "cannot move '{}' into its own descendant '{}'",
1036                    old_path, new_path
1037                ),
1038            ));
1039        }
1040
1041        let mut snapshot_entries = Vec::new();
1042        self.collect_snapshot_entries(&old_normalized, &mut snapshot_entries)?;
1043
1044        if let Ok(destination_stat) = self.merged_lstat(&new_normalized) {
1045            if destination_stat.is_directory
1046                && !destination_stat.is_symbolic_link
1047                && !self.read_dir(&new_normalized)?.is_empty()
1048            {
1049                return Err(Self::not_empty(&new_normalized));
1050            }
1051
1052            if self.has_entry_in_upper(&new_normalized) {
1053                if destination_stat.is_directory && !destination_stat.is_symbolic_link {
1054                    self.writable_upper(&new_normalized)?
1055                        .remove_dir(&new_normalized)?;
1056                } else {
1057                    self.writable_upper(&new_normalized)?
1058                        .remove_file(&new_normalized)?;
1059                }
1060            }
1061            self.clear_subtree_metadata(&new_normalized)?;
1062        }
1063
1064        self.stage_snapshot_entries_in_upper(&snapshot_entries)?;
1065        self.copy_subtree_metadata(&old_normalized, &new_normalized)?;
1066        self.writable_upper(&old_normalized)?
1067            .rename(&old_normalized, &new_normalized)?;
1068        self.remove_snapshot_entries(&snapshot_entries)
1069    }
1070
1071    fn realpath(&self, path: &str) -> VfsResult<String> {
1072        if Self::is_internal_metadata_path(path) {
1073            return Err(Self::entry_not_found(path));
1074        }
1075        if self.is_whited_out(path) {
1076            return Err(Self::entry_not_found(path));
1077        }
1078        if self.exists_in_upper(path) {
1079            return self
1080                .upper
1081                .as_ref()
1082                .expect("upper must exist when path exists")
1083                .realpath(path);
1084        }
1085        let Some(index) = self.find_lower_by_exists(path) else {
1086            return Err(Self::entry_not_found(path));
1087        };
1088        self.lowers[index].realpath(path)
1089    }
1090
1091    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
1092        if Self::is_internal_metadata_path(link_path) {
1093            return Err(VfsError::permission_denied("symlink", link_path));
1094        }
1095        self.clear_path_metadata(link_path)?;
1096        self.ensure_ancestor_directories_in_upper(link_path)?;
1097        self.writable_upper(link_path)?.symlink(target, link_path)
1098    }
1099
1100    fn read_link(&self, path: &str) -> VfsResult<String> {
1101        if Self::is_internal_metadata_path(path) {
1102            return Err(Self::entry_not_found(path));
1103        }
1104        if self.is_whited_out(path) {
1105            return Err(Self::entry_not_found(path));
1106        }
1107        if self.has_entry_in_upper(path) {
1108            return self
1109                .upper
1110                .as_ref()
1111                .expect("upper must exist when path exists")
1112                .read_link(path);
1113        }
1114        let Some((index, _)) = self.find_lower_by_entry(path) else {
1115            return Err(Self::entry_not_found(path));
1116        };
1117        self.lowers[index].read_link(path)
1118    }
1119
1120    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
1121        if Self::is_internal_metadata_path(path) {
1122            return Err(Self::entry_not_found(path));
1123        }
1124        if self.is_whited_out(path) {
1125            return Err(Self::entry_not_found(path));
1126        }
1127        if self.has_entry_in_upper(path) {
1128            return self
1129                .upper
1130                .as_ref()
1131                .expect("upper must exist when path exists")
1132                .lstat(path);
1133        }
1134        self.find_lower_by_entry(path)
1135            .map(|(_, stat)| stat)
1136            .ok_or_else(|| Self::entry_not_found(path))
1137    }
1138
1139    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1140        if Self::is_internal_metadata_path(old_path) || Self::is_internal_metadata_path(new_path) {
1141            return Err(VfsError::permission_denied("link", new_path));
1142        }
1143        self.clear_path_metadata(new_path)?;
1144        self.copy_up_path(old_path)?;
1145        self.ensure_ancestor_directories_in_upper(new_path)?;
1146        self.writable_upper(new_path)?.link(old_path, new_path)
1147    }
1148
1149    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
1150        if Self::is_internal_metadata_path(path) {
1151            return Err(VfsError::permission_denied("chmod", path));
1152        }
1153        if self.is_whited_out(path) {
1154            return Err(Self::entry_not_found(path));
1155        }
1156        if !self.exists_in_upper(path) {
1157            self.copy_up_path(path)?;
1158        }
1159        self.writable_upper(path)?.chmod(path, mode)
1160    }
1161
1162    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1163        if Self::is_internal_metadata_path(path) {
1164            return Err(VfsError::permission_denied("chown", path));
1165        }
1166        if self.is_whited_out(path) {
1167            return Err(Self::entry_not_found(path));
1168        }
1169        if !self.exists_in_upper(path) {
1170            self.copy_up_path(path)?;
1171        }
1172        self.writable_upper(path)?.chown(path, uid, gid)
1173    }
1174
1175    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
1176        if Self::is_internal_metadata_path(path) {
1177            return Err(VfsError::permission_denied("utime", path));
1178        }
1179        if self.is_whited_out(path) {
1180            return Err(Self::entry_not_found(path));
1181        }
1182        if !self.exists_in_upper(path) {
1183            self.copy_up_path(path)?;
1184        }
1185        self.writable_upper(path)?.utimes(path, atime_ms, mtime_ms)
1186    }
1187
1188    fn utimes_spec(
1189        &mut self,
1190        path: &str,
1191        atime: VirtualUtimeSpec,
1192        mtime: VirtualUtimeSpec,
1193        follow_symlinks: bool,
1194    ) -> VfsResult<()> {
1195        if Self::is_internal_metadata_path(path) {
1196            return Err(VfsError::permission_denied("utime", path));
1197        }
1198        if self.is_whited_out(path) {
1199            return Err(Self::entry_not_found(path));
1200        }
1201        if !self.exists_in_upper(path) {
1202            self.copy_up_path(path)?;
1203        }
1204        self.writable_upper(path)?
1205            .utimes_spec(path, atime, mtime, follow_symlinks)
1206    }
1207
1208    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
1209        if Self::is_internal_metadata_path(path) {
1210            return Err(VfsError::permission_denied("truncate", path));
1211        }
1212        if self.is_whited_out(path) {
1213            return Err(Self::entry_not_found(path));
1214        }
1215        if !self.exists_in_upper(path) {
1216            self.copy_up_path(path)?;
1217        }
1218        self.writable_upper(path)?.truncate(path, length)
1219    }
1220
1221    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
1222        if Self::is_internal_metadata_path(path) {
1223            return Err(Self::entry_not_found(path));
1224        }
1225        if self.is_whited_out(path) {
1226            return Err(Self::entry_not_found(path));
1227        }
1228        if self.exists_in_upper(path) {
1229            return self
1230                .upper
1231                .as_mut()
1232                .expect("upper must exist when path exists")
1233                .pread(path, offset, length);
1234        }
1235        let Some(index) = self.find_lower_by_exists(path) else {
1236            return Err(Self::entry_not_found(path));
1237        };
1238        self.lowers[index].pread(path, offset, length)
1239    }
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244    use super::{OverlayFileSystem, OverlayMode};
1245    use crate::vfs::{MemoryFileSystem, VfsResult, VirtualFileSystem};
1246
1247    #[test]
1248    fn whiteouts_persist_when_overlay_reopens_with_same_upper() {
1249        let mut lower = MemoryFileSystem::new();
1250        lower.mkdir("/data", true).expect("create lower directory");
1251        lower
1252            .write_file("/data/base.txt", b"base".to_vec())
1253            .expect("seed lower file");
1254        let lower_snapshot = lower.snapshot();
1255
1256        let mut overlay = OverlayFileSystem::with_upper(
1257            vec![MemoryFileSystem::from_snapshot(lower_snapshot.clone())],
1258            MemoryFileSystem::new(),
1259        );
1260        overlay
1261            .remove_file("/data/base.txt")
1262            .expect("whiteout lower file");
1263
1264        let upper = overlay.upper.take().expect("overlay upper");
1265        let restored_lower = MemoryFileSystem::from_snapshot(lower_snapshot);
1266        let mut restored = OverlayFileSystem::with_upper(vec![restored_lower], upper);
1267
1268        assert!(!restored.exists("/data/base.txt"));
1269        assert_eq!(
1270            restored.read_dir("/data").expect("read merged directory"),
1271            Vec::<String>::new()
1272        );
1273    }
1274
1275    #[test]
1276    fn copied_up_directories_become_opaque_and_hide_overlay_metadata() {
1277        let mut lower = MemoryFileSystem::new();
1278        lower.mkdir("/data", true).expect("create lower directory");
1279        lower
1280            .write_file("/data/base.txt", b"base".to_vec())
1281            .expect("seed lower file");
1282
1283        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
1284        overlay
1285            .chmod("/data", 0o700)
1286            .expect("copy up lower directory");
1287
1288        assert_eq!(
1289            overlay.read_dir("/data").expect("read opaque directory"),
1290            Vec::<String>::new()
1291        );
1292        let root_entries = overlay.read_dir("/").expect("read root");
1293        assert!(!root_entries
1294            .iter()
1295            .any(|entry| entry == ".agent-os-overlay"));
1296    }
1297
1298    #[test]
1299    fn remove_dir_succeeds_when_only_lower_children_are_whited_out() {
1300        let mut lower = MemoryFileSystem::new();
1301        lower.mkdir("/a", true).expect("create lower directory");
1302        lower
1303            .write_file("/a/c", b"child".to_vec())
1304            .expect("seed lower child");
1305
1306        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
1307        overlay.remove_file("/a/c").expect("whiteout lower child");
1308        overlay
1309            .remove_dir("/a")
1310            .expect("remove merged-empty directory");
1311
1312        assert!(!overlay.exists("/a"));
1313        assert_error_code(overlay.read_dir("/a"), "ENOENT");
1314    }
1315
1316    #[test]
1317    fn remove_dir_still_rejects_visible_children() {
1318        let mut lower = MemoryFileSystem::new();
1319        lower.mkdir("/a", true).expect("create lower directory");
1320        lower
1321            .write_file("/a/c", b"child".to_vec())
1322            .expect("seed lower child");
1323
1324        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
1325        assert_error_code(overlay.remove_dir("/a"), "ENOTEMPTY");
1326        assert!(overlay.exists("/a/c"));
1327    }
1328
1329    fn assert_error_code<T: std::fmt::Debug>(result: VfsResult<T>, expected: &str) {
1330        let error = result.expect_err("expected operation to fail");
1331        assert_eq!(error.code(), expected);
1332    }
1333}