1use std::cell::RefCell;
15use std::collections::BTreeMap;
16use std::path::{Component, Path, PathBuf};
17use std::sync::{Arc, Mutex};
18
19use crate::testbench::tape::{self, TapeRecordKind};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct DiffEntry {
25 pub path: PathBuf,
26 pub kind: DiffKind,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum DiffKind {
31 Added { content: Vec<u8> },
33 Modified { content: Vec<u8> },
35 Deleted,
37}
38
39#[derive(Debug, Clone)]
40enum OverlayEntry {
41 File(Vec<u8>),
42 Deleted,
43 Directory,
44}
45
46#[derive(Debug)]
47pub struct OverlayFs {
48 root: PathBuf,
49 layer: Mutex<BTreeMap<PathBuf, OverlayEntry>>,
50}
51
52impl OverlayFs {
53 pub fn rooted_at(root: impl Into<PathBuf>) -> Self {
54 let root = root.into();
55 let canonical = std::fs::canonicalize(&root).unwrap_or_else(|_| root.clone());
61 Self {
62 root: normalize_logical(&canonical),
63 layer: Mutex::new(BTreeMap::new()),
64 }
65 }
66
67 pub fn root(&self) -> &Path {
68 &self.root
69 }
70
71 fn key(&self, path: &Path) -> PathBuf {
72 canonicalize_for_overlay(path)
73 }
74
75 fn within_root(&self, path: &Path) -> bool {
80 let key = self.key(path);
81 key.starts_with(&self.root)
82 }
83
84 pub fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
85 if !self.within_root(path) {
86 return std::fs::read(path);
87 }
88 let key = self.key(path);
89 let layer = self.layer.lock().expect("overlay layer poisoned");
90 match layer.get(&key) {
91 Some(OverlayEntry::File(bytes)) => Ok(bytes.clone()),
92 Some(OverlayEntry::Deleted) => Err(std::io::Error::new(
93 std::io::ErrorKind::NotFound,
94 format!("overlay: {} was deleted", key.display()),
95 )),
96 Some(OverlayEntry::Directory) => Err(std::io::Error::new(
97 std::io::ErrorKind::IsADirectory,
98 format!("overlay: {} is a directory", key.display()),
99 )),
100 None => std::fs::read(path),
101 }
102 }
103
104 pub fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
105 let bytes = self.read(path)?;
106 String::from_utf8(bytes)
107 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string()))
108 }
109
110 pub(crate) fn bounded_override(
111 &self,
112 path: &Path,
113 offset: u64,
114 limit: usize,
115 ) -> Option<std::io::Result<(Vec<u8>, u64)>> {
116 if !self.within_root(path) {
117 return None;
118 }
119 let key = self.key(path);
120 let layer = self.layer.lock().expect("overlay layer poisoned");
121 match layer.get(&key) {
122 Some(OverlayEntry::File(bytes)) => {
123 let length = bytes.len() as u64;
124 if offset > length {
125 return Some(Err(std::io::Error::new(
126 std::io::ErrorKind::InvalidInput,
127 format!("byte offset {offset} exceeds file length {length}"),
128 )));
129 }
130 let start = offset as usize;
131 let end = start.saturating_add(limit).min(bytes.len());
132 Some(Ok((bytes[start..end].to_vec(), length)))
133 }
134 Some(OverlayEntry::Deleted) => Some(Err(std::io::Error::new(
135 std::io::ErrorKind::NotFound,
136 format!("overlay: {} was deleted", key.display()),
137 ))),
138 Some(OverlayEntry::Directory) => Some(Err(std::io::Error::new(
139 std::io::ErrorKind::IsADirectory,
140 format!("overlay: {} is a directory", key.display()),
141 ))),
142 None => None,
143 }
144 }
145
146 pub fn write(&self, path: &Path, contents: &[u8]) -> std::io::Result<()> {
147 if !self.within_root(path) {
148 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
154 std::fs::create_dir_all(parent)?;
155 }
156 return std::fs::write(path, contents);
157 }
158 let key = self.key(path);
159 let mut layer = self.layer.lock().expect("overlay layer poisoned");
160 layer.insert(key, OverlayEntry::File(contents.to_vec()));
161 Ok(())
162 }
163
164 fn read_for_replace(&self, builtin: &str, path: &Path) -> std::io::Result<Vec<u8>> {
165 if !self.within_root(path) {
166 return crate::stdlib::sandbox::read_for_replace_scoped_at_open(builtin, path);
167 }
168 let key = self.key(path);
169 let layer = self.layer.lock().expect("overlay layer poisoned");
170 match layer.get(&key) {
171 Some(OverlayEntry::File(bytes)) => return Ok(bytes.clone()),
172 Some(OverlayEntry::Deleted) => {
173 return Err(std::io::Error::new(
174 std::io::ErrorKind::NotFound,
175 format!("overlay: {} was deleted", key.display()),
176 ));
177 }
178 Some(OverlayEntry::Directory) => {
179 return Err(std::io::Error::new(
180 std::io::ErrorKind::IsADirectory,
181 format!("overlay: {} is a directory", key.display()),
182 ));
183 }
184 None => {}
185 }
186 drop(layer);
187 crate::stdlib::sandbox::read_for_replace_scoped_at_open(builtin, path)
188 }
189
190 fn replace(
191 &self,
192 builtin: &str,
193 path: &Path,
194 contents: &[u8],
195 durability: crate::atomic_io::AtomicWriteDurability,
196 create_parents: bool,
197 ) -> std::io::Result<crate::atomic_io::AtomicWriteReceipt> {
198 if !self.within_root(path) {
199 return crate::stdlib::sandbox::atomic_replace_scoped_at_open_unlocked(
200 builtin,
201 path,
202 contents,
203 durability,
204 create_parents,
205 );
206 }
207 if !create_parents {
208 if let Some(parent) = path.parent() {
209 if !parent.as_os_str().is_empty() && !self.exists(parent) {
210 return Err(std::io::Error::new(
211 std::io::ErrorKind::NotFound,
212 format!("parent directory for '{}' does not exist", path.display()),
213 ));
214 }
215 }
216 }
217 self.write(path, contents)?;
218 Ok(crate::atomic_io::AtomicWriteReceipt {
219 file_synced: false,
220 namespace_synced: false,
221 })
222 }
223
224 pub fn append(&self, path: &Path, contents: &[u8]) -> std::io::Result<()> {
225 if !self.within_root(path) {
226 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
229 std::fs::create_dir_all(parent)?;
230 }
231 return std::fs::OpenOptions::new()
232 .create(true)
233 .append(true)
234 .open(path)
235 .and_then(|mut file| std::io::Write::write_all(&mut file, contents));
236 }
237 let mut combined = match self.read(path) {
238 Ok(bytes) => bytes,
239 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Vec::new(),
240 Err(err) => return Err(err),
241 };
242 combined.extend_from_slice(contents);
243 self.write(path, &combined)
244 }
245
246 pub fn copy(&self, src: &Path, dst: &Path) -> std::io::Result<u64> {
247 let bytes = self.read(src)?;
248 let len = bytes.len() as u64;
249 self.write(dst, &bytes)?;
250 Ok(len)
251 }
252
253 pub fn rename(&self, src: &Path, dst: &Path) -> std::io::Result<u64> {
254 let len = self.copy(src, dst)?;
255 self.remove_file(src)?;
256 Ok(len)
257 }
258
259 pub fn exists(&self, path: &Path) -> bool {
260 if !self.within_root(path) {
261 return path.exists();
262 }
263 let key = self.key(path);
264 let layer = self.layer.lock().expect("overlay layer poisoned");
265 match layer.get(&key) {
266 Some(OverlayEntry::File(_)) | Some(OverlayEntry::Directory) => true,
267 Some(OverlayEntry::Deleted) => false,
268 None => path.exists(),
269 }
270 }
271
272 pub fn remove_file(&self, path: &Path) -> std::io::Result<()> {
273 if !self.within_root(path) {
274 return std::fs::remove_file(path);
275 }
276 let key = self.key(path);
277 let mut layer = self.layer.lock().expect("overlay layer poisoned");
278 let underlying_present = path.exists();
281 match layer.get(&key) {
282 Some(OverlayEntry::Deleted) => Err(std::io::Error::new(
283 std::io::ErrorKind::NotFound,
284 format!("overlay: {} already deleted", key.display()),
285 )),
286 _ => {
287 layer.retain(|entry_path, _| !entry_path.starts_with(&key) || entry_path == &key);
288 if underlying_present {
289 layer.insert(key, OverlayEntry::Deleted);
290 } else {
291 layer.remove(&key);
292 }
293 Ok(())
294 }
295 }
296 }
297
298 pub fn create_dir(&self, path: &Path) -> std::io::Result<()> {
299 if !self.within_root(path) {
300 return std::fs::create_dir(path);
301 }
302 let key = self.key(path);
303 let mut layer = self.layer.lock().expect("overlay layer poisoned");
304 match layer.get(&key) {
305 Some(OverlayEntry::File(_)) | Some(OverlayEntry::Directory) => {
306 return Err(std::io::Error::new(
307 std::io::ErrorKind::AlreadyExists,
308 format!("overlay: {} already exists", key.display()),
309 ));
310 }
311 Some(OverlayEntry::Deleted) | None => {}
312 }
313 if !matches!(layer.get(&key), Some(OverlayEntry::Deleted)) && path.exists() {
314 return Err(std::io::Error::new(
315 std::io::ErrorKind::AlreadyExists,
316 format!("overlay: {} already exists", key.display()),
317 ));
318 }
319 let parent = key.parent().ok_or_else(|| {
320 std::io::Error::new(
321 std::io::ErrorKind::NotFound,
322 format!("overlay: {} has no parent", key.display()),
323 )
324 })?;
325 match layer.get(parent) {
326 Some(OverlayEntry::Directory) => {}
327 Some(OverlayEntry::File(_)) => {
328 return Err(std::io::Error::new(
329 std::io::ErrorKind::NotADirectory,
330 format!("overlay: {} parent is a file", key.display()),
331 ));
332 }
333 Some(OverlayEntry::Deleted) => {
334 return Err(std::io::Error::new(
335 std::io::ErrorKind::NotFound,
336 format!("overlay: {} parent was deleted", key.display()),
337 ));
338 }
339 None if parent.is_dir() => {}
340 None => {
341 return Err(std::io::Error::new(
342 std::io::ErrorKind::NotFound,
343 format!("overlay: {} parent does not exist", key.display()),
344 ));
345 }
346 }
347 layer.insert(key, OverlayEntry::Directory);
348 Ok(())
349 }
350
351 pub fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
352 if !self.within_root(path) {
353 return std::fs::create_dir_all(path);
354 }
355 let key = self.key(path);
356 let mut layer = self.layer.lock().expect("overlay layer poisoned");
357 if key == self.root {
358 layer.insert(key, OverlayEntry::Directory);
359 return Ok(());
360 }
361 let relative = key.strip_prefix(&self.root).map_err(|_| {
362 std::io::Error::new(
363 std::io::ErrorKind::InvalidInput,
364 format!(
365 "overlay: {} is outside {}",
366 key.display(),
367 self.root.display()
368 ),
369 )
370 })?;
371 let mut current = self.root.clone();
372 for component in relative.components() {
373 current.push(component.as_os_str());
374 layer.insert(current.clone(), OverlayEntry::Directory);
375 }
376 Ok(())
377 }
378
379 pub fn read_dir(&self, path: &Path) -> std::io::Result<Vec<OverlayDirEntry>> {
380 if !self.within_root(path) {
381 let mut entries = Vec::new();
382 for entry in std::fs::read_dir(path)? {
383 let entry = entry?;
384 entries.push(OverlayDirEntry {
385 path: entry.path(),
386 is_dir: entry.file_type().map(|t| t.is_dir()).unwrap_or(false),
387 is_file: entry.file_type().map(|t| t.is_file()).unwrap_or(false),
388 });
389 }
390 return Ok(entries);
391 }
392 let dir_key = self.key(path);
393 let virtual_dir_exists;
394 {
395 let layer = self.layer.lock().expect("overlay layer poisoned");
396 match layer.get(&dir_key) {
397 Some(OverlayEntry::Deleted) => {
398 return Err(std::io::Error::new(
399 std::io::ErrorKind::NotFound,
400 format!("overlay: {} was deleted", dir_key.display()),
401 ));
402 }
403 Some(OverlayEntry::File(_)) => {
404 return Err(std::io::Error::new(
405 std::io::ErrorKind::NotADirectory,
406 format!("overlay: {} is a file", dir_key.display()),
407 ));
408 }
409 Some(OverlayEntry::Directory) => {
410 virtual_dir_exists = true;
411 }
412 None => {
413 virtual_dir_exists = false;
414 }
415 }
416 }
417 let disk_dir_exists = path.exists();
418 let mut entries: BTreeMap<PathBuf, OverlayDirEntry> = BTreeMap::new();
419 if disk_dir_exists {
420 for entry in std::fs::read_dir(path)? {
421 let entry = entry?;
422 let p = entry.path();
423 entries.insert(
424 p.clone(),
425 OverlayDirEntry {
426 path: p,
427 is_dir: entry.file_type().map(|t| t.is_dir()).unwrap_or(false),
428 is_file: entry.file_type().map(|t| t.is_file()).unwrap_or(false),
429 },
430 );
431 }
432 }
433 let layer = self.layer.lock().expect("overlay layer poisoned");
434 for (key, entry) in layer.iter() {
435 if key.parent() != Some(dir_key.as_path()) {
436 continue;
437 }
438 match entry {
439 OverlayEntry::File(_) => {
440 entries.insert(
441 key.clone(),
442 OverlayDirEntry {
443 path: key.clone(),
444 is_dir: false,
445 is_file: true,
446 },
447 );
448 }
449 OverlayEntry::Directory => {
450 entries.insert(
451 key.clone(),
452 OverlayDirEntry {
453 path: key.clone(),
454 is_dir: true,
455 is_file: false,
456 },
457 );
458 }
459 OverlayEntry::Deleted => {
460 entries.remove(key);
461 }
462 }
463 }
464 if entries.is_empty() && !disk_dir_exists && !virtual_dir_exists {
465 return Err(std::io::Error::new(
466 std::io::ErrorKind::NotFound,
467 format!("overlay: {} was not found", dir_key.display()),
468 ));
469 }
470 Ok(entries.into_values().collect())
471 }
472
473 pub fn diff(&self) -> Vec<DiffEntry> {
475 let layer = self.layer.lock().expect("overlay layer poisoned");
476 let mut diff = Vec::new();
477 for (path, entry) in layer.iter() {
478 match entry {
479 OverlayEntry::File(content) => {
480 if path.exists() {
481 let underlying = std::fs::read(path).unwrap_or_default();
482 if &underlying != content {
483 diff.push(DiffEntry {
484 path: path.clone(),
485 kind: DiffKind::Modified {
486 content: content.clone(),
487 },
488 });
489 }
490 } else {
491 diff.push(DiffEntry {
492 path: path.clone(),
493 kind: DiffKind::Added {
494 content: content.clone(),
495 },
496 });
497 }
498 }
499 OverlayEntry::Deleted => {
500 if path.exists() {
501 diff.push(DiffEntry {
502 path: path.clone(),
503 kind: DiffKind::Deleted,
504 });
505 }
506 }
507 OverlayEntry::Directory => {}
508 }
509 }
510 diff
511 }
512
513 pub fn render_unified_diff(&self) -> String {
517 render_unified_diff(&self.diff())
518 }
519}
520
521pub fn render_unified_diff(diff: &[DiffEntry]) -> String {
526 let mut out = String::new();
527 for entry in diff {
528 match &entry.kind {
529 DiffKind::Added { content } => {
530 out.push_str(&format!("--- /dev/null\n+++ b/{}\n", entry.path.display()));
531 push_lines(&mut out, content, '+');
532 }
533 DiffKind::Modified { content } => {
534 let underlying = std::fs::read(&entry.path).unwrap_or_default();
535 out.push_str(&format!(
536 "--- a/{}\n+++ b/{}\n",
537 entry.path.display(),
538 entry.path.display()
539 ));
540 push_lines(&mut out, &underlying, '-');
541 push_lines(&mut out, content, '+');
542 }
543 DiffKind::Deleted => {
544 let underlying = std::fs::read(&entry.path).unwrap_or_default();
545 out.push_str(&format!("--- a/{}\n+++ /dev/null\n", entry.path.display()));
546 push_lines(&mut out, &underlying, '-');
547 }
548 }
549 }
550 out
551}
552
553#[derive(Debug, Clone)]
554pub struct OverlayDirEntry {
555 pub path: PathBuf,
556 pub is_dir: bool,
557 pub is_file: bool,
558}
559
560fn push_lines(out: &mut String, bytes: &[u8], prefix: char) {
561 let text = String::from_utf8_lossy(bytes);
562 for line in text.split_inclusive('\n') {
563 out.push(prefix);
564 out.push_str(line);
565 if !line.ends_with('\n') {
566 out.push('\n');
567 }
568 }
569}
570
571fn normalize_logical(path: &Path) -> PathBuf {
575 let absolute = if path.is_absolute() {
576 path.to_path_buf()
577 } else {
578 std::env::current_dir()
579 .map(|cwd| cwd.join(path))
580 .unwrap_or_else(|_| path.to_path_buf())
581 };
582 let mut out = PathBuf::new();
583 for component in absolute.components() {
584 match component {
585 Component::ParentDir => {
586 out.pop();
587 }
588 Component::CurDir => {}
589 other => out.push(other),
590 }
591 }
592 out
593}
594
595fn canonicalize_for_overlay(path: &Path) -> PathBuf {
601 let absolute = normalize_logical(path);
602 if let Ok(direct) = std::fs::canonicalize(&absolute) {
603 return direct;
604 }
605 let mut suffix = Vec::new();
606 let mut probe = absolute.clone();
607 loop {
608 if let Ok(canon) = std::fs::canonicalize(&probe) {
609 let mut joined = canon;
610 for component in suffix.iter().rev() {
611 joined.push(component);
612 }
613 return joined;
614 }
615 match probe.file_name().map(|n| n.to_owned()) {
616 Some(name) => {
617 suffix.push(name);
618 if !probe.pop() {
619 break;
620 }
621 }
622 None => break,
623 }
624 }
625 absolute
626}
627
628thread_local! {
629 static ACTIVE_OVERLAY: RefCell<Option<Arc<OverlayFs>>> = const { RefCell::new(None) };
630}
631
632pub struct OverlayFsGuard {
633 previous: Option<Arc<OverlayFs>>,
634}
635
636impl Drop for OverlayFsGuard {
637 fn drop(&mut self) {
638 let prev = self.previous.take();
639 ACTIVE_OVERLAY.with(|slot| {
640 *slot.borrow_mut() = prev;
641 });
642 }
643}
644
645pub fn install_overlay(overlay: Arc<OverlayFs>) -> OverlayFsGuard {
646 let previous = ACTIVE_OVERLAY.with(|slot| slot.replace(Some(overlay)));
647 OverlayFsGuard { previous }
648}
649
650pub fn active_overlay() -> Option<Arc<OverlayFs>> {
651 ACTIVE_OVERLAY.with(|slot| slot.borrow().clone())
652}
653
654pub mod helpers {
663 use super::*;
664
665 fn record_file_read(path: &Path, bytes: &[u8]) {
666 if tape::active_recorder().is_none() {
669 return;
670 }
671 let path_str = path.to_string_lossy().into_owned();
672 let len = bytes.len() as u64;
673 let hash = tape::content_hash(bytes);
674 tape::with_active_recorder(|_recorder| {
675 Some(TapeRecordKind::FileRead {
676 path: path_str,
677 content_hash: hash,
678 len_bytes: len,
679 })
680 });
681 }
682
683 fn record_file_write(path: &Path, bytes: &[u8]) {
684 if tape::active_recorder().is_none() {
685 return;
686 }
687 let path_str = path.to_string_lossy().into_owned();
688 let len = bytes.len() as u64;
689 let hash = tape::content_hash(bytes);
690 tape::with_active_recorder(|_recorder| {
691 Some(TapeRecordKind::FileWrite {
692 path: path_str,
693 content_hash: hash,
694 len_bytes: len,
695 })
696 });
697 }
698
699 fn record_file_delete(path: &Path) {
700 if tape::active_recorder().is_none() {
701 return;
702 }
703 let path_str = path.to_string_lossy().into_owned();
704 tape::with_active_recorder(|_recorder| Some(TapeRecordKind::FileDelete { path: path_str }));
705 }
706
707 pub fn read(path: &Path) -> std::io::Result<Vec<u8>> {
708 let result = match active_overlay() {
709 Some(overlay) => overlay.read(path),
710 None => std::fs::read(path),
711 };
712 if let Ok(bytes) = result.as_ref() {
713 record_file_read(path, bytes);
714 }
715 result
716 }
717
718 pub fn read_to_string(path: &Path) -> std::io::Result<String> {
719 let result = match active_overlay() {
720 Some(overlay) => overlay.read_to_string(path),
721 None => std::fs::read_to_string(path),
722 };
723 if let Ok(text) = result.as_ref() {
724 record_file_read(path, text.as_bytes());
725 }
726 result
727 }
728
729 pub fn write(path: &Path, contents: &[u8]) -> std::io::Result<()> {
730 let result = match active_overlay() {
731 Some(overlay) => overlay.write(path, contents),
732 None => crate::atomic_io::atomic_write(path, contents),
733 };
734 if result.is_ok() {
735 record_file_write(path, contents);
736 }
737 result
738 }
739
740 pub fn write_scoped(builtin: &str, path: &Path, contents: &[u8]) -> std::io::Result<()> {
741 let result = match active_overlay() {
742 Some(overlay) => overlay.write(path, contents),
743 None => crate::stdlib::sandbox::atomic_write_scoped_at_open(builtin, path, contents),
744 };
745 if result.is_ok() {
746 record_file_write(path, contents);
747 }
748 result
749 }
750
751 pub fn replace_scoped(
752 builtin: &str,
753 path: &Path,
754 contents: &[u8],
755 options: &crate::conditional_replace::ConditionalReplaceOptions,
756 ) -> std::io::Result<crate::conditional_replace::ConditionalReplaceReceipt> {
757 let receipt = match active_overlay() {
758 Some(overlay) => crate::conditional_replace::conditional_replace_with_io(
759 path,
760 contents,
761 options,
762 |candidate| overlay.read_for_replace(builtin, candidate),
763 |candidate, bytes, durability, create_parents| {
764 overlay.replace(builtin, candidate, bytes, durability, create_parents)
765 },
766 || {},
767 ),
768 None => crate::conditional_replace::conditional_replace_with_io(
769 path,
770 contents,
771 options,
772 |candidate| {
773 crate::stdlib::sandbox::read_for_replace_scoped_at_open(builtin, candidate)
774 },
775 |candidate, bytes, durability, create_parents| {
776 crate::stdlib::sandbox::atomic_replace_scoped_at_open_unlocked(
777 builtin,
778 candidate,
779 bytes,
780 durability,
781 create_parents,
782 )
783 },
784 || {},
785 ),
786 }?;
787 if matches!(
788 receipt.status,
789 crate::conditional_replace::ConditionalReplaceStatus::Created
790 | crate::conditional_replace::ConditionalReplaceStatus::Replaced
791 ) {
792 record_file_write(path, contents);
793 }
794 Ok(receipt)
795 }
796
797 pub fn append(path: &Path, contents: &[u8]) -> std::io::Result<()> {
798 let result = match active_overlay() {
799 Some(overlay) => overlay.append(path, contents),
800 None => std::fs::OpenOptions::new()
801 .create(true)
802 .append(true)
803 .open(path)
804 .and_then(|mut file| std::io::Write::write_all(&mut file, contents)),
805 };
806 if result.is_ok() {
807 record_file_write(path, contents);
808 }
809 result
810 }
811
812 pub fn append_scoped(builtin: &str, path: &Path, contents: &[u8]) -> std::io::Result<()> {
813 let result = match active_overlay() {
814 Some(overlay) => overlay.append(path, contents),
815 None => crate::stdlib::sandbox::append_scoped_at_open(builtin, path, contents),
816 };
817 if result.is_ok() {
818 record_file_write(path, contents);
819 }
820 result
821 }
822
823 pub(crate) fn append_locked_scoped(
824 builtin: &str,
825 path: &Path,
826 contents: &[u8],
827 options: crate::stdlib::sandbox::AppendLockOptions,
828 ) -> std::io::Result<()> {
829 let result = match active_overlay() {
830 Some(overlay) => overlay.append(path, contents),
831 None => crate::stdlib::sandbox::append_locked_scoped_at_open(
832 builtin, path, contents, options,
833 ),
834 };
835 if result.is_ok() {
836 record_file_write(path, contents);
837 }
838 result
839 }
840
841 pub fn copy(src: &Path, dst: &Path) -> std::io::Result<u64> {
842 match active_overlay() {
843 Some(overlay) => {
844 let result = overlay.copy(src, dst);
845 if let Ok(bytes) = overlay.read(src) {
846 record_file_read(src, &bytes);
847 if result.is_ok() {
848 record_file_write(dst, &bytes);
849 }
850 }
851 result
852 }
853 None => {
854 let copied = std::fs::copy(src, dst)?;
855 if tape::active_recorder().is_some() {
856 let bytes = std::fs::read(dst)?;
857 record_file_read(src, &bytes);
858 record_file_write(dst, &bytes);
859 }
860 Ok(copied)
861 }
862 }
863 }
864
865 pub fn copy_scoped(builtin: &str, src: &Path, dst: &Path) -> std::io::Result<u64> {
866 match active_overlay() {
867 Some(overlay) => {
868 let result = overlay.copy(src, dst);
869 if let Ok(bytes) = overlay.read(src) {
870 record_file_read(src, &bytes);
871 if result.is_ok() {
872 record_file_write(dst, &bytes);
873 }
874 }
875 result
876 }
877 None => {
878 let copied = crate::stdlib::sandbox::copy_scoped_at_open(builtin, src, dst)?;
879 if tape::active_recorder().is_some() {
880 let bytes = std::fs::read(dst)?;
881 record_file_read(src, &bytes);
882 record_file_write(dst, &bytes);
883 }
884 Ok(copied)
885 }
886 }
887 }
888
889 pub fn rename(src: &Path, dst: &Path) -> std::io::Result<u64> {
890 match active_overlay() {
891 Some(overlay) => {
892 let bytes_for_record = overlay.read(src).ok();
893 let result = overlay.rename(src, dst);
894 if result.is_ok() {
895 if let Some(bytes) = bytes_for_record.as_deref() {
896 record_file_read(src, bytes);
897 record_file_write(dst, bytes);
898 record_file_delete(src);
899 }
900 }
901 result
902 }
903 None => {
904 let bytes = tape::active_recorder()
905 .is_some()
906 .then(|| std::fs::read(src))
907 .transpose()?;
908 let len = bytes
909 .as_ref()
910 .map(|bytes| bytes.len() as u64)
911 .or_else(|| std::fs::metadata(src).ok().map(|metadata| metadata.len()))
912 .unwrap_or(0);
913 std::fs::rename(src, dst)?;
914 if let Some(bytes) = bytes.as_deref() {
915 record_file_read(src, bytes);
916 record_file_write(dst, bytes);
917 record_file_delete(src);
918 }
919 Ok(len)
920 }
921 }
922 }
923
924 pub fn rename_scoped(builtin: &str, src: &Path, dst: &Path) -> std::io::Result<u64> {
925 match active_overlay() {
926 Some(overlay) => {
927 let bytes_for_record = overlay.read(src).ok();
928 let result = overlay.rename(src, dst);
929 if result.is_ok() {
930 if let Some(bytes) = bytes_for_record.as_deref() {
931 record_file_read(src, bytes);
932 record_file_write(dst, bytes);
933 record_file_delete(src);
934 }
935 }
936 result
937 }
938 None => {
939 let bytes = tape::active_recorder()
940 .is_some()
941 .then(|| std::fs::read(src))
942 .transpose()?;
943 let len = bytes
944 .as_ref()
945 .map(|bytes| bytes.len() as u64)
946 .or_else(|| std::fs::metadata(src).ok().map(|metadata| metadata.len()))
947 .unwrap_or(0);
948 crate::stdlib::sandbox::rename_scoped_at_open(builtin, src, dst)?;
949 if let Some(bytes) = bytes.as_deref() {
950 record_file_read(src, bytes);
951 record_file_write(dst, bytes);
952 record_file_delete(src);
953 }
954 Ok(len)
955 }
956 }
957 }
958
959 pub fn exists(path: &Path) -> bool {
960 match active_overlay() {
961 Some(overlay) => overlay.exists(path),
962 None => path.exists(),
963 }
964 }
965
966 pub fn remove_file(path: &Path) -> std::io::Result<()> {
967 let result = match active_overlay() {
968 Some(overlay) => overlay.remove_file(path),
969 None => std::fs::remove_file(path),
970 };
971 if result.is_ok() {
972 record_file_delete(path);
973 }
974 result
975 }
976
977 pub fn create_dir_all(path: &Path) -> std::io::Result<()> {
978 match active_overlay() {
979 Some(overlay) => overlay.create_dir_all(path),
980 None => std::fs::create_dir_all(path),
981 }
982 }
983
984 pub fn create_dir(path: &Path) -> std::io::Result<()> {
985 match active_overlay() {
986 Some(overlay) => overlay.create_dir(path),
987 None => std::fs::create_dir(path),
988 }
989 }
990
991 pub fn create_dir_scoped(builtin: &str, path: &Path, recursive: bool) -> std::io::Result<()> {
992 match active_overlay() {
993 Some(overlay) if recursive => overlay.create_dir_all(path),
994 Some(overlay) => overlay.create_dir(path),
995 None => crate::stdlib::sandbox::create_dir_scoped_at_open(builtin, path, recursive),
996 }
997 }
998
999 pub fn read_dir(path: &Path) -> std::io::Result<Vec<OverlayDirEntry>> {
1000 match active_overlay() {
1001 Some(overlay) => overlay.read_dir(path),
1002 None => {
1003 let mut entries = Vec::new();
1004 for entry in std::fs::read_dir(path)? {
1005 let entry = entry?;
1006 let file_type = entry.file_type()?;
1007 entries.push(OverlayDirEntry {
1008 path: entry.path(),
1009 is_dir: file_type.is_dir(),
1010 is_file: file_type.is_file(),
1011 });
1012 }
1013 Ok(entries)
1014 }
1015 }
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022
1023 #[test]
1024 fn writes_land_in_overlay_only() {
1025 let dir = tempfile::tempdir().unwrap();
1026 let overlay = OverlayFs::rooted_at(dir.path());
1027 overlay.write(&dir.path().join("hello.txt"), b"hi").unwrap();
1028 assert!(!dir.path().join("hello.txt").exists());
1030 assert_eq!(
1032 overlay
1033 .read_to_string(&dir.path().join("hello.txt"))
1034 .unwrap(),
1035 "hi"
1036 );
1037 }
1038
1039 #[test]
1040 fn reads_pass_through_to_underlying_tree() {
1041 let dir = tempfile::tempdir().unwrap();
1042 std::fs::write(dir.path().join("seed.txt"), "underlying").unwrap();
1043 let overlay = OverlayFs::rooted_at(dir.path());
1044 assert_eq!(
1045 overlay
1046 .read_to_string(&dir.path().join("seed.txt"))
1047 .unwrap(),
1048 "underlying"
1049 );
1050 }
1051
1052 #[test]
1053 fn delete_masks_underlying_file() {
1054 let dir = tempfile::tempdir().unwrap();
1055 std::fs::write(dir.path().join("doomed.txt"), "x").unwrap();
1056 let overlay = OverlayFs::rooted_at(dir.path());
1057 overlay.remove_file(&dir.path().join("doomed.txt")).unwrap();
1058 assert!(!overlay.exists(&dir.path().join("doomed.txt")));
1059 assert!(dir.path().join("doomed.txt").exists());
1061 let diff = overlay.diff();
1062 assert_eq!(diff.len(), 1);
1063 assert!(matches!(diff[0].kind, DiffKind::Deleted));
1064 }
1065
1066 #[test]
1067 fn delete_masks_underlying_directory_contents() {
1068 let dir = tempfile::tempdir().unwrap();
1069 let nested = dir.path().join("doomed");
1070 std::fs::create_dir_all(&nested).unwrap();
1071 std::fs::write(nested.join("secret.txt"), "x").unwrap();
1072 let overlay = OverlayFs::rooted_at(dir.path());
1073
1074 overlay.remove_file(&nested).unwrap();
1075
1076 assert!(!overlay.exists(&nested));
1077 assert_eq!(
1078 overlay.read_dir(&nested).unwrap_err().kind(),
1079 std::io::ErrorKind::NotFound
1080 );
1081 assert!(nested.join("secret.txt").exists());
1082 }
1083
1084 #[test]
1085 fn recursive_mkdir_creates_visible_overlay_ancestors() {
1086 let dir = tempfile::tempdir().unwrap();
1087 let overlay = OverlayFs::rooted_at(dir.path());
1088 overlay
1089 .create_dir_all(&dir.path().join("alpha/beta/gamma"))
1090 .unwrap();
1091
1092 let root_entries = overlay.read_dir(&dir.path().join("alpha")).unwrap();
1093 assert_eq!(root_entries.len(), 1);
1094 assert_eq!(
1095 root_entries[0]
1096 .path
1097 .file_name()
1098 .and_then(|name| name.to_str()),
1099 Some("beta")
1100 );
1101 assert!(root_entries[0].is_dir);
1102 }
1103
1104 #[test]
1105 fn read_dir_reports_missing_empty_overlay_path() {
1106 let dir = tempfile::tempdir().unwrap();
1107 let overlay = OverlayFs::rooted_at(dir.path());
1108
1109 assert_eq!(
1110 overlay
1111 .read_dir(&dir.path().join("missing"))
1112 .unwrap_err()
1113 .kind(),
1114 std::io::ErrorKind::NotFound
1115 );
1116 }
1117
1118 #[test]
1121 fn no_overlay_write_replaces_content() {
1122 let dir = tempfile::tempdir().unwrap();
1123 let target = dir.path().join("important.txt");
1124 std::fs::write(&target, "ORIGINAL IMPORTANT CONTENT").unwrap();
1125 assert!(active_overlay().is_none(), "no overlay should be installed");
1126
1127 helpers::write(&target, b"NEW CONTENT").unwrap();
1128
1129 assert_eq!(std::fs::read_to_string(&target).unwrap(), "NEW CONTENT");
1130 let leftovers: Vec<_> = std::fs::read_dir(dir.path())
1132 .unwrap()
1133 .filter_map(|e| e.ok())
1134 .map(|e| e.file_name().to_string_lossy().into_owned())
1135 .filter(|n| n.contains("harn-tmp"))
1136 .collect();
1137 assert!(
1138 leftovers.is_empty(),
1139 "temp files left behind: {leftovers:?}"
1140 );
1141 }
1142
1143 #[cfg(unix)]
1156 #[test]
1157 fn no_overlay_write_failure_preserves_original() {
1158 use std::os::unix::fs::PermissionsExt;
1159
1160 let dir = tempfile::tempdir().unwrap();
1161 let target = dir.path().join("important.txt");
1162 std::fs::write(&target, "ORIGINAL IMPORTANT CONTENT").unwrap();
1163
1164 let mut perms = std::fs::metadata(dir.path()).unwrap().permissions();
1167 perms.set_mode(0o500);
1168 std::fs::set_permissions(dir.path(), perms).unwrap();
1169
1170 let result = helpers::write(&target, b"NEW CONTENT");
1171
1172 let mut restore = std::fs::metadata(dir.path()).unwrap().permissions();
1174 restore.set_mode(0o700);
1175 std::fs::set_permissions(dir.path(), restore).unwrap();
1176
1177 assert_eq!(
1179 std::fs::read_to_string(&target).unwrap(),
1180 "ORIGINAL IMPORTANT CONTENT",
1181 "a write that cannot complete must not truncate or corrupt the original file"
1182 );
1183 assert!(
1186 result.is_err(),
1187 "atomic write should report failure when it cannot create its temp file"
1188 );
1189 }
1190
1191 #[test]
1192 fn diff_distinguishes_added_vs_modified() {
1193 let dir = tempfile::tempdir().unwrap();
1194 std::fs::write(dir.path().join("existing.txt"), "v1").unwrap();
1195 let overlay = OverlayFs::rooted_at(dir.path());
1196 overlay
1197 .write(&dir.path().join("existing.txt"), b"v2")
1198 .unwrap();
1199 overlay
1200 .write(&dir.path().join("brand-new.txt"), b"hi")
1201 .unwrap();
1202 let mut diff = overlay.diff();
1203 diff.sort_by(|a, b| a.path.cmp(&b.path));
1204 assert_eq!(diff.len(), 2);
1205 assert!(matches!(diff[0].kind, DiffKind::Added { .. }));
1206 assert!(matches!(diff[1].kind, DiffKind::Modified { .. }));
1207 }
1208}