1#![allow(clippy::unwrap_used)]
14
15use async_trait::async_trait;
16use std::collections::HashSet;
17use std::io::{Error as IoError, ErrorKind};
18use std::path::{Path, PathBuf};
19use std::sync::{Arc, RwLock};
20
21use super::limits::{FsLimits, FsUsage};
22use super::memory::InMemoryFs;
23use super::traits::{DirEntry, FileSystem, FileType, Metadata};
24use crate::error::Result;
25
26pub struct OverlayFs {
143 lower: Arc<dyn FileSystem>,
145 upper: InMemoryFs,
147 whiteouts: RwLock<HashSet<PathBuf>>,
149 limits: FsLimits,
151 lower_hidden: RwLock<FsUsage>,
154}
155
156impl OverlayFs {
157 pub fn new(lower: Arc<dyn FileSystem>) -> Self {
194 Self::with_limits(lower, FsLimits::default())
195 }
196
197 pub fn with_limits(lower: Arc<dyn FileSystem>, limits: FsLimits) -> Self {
218 Self {
220 lower,
221 upper: InMemoryFs::with_limits(FsLimits::unlimited()),
222 whiteouts: RwLock::new(HashSet::new()),
223 limits,
224 lower_hidden: RwLock::new(FsUsage::default()),
225 }
226 }
227
228 pub fn upper(&self) -> &InMemoryFs {
246 &self.upper
247 }
248
249 fn compute_usage(&self) -> FsUsage {
255 let upper_usage = self.upper.usage();
256 let lower_usage = self.lower.usage();
257 let hidden = self.lower_hidden.read().unwrap();
258
259 let total_bytes = upper_usage
260 .total_bytes
261 .saturating_add(lower_usage.total_bytes)
262 .saturating_sub(hidden.total_bytes);
263 let file_count = upper_usage
264 .file_count
265 .saturating_add(lower_usage.file_count)
266 .saturating_sub(hidden.file_count);
267 let dir_count = upper_usage
268 .dir_count
269 .saturating_add(lower_usage.dir_count)
270 .saturating_sub(hidden.dir_count);
271
272 FsUsage::new(total_bytes, file_count, dir_count)
273 }
274
275 fn hide_lower_file(&self, size: u64) {
277 let mut h = self.lower_hidden.write().unwrap();
278 h.total_bytes = h.total_bytes.saturating_add(size);
279 h.file_count = h.file_count.saturating_add(1);
280 }
281
282 fn hide_lower_dir(&self) {
284 let mut h = self.lower_hidden.write().unwrap();
285 h.dir_count = h.dir_count.saturating_add(1);
286 }
287
288 async fn hide_lower_children_recursive(&self, dir: &Path) {
291 if let Ok(entries) = self.lower.read_dir(dir).await {
292 for entry in entries {
293 let child = dir.join(&entry.name);
294 if let Ok(meta) = self.lower.stat(&child).await {
295 match meta.file_type {
296 FileType::File => self.hide_lower_file(meta.size),
297 FileType::Directory => {
298 self.hide_lower_dir();
299 Box::pin(self.hide_lower_children_recursive(&child)).await;
301 }
302 _ => {}
303 }
304 }
305 }
306 }
307 }
308
309 fn check_write_limits(&self, content_size: usize) -> Result<()> {
311 if content_size as u64 > self.limits.max_file_size {
313 return Err(IoError::other(format!(
314 "file too large: {} bytes exceeds {} byte limit",
315 content_size, self.limits.max_file_size
316 ))
317 .into());
318 }
319
320 let usage = self.compute_usage();
323 let new_total = usage.total_bytes + content_size as u64;
324 if new_total > self.limits.max_total_bytes {
325 return Err(IoError::other(format!(
326 "filesystem full: {} bytes would exceed {} byte limit",
327 new_total, self.limits.max_total_bytes
328 ))
329 .into());
330 }
331
332 if usage.file_count >= self.limits.max_file_count {
334 return Err(IoError::other(format!(
335 "too many files: {} files at {} file limit",
336 usage.file_count, self.limits.max_file_count
337 ))
338 .into());
339 }
340
341 Ok(())
342 }
343
344 fn normalize_path(path: &Path) -> PathBuf {
346 let mut result = PathBuf::new();
347
348 for component in path.components() {
349 match component {
350 std::path::Component::RootDir => {
351 result.push("/");
352 }
353 std::path::Component::Normal(name) => {
354 result.push(name);
355 }
356 std::path::Component::ParentDir => {
357 result.pop();
358 }
359 std::path::Component::CurDir => {}
360 std::path::Component::Prefix(_) => {}
361 }
362 }
363
364 if result.as_os_str().is_empty() {
365 result.push("/");
366 }
367
368 result
369 }
370
371 fn is_whiteout(&self, path: &Path) -> bool {
373 let path = Self::normalize_path(path);
374 let whiteouts = self.whiteouts.read().unwrap();
375 let mut check = path.as_path();
378 loop {
379 if whiteouts.contains(check) {
380 return true;
381 }
382 match check.parent() {
383 Some(p) if p != check => check = p,
384 _ => break,
385 }
386 }
387 false
388 }
389
390 fn add_whiteout(&self, path: &Path) {
392 let path = Self::normalize_path(path);
393 let mut whiteouts = self.whiteouts.write().unwrap();
394 whiteouts.insert(path);
395 }
396
397 fn remove_whiteout(&self, path: &Path) {
399 let path = Self::normalize_path(path);
400 let mut whiteouts = self.whiteouts.write().unwrap();
401 whiteouts.remove(&path);
402 }
403}
404
405#[async_trait]
406impl FileSystem for OverlayFs {
407 async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
408 let path = Self::normalize_path(path);
409
410 if self.is_whiteout(&path) {
412 return Err(IoError::new(ErrorKind::NotFound, "file not found").into());
413 }
414
415 if self.upper.exists(&path).await.unwrap_or(false) {
417 return self.upper.read_file(&path).await;
418 }
419
420 self.lower.read_file(&path).await
422 }
423
424 async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
425 self.limits
427 .validate_path(path)
428 .map_err(|e| IoError::other(e.to_string()))?;
429
430 let path = Self::normalize_path(path);
431
432 self.check_write_limits(content.len())?;
434
435 let already_in_upper = self.upper.exists(&path).await.unwrap_or(false);
439 let already_whited = self.is_whiteout(&path);
440 let lower_exists = self.lower.exists(&path).await.unwrap_or(false);
441
442 self.remove_whiteout(&path);
444
445 if let Some(parent) = path.parent() {
447 if !self.upper.exists(parent).await.unwrap_or(false) {
448 if self.lower.exists(parent).await.unwrap_or(false) {
450 self.upper.mkdir(parent, true).await?;
451 } else {
452 return Err(
453 IoError::new(ErrorKind::NotFound, "parent directory not found").into(),
454 );
455 }
456 }
457 }
458
459 self.upper.write_file(&path, content).await?;
461
462 if lower_exists && !already_in_upper && !already_whited {
465 if let Ok(meta) = self.lower.stat(&path).await {
466 match meta.file_type {
467 FileType::File => self.hide_lower_file(meta.size),
468 FileType::Directory => self.hide_lower_dir(),
469 _ => {}
470 }
471 }
472 }
473
474 Ok(())
475 }
476
477 async fn append_file(&self, path: &Path, content: &[u8]) -> Result<()> {
478 self.limits
480 .validate_path(path)
481 .map_err(|e| IoError::other(e.to_string()))?;
482
483 let path = Self::normalize_path(path);
484
485 if self.is_whiteout(&path) {
487 return Err(IoError::new(ErrorKind::NotFound, "file not found").into());
488 }
489
490 if self.upper.exists(&path).await.unwrap_or(false) {
492 self.check_write_limits(content.len())?;
494 return self.upper.append_file(&path, content).await;
495 }
496
497 if self.lower.exists(&path).await.unwrap_or(false) {
499 let lower_meta = self.lower.stat(&path).await?;
500 let existing = self.lower.read_file(&path).await?;
501
502 self.check_write_limits(existing.len() + content.len())?;
504
505 if let Some(parent) = path.parent() {
507 if !self.upper.exists(parent).await.unwrap_or(false) {
508 self.upper.mkdir(parent, true).await?;
509 }
510 }
511
512 let mut combined = existing;
514 combined.extend_from_slice(content);
515 self.upper.write_file(&path, &combined).await?;
516
517 self.hide_lower_file(lower_meta.size);
519 return Ok(());
520 }
521
522 self.check_write_limits(content.len())?;
524 self.upper.write_file(&path, content).await
525 }
526
527 async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()> {
528 self.limits
530 .validate_path(path)
531 .map_err(|e| IoError::other(e.to_string()))?;
532
533 let path = Self::normalize_path(path);
534
535 self.remove_whiteout(&path);
537
538 self.upper.mkdir(&path, recursive).await
540 }
541
542 async fn remove(&self, path: &Path, recursive: bool) -> Result<()> {
543 let path = Self::normalize_path(path);
544
545 let in_upper = self.upper.exists(&path).await.unwrap_or(false);
547 let in_lower = !self.is_whiteout(&path) && self.lower.exists(&path).await.unwrap_or(false);
548
549 if !in_upper && !in_lower {
550 return Err(IoError::new(ErrorKind::NotFound, "not found").into());
551 }
552
553 if in_upper {
555 self.upper.remove(&path, recursive).await?;
556 }
557
558 if in_lower {
563 if !in_upper {
565 if let Ok(meta) = self.lower.stat(&path).await {
566 match meta.file_type {
567 FileType::File => self.hide_lower_file(meta.size),
568 FileType::Directory => {
569 self.hide_lower_dir();
570 if recursive {
573 self.hide_lower_children_recursive(&path).await;
574 }
575 }
576 _ => {}
577 }
578 }
579 }
580 self.add_whiteout(&path);
581 }
582
583 Ok(())
584 }
585
586 async fn stat(&self, path: &Path) -> Result<Metadata> {
587 let path = Self::normalize_path(path);
588
589 if self.is_whiteout(&path) {
591 return Err(IoError::new(ErrorKind::NotFound, "not found").into());
592 }
593
594 if self.upper.exists(&path).await.unwrap_or(false) {
596 return self.upper.stat(&path).await;
597 }
598
599 self.lower.stat(&path).await
601 }
602
603 async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
604 let path = Self::normalize_path(path);
605
606 if self.is_whiteout(&path) {
608 return Err(IoError::new(ErrorKind::NotFound, "not found").into());
609 }
610
611 let mut entries: std::collections::HashMap<String, DirEntry> =
612 std::collections::HashMap::new();
613
614 if self.lower.exists(&path).await.unwrap_or(false) {
616 if let Ok(lower_entries) = self.lower.read_dir(&path).await {
617 for entry in lower_entries {
618 let entry_path = path.join(&entry.name);
620 if !self.is_whiteout(&entry_path) {
621 entries.insert(entry.name.clone(), entry);
622 }
623 }
624 }
625 }
626
627 if self.upper.exists(&path).await.unwrap_or(false) {
629 if let Ok(upper_entries) = self.upper.read_dir(&path).await {
630 for entry in upper_entries {
631 entries.insert(entry.name.clone(), entry);
632 }
633 }
634 }
635
636 Ok(entries.into_values().collect())
637 }
638
639 async fn exists(&self, path: &Path) -> Result<bool> {
640 let path = Self::normalize_path(path);
641
642 if self.is_whiteout(&path) {
644 return Ok(false);
645 }
646
647 if self.upper.exists(&path).await.unwrap_or(false) {
649 return Ok(true);
650 }
651
652 self.lower.exists(&path).await
654 }
655
656 async fn rename(&self, from: &Path, to: &Path) -> Result<()> {
657 let from = Self::normalize_path(from);
658 let to = Self::normalize_path(to);
659
660 let content = self.read_file(&from).await?;
662
663 self.write_file(&to, &content).await?;
665
666 self.remove(&from, false).await?;
668
669 Ok(())
670 }
671
672 async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
673 let from = Self::normalize_path(from);
674 let to = Self::normalize_path(to);
675
676 let content = self.read_file(&from).await?;
678
679 self.write_file(&to, &content).await
681 }
682
683 async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
684 self.limits
686 .validate_path(link)
687 .map_err(|e| IoError::other(e.to_string()))?;
688
689 let link = Self::normalize_path(link);
690
691 self.check_write_limits(0)?;
693
694 self.remove_whiteout(&link);
696
697 self.upper.symlink(target, &link).await
699 }
700
701 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
702 let path = Self::normalize_path(path);
703
704 if self.is_whiteout(&path) {
706 return Err(IoError::new(ErrorKind::NotFound, "not found").into());
707 }
708
709 if self.upper.exists(&path).await.unwrap_or(false) {
711 return self.upper.read_link(&path).await;
712 }
713
714 self.lower.read_link(&path).await
716 }
717
718 async fn chmod(&self, path: &Path, mode: u32) -> Result<()> {
719 let path = Self::normalize_path(path);
720
721 if self.is_whiteout(&path) {
723 return Err(IoError::new(ErrorKind::NotFound, "not found").into());
724 }
725
726 if self.upper.exists(&path).await.unwrap_or(false) {
728 return self.upper.chmod(&path, mode).await;
729 }
730
731 if self.lower.exists(&path).await.unwrap_or(false) {
733 let stat = self.lower.stat(&path).await?;
734
735 if stat.file_type == FileType::File {
737 let content = self.lower.read_file(&path).await?;
738 self.check_write_limits(content.len())?;
739
740 if let Some(parent) = path.parent() {
742 if !self.upper.exists(parent).await.unwrap_or(false) {
743 self.upper.mkdir(parent, true).await?;
744 }
745 }
746
747 self.upper.write_file(&path, &content).await?;
748 self.hide_lower_file(stat.size);
749 } else if stat.file_type == FileType::Directory {
750 self.upper.mkdir(&path, true).await?;
751 self.hide_lower_dir();
752 }
753
754 return self.upper.chmod(&path, mode).await;
755 }
756
757 Err(IoError::new(ErrorKind::NotFound, "not found").into())
758 }
759
760 fn usage(&self) -> FsUsage {
761 self.compute_usage()
762 }
763
764 fn limits(&self) -> FsLimits {
765 self.limits.clone()
766 }
767}
768
769#[cfg(test)]
770#[allow(clippy::unwrap_used)]
771mod tests {
772 use super::*;
773
774 #[tokio::test]
775 async fn test_read_from_lower() {
776 let lower = Arc::new(InMemoryFs::new());
777 lower
778 .write_file(Path::new("/tmp/test.txt"), b"hello")
779 .await
780 .unwrap();
781
782 let overlay = OverlayFs::new(lower);
783 let content = overlay.read_file(Path::new("/tmp/test.txt")).await.unwrap();
784 assert_eq!(content, b"hello");
785 }
786
787 #[tokio::test]
788 async fn test_write_to_upper() {
789 let lower = Arc::new(InMemoryFs::new());
790 let overlay = OverlayFs::new(lower.clone());
791
792 overlay
793 .write_file(Path::new("/tmp/new.txt"), b"new file")
794 .await
795 .unwrap();
796
797 let content = overlay.read_file(Path::new("/tmp/new.txt")).await.unwrap();
799 assert_eq!(content, b"new file");
800
801 assert!(!lower.exists(Path::new("/tmp/new.txt")).await.unwrap());
803 }
804
805 #[tokio::test]
806 async fn test_copy_on_write() {
807 let lower = Arc::new(InMemoryFs::new());
808 lower
809 .write_file(Path::new("/tmp/test.txt"), b"original")
810 .await
811 .unwrap();
812
813 let overlay = OverlayFs::new(lower.clone());
814
815 overlay
817 .write_file(Path::new("/tmp/test.txt"), b"modified")
818 .await
819 .unwrap();
820
821 let content = overlay.read_file(Path::new("/tmp/test.txt")).await.unwrap();
823 assert_eq!(content, b"modified");
824
825 let lower_content = lower.read_file(Path::new("/tmp/test.txt")).await.unwrap();
827 assert_eq!(lower_content, b"original");
828 }
829
830 #[tokio::test]
831 async fn test_delete_with_whiteout() {
832 let lower = Arc::new(InMemoryFs::new());
833 lower
834 .write_file(Path::new("/tmp/test.txt"), b"hello")
835 .await
836 .unwrap();
837
838 let overlay = OverlayFs::new(lower.clone());
839
840 overlay
842 .remove(Path::new("/tmp/test.txt"), false)
843 .await
844 .unwrap();
845
846 assert!(!overlay.exists(Path::new("/tmp/test.txt")).await.unwrap());
848
849 assert!(lower.exists(Path::new("/tmp/test.txt")).await.unwrap());
851 }
852
853 #[tokio::test]
854 async fn test_recreate_after_delete() {
855 let lower = Arc::new(InMemoryFs::new());
856 lower
857 .write_file(Path::new("/tmp/test.txt"), b"original")
858 .await
859 .unwrap();
860
861 let overlay = OverlayFs::new(lower);
862
863 overlay
865 .remove(Path::new("/tmp/test.txt"), false)
866 .await
867 .unwrap();
868 assert!(!overlay.exists(Path::new("/tmp/test.txt")).await.unwrap());
869
870 overlay
872 .write_file(Path::new("/tmp/test.txt"), b"new content")
873 .await
874 .unwrap();
875
876 assert!(overlay.exists(Path::new("/tmp/test.txt")).await.unwrap());
878 let content = overlay.read_file(Path::new("/tmp/test.txt")).await.unwrap();
879 assert_eq!(content, b"new content");
880 }
881
882 #[tokio::test]
883 async fn test_chmod_cow_enforces_write_limits() {
884 let lower = Arc::new(InMemoryFs::new());
886 lower
887 .write_file(Path::new("/tmp/big.txt"), &vec![b'x'; 5000])
888 .await
889 .unwrap();
890
891 let limits = FsLimits::new().max_total_bytes(1000);
893 let overlay = OverlayFs::with_limits(lower, limits);
894
895 let result = overlay.chmod(Path::new("/tmp/big.txt"), 0o755).await;
897 assert!(
898 result.is_err(),
899 "chmod CoW should fail when content exceeds write limits"
900 );
901 let err = result.unwrap_err().to_string();
902 assert!(
903 err.contains("filesystem full"),
904 "expected 'filesystem full' error, got: {err}"
905 );
906
907 assert!(
909 !overlay
910 .upper
911 .exists(Path::new("/tmp/big.txt"))
912 .await
913 .unwrap(),
914 "file should not have been copied to upper layer"
915 );
916 }
917
918 #[tokio::test]
919 async fn test_usage_no_double_count_override() {
920 let lower = Arc::new(InMemoryFs::new());
922 lower
923 .write_file(Path::new("/tmp/file.txt"), b"lower data") .await
925 .unwrap();
926
927 let overlay = OverlayFs::new(lower);
928
929 let usage_before = overlay.usage();
931
932 overlay
934 .write_file(Path::new("/tmp/file.txt"), b"upper!") .await
936 .unwrap();
937
938 let usage_after = overlay.usage();
939 assert_eq!(
941 usage_after.file_count, usage_before.file_count,
942 "overridden file should not increase file_count"
943 );
944 assert_eq!(
947 usage_after.total_bytes,
948 usage_before.total_bytes - 4,
949 "overridden file bytes should reflect upper size, not sum"
950 );
951 }
952
953 #[tokio::test]
954 async fn test_usage_no_double_count_whiteout() {
955 let lower = Arc::new(InMemoryFs::new());
957 lower
958 .write_file(Path::new("/tmp/gone.txt"), b"12345") .await
960 .unwrap();
961
962 let overlay = OverlayFs::new(lower.clone());
963 let usage_before = overlay.usage();
964
965 overlay
967 .remove(Path::new("/tmp/gone.txt"), false)
968 .await
969 .unwrap();
970
971 let usage_after = overlay.usage();
972 assert_eq!(
973 usage_after.file_count,
974 usage_before.file_count - 1,
975 "whited-out file should not be counted"
976 );
977 assert_eq!(
978 usage_after.total_bytes,
979 usage_before.total_bytes - 5,
980 "whited-out file bytes should be deducted"
981 );
982 }
983
984 #[tokio::test]
985 async fn test_usage_unique_files_both_layers() {
986 let lower = Arc::new(InMemoryFs::new());
988 lower
989 .write_file(Path::new("/tmp/lower.txt"), b"aaa") .await
991 .unwrap();
992
993 let overlay = OverlayFs::new(lower);
994 let usage_before = overlay.usage();
995
996 overlay
997 .write_file(Path::new("/tmp/upper.txt"), b"bbbbb") .await
999 .unwrap();
1000
1001 let usage_after = overlay.usage();
1002 assert_eq!(
1004 usage_after.file_count,
1005 usage_before.file_count + 1,
1006 "unique upper file adds one to count"
1007 );
1008 assert_eq!(
1009 usage_after.total_bytes,
1010 usage_before.total_bytes + 5,
1011 "unique upper file adds its bytes"
1012 );
1013 }
1014
1015 #[tokio::test]
1016 async fn test_usage_recreate_after_whiteout() {
1017 let lower = Arc::new(InMemoryFs::new());
1019 lower
1020 .write_file(Path::new("/tmp/file.txt"), b"old data 10") .await
1022 .unwrap();
1023
1024 let overlay = OverlayFs::new(lower);
1025 let usage_before = overlay.usage();
1026
1027 overlay
1029 .remove(Path::new("/tmp/file.txt"), false)
1030 .await
1031 .unwrap();
1032
1033 overlay
1035 .write_file(Path::new("/tmp/file.txt"), b"new") .await
1037 .unwrap();
1038
1039 let usage_after = overlay.usage();
1040 assert_eq!(
1042 usage_after.file_count, usage_before.file_count,
1043 "recreated file counted once"
1044 );
1045 assert_eq!(
1046 usage_after.total_bytes,
1047 usage_before.total_bytes - 8,
1048 "recreated file uses new size"
1049 );
1050 }
1051
1052 #[tokio::test]
1053 async fn test_read_dir_merged() {
1054 let lower = Arc::new(InMemoryFs::new());
1055 lower
1056 .write_file(Path::new("/tmp/lower.txt"), b"lower")
1057 .await
1058 .unwrap();
1059
1060 let overlay = OverlayFs::new(lower);
1061 overlay
1062 .write_file(Path::new("/tmp/upper.txt"), b"upper")
1063 .await
1064 .unwrap();
1065
1066 let entries = overlay.read_dir(Path::new("/tmp")).await.unwrap();
1067 let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
1068
1069 assert!(names.contains(&&"lower.txt".to_string()));
1070 assert!(names.contains(&&"upper.txt".to_string()));
1071 }
1072
1073 #[tokio::test]
1075 async fn test_usage_deducts_whiteouts() {
1076 let lower = Arc::new(InMemoryFs::new());
1077 lower
1078 .write_file(Path::new("/tmp/deleted.txt"), &[b'X'; 50])
1079 .await
1080 .unwrap();
1081
1082 let overlay = OverlayFs::new(lower);
1083 let before = overlay.usage();
1084
1085 overlay
1086 .remove(Path::new("/tmp/deleted.txt"), false)
1087 .await
1088 .unwrap();
1089
1090 let after = overlay.usage();
1091 assert_eq!(
1092 after.total_bytes,
1093 before.total_bytes - 50,
1094 "whited-out file bytes should be deducted"
1095 );
1096 assert_eq!(
1097 after.file_count,
1098 before.file_count - 1,
1099 "whited-out file should be deducted from count"
1100 );
1101 }
1102
1103 #[tokio::test]
1105 async fn test_usage_no_double_count_append_cow() {
1106 let lower = Arc::new(InMemoryFs::new());
1107 lower
1108 .write_file(Path::new("/tmp/log.txt"), &[b'A'; 100])
1109 .await
1110 .unwrap();
1111
1112 let overlay = OverlayFs::new(lower);
1113 let before = overlay.usage();
1114
1115 overlay
1116 .append_file(Path::new("/tmp/log.txt"), &[b'B'; 10])
1117 .await
1118 .unwrap();
1119
1120 let after = overlay.usage();
1121 assert_eq!(
1122 after.total_bytes,
1123 before.total_bytes + 10,
1124 "CoW append should add only new content bytes"
1125 );
1126 assert_eq!(after.file_count, before.file_count);
1127 }
1128
1129 #[tokio::test]
1131 async fn test_write_limits_include_lower_layer() {
1132 use super::super::limits::FsLimits;
1133
1134 let lower = Arc::new(InMemoryFs::new());
1135 lower
1137 .write_file(Path::new("/tmp/big.txt"), &[b'A'; 80])
1138 .await
1139 .unwrap();
1140
1141 let limits = FsLimits::new().max_total_bytes(100);
1143 let overlay = OverlayFs::with_limits(lower, limits);
1144
1145 let result = overlay
1147 .write_file(Path::new("/tmp/extra.txt"), &[b'B'; 30])
1148 .await;
1149 assert!(
1150 result.is_err(),
1151 "should reject write that exceeds combined limit"
1152 );
1153
1154 let result = overlay
1156 .write_file(Path::new("/tmp/small.txt"), &[b'C'; 15])
1157 .await;
1158 assert!(result.is_ok(), "should allow write within combined limit");
1159 }
1160
1161 #[tokio::test]
1163 async fn test_file_count_limit_includes_lower() {
1164 use super::super::limits::FsLimits;
1165
1166 let lower = Arc::new(InMemoryFs::new());
1167 lower
1168 .write_file(Path::new("/tmp/existing.txt"), b"data")
1169 .await
1170 .unwrap();
1171
1172 let temp_overlay = OverlayFs::new(lower.clone());
1174 let base_count = temp_overlay.usage().file_count;
1175
1176 let limits = FsLimits::new().max_file_count(base_count + 1);
1178 let overlay = OverlayFs::with_limits(lower, limits);
1179
1180 overlay
1182 .write_file(Path::new("/tmp/new1.txt"), b"ok")
1183 .await
1184 .unwrap();
1185
1186 let result = overlay
1188 .write_file(Path::new("/tmp/new2.txt"), b"fail")
1189 .await;
1190 assert!(
1191 result.is_err(),
1192 "should reject when combined file count exceeds limit"
1193 );
1194 }
1195
1196 #[tokio::test]
1198 async fn test_recursive_delete_whiteouts_children() {
1199 let lower = Arc::new(InMemoryFs::new());
1200 lower.mkdir(Path::new("/data"), true).await.unwrap();
1201 lower
1202 .write_file(Path::new("/data/a.txt"), b"aaa")
1203 .await
1204 .unwrap();
1205 lower
1206 .write_file(Path::new("/data/b.txt"), b"bbb")
1207 .await
1208 .unwrap();
1209 lower.mkdir(Path::new("/data/sub"), true).await.unwrap();
1210 lower
1211 .write_file(Path::new("/data/sub/c.txt"), b"ccc")
1212 .await
1213 .unwrap();
1214
1215 let overlay = OverlayFs::new(lower);
1216
1217 overlay.remove(Path::new("/data"), true).await.unwrap();
1219
1220 assert!(
1222 !overlay.exists(Path::new("/data/a.txt")).await.unwrap(),
1223 "child file should be hidden after recursive delete"
1224 );
1225 assert!(
1226 !overlay.exists(Path::new("/data/sub/c.txt")).await.unwrap(),
1227 "nested child should be hidden after recursive delete"
1228 );
1229 assert!(
1230 !overlay.exists(Path::new("/data")).await.unwrap(),
1231 "directory itself should be hidden"
1232 );
1233
1234 assert!(overlay.read_file(Path::new("/data/a.txt")).await.is_err());
1236 }
1237
1238 #[tokio::test]
1240 async fn test_recursive_delete_deducts_all_children() {
1241 let lower = Arc::new(InMemoryFs::new());
1242 lower.mkdir(Path::new("/stuff"), true).await.unwrap();
1243 lower
1244 .write_file(Path::new("/stuff/x.txt"), &[b'X'; 100])
1245 .await
1246 .unwrap();
1247 lower
1248 .write_file(Path::new("/stuff/y.txt"), &[b'Y'; 200])
1249 .await
1250 .unwrap();
1251
1252 let overlay = OverlayFs::new(lower);
1253 let before = overlay.usage();
1254
1255 overlay.remove(Path::new("/stuff"), true).await.unwrap();
1256
1257 let after = overlay.usage();
1258 assert_eq!(
1259 after.total_bytes,
1260 before.total_bytes - 300,
1261 "should deduct all child file bytes"
1262 );
1263 assert_eq!(
1264 after.file_count,
1265 before.file_count - 2,
1266 "should deduct all child file counts"
1267 );
1268 }
1269}