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(¤t) {
313 continue;
314 }
315
316 if let Some(index) = self.find_lower_by_exists(¤t) {
317 let stat = self.lowers[index].stat(¤t)?;
318 if !stat.is_directory {
319 return Err(Self::not_directory(¤t));
320 }
321
322 let upper = self.writable_upper(¤t)?;
323 upper.mkdir(¤t, false)?;
324 upper.chmod(¤t, stat.mode)?;
325 upper.chown(¤t, stat.uid, stat.gid)?;
326 continue;
327 }
328
329 let upper = self.writable_upper(¤t)?;
330 upper.mkdir(¤t, 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(¤t_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(¤t_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(¤t_path)?;
418 for entry in children.into_iter().rev() {
419 pending.push((Self::join_path(¤t_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(¤t_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}