1use std::io;
11use std::path::Path;
12
13#[cfg(unix)]
37pub const fn unix_o_nofollow() -> i32 {
38 #[cfg(all(
39 target_os = "linux",
40 not(any(target_arch = "powerpc", target_arch = "powerpc64"))
41 ))]
42 {
43 0x20000
44 }
45 #[cfg(all(target_os = "linux", any(target_arch = "powerpc", target_arch = "powerpc64")))]
46 {
47 0x8000
48 }
49 #[cfg(any(
50 target_os = "macos",
51 target_os = "ios",
52 target_os = "freebsd",
53 target_os = "openbsd",
54 target_os = "netbsd",
55 target_os = "dragonfly"
56 ))]
57 {
58 0x100
59 }
60 #[cfg(not(any(
61 target_os = "linux",
62 target_os = "macos",
63 target_os = "ios",
64 target_os = "freebsd",
65 target_os = "openbsd",
66 target_os = "netbsd",
67 target_os = "dragonfly"
68 )))]
69 {
70 0x20000
71 }
72}
73
74#[cfg(unix)]
85pub const fn unix_o_nonblock() -> i32 {
86 #[cfg(target_os = "linux")]
89 {
90 0x800
91 }
92 #[cfg(any(
93 target_os = "macos",
94 target_os = "ios",
95 target_os = "freebsd",
96 target_os = "openbsd",
97 target_os = "netbsd",
98 target_os = "dragonfly"
99 ))]
100 {
101 0x4
102 }
103 #[cfg(not(any(
104 target_os = "linux",
105 target_os = "macos",
106 target_os = "ios",
107 target_os = "freebsd",
108 target_os = "openbsd",
109 target_os = "netbsd",
110 target_os = "dragonfly"
111 )))]
112 {
113 0x800
114 }
115}
116
117pub fn basename(path: &Path) -> &str {
134 path.file_name().and_then(|name| name.to_str()).unwrap_or("?")
135}
136
137pub fn sync_directory(path: &Path) -> io::Result<()> {
142 #[cfg(unix)]
143 {
144 std::fs::File::open(path)?.sync_all()
145 }
146 #[cfg(not(unix))]
147 {
148 let _ = path;
149 Ok(())
150 }
151}
152
153pub fn create_dir_all_synced(path: &Path) -> io::Result<()> {
160 create_dir_all_synced_with_directory_sync(path, sync_directory)
161}
162
163fn create_dir_all_synced_with_directory_sync(
164 path: &Path,
165 synchronize_parent: impl FnMut(&Path) -> io::Result<()>,
166) -> io::Result<()> {
167 if path.as_os_str().is_empty() {
168 return Ok(());
169 }
170 if !path.is_dir() {
174 #[cfg(unix)]
176 {
177 use std::os::unix::fs::DirBuilderExt as _;
178 let mut builder = std::fs::DirBuilder::new();
179 builder.recursive(true).mode(0o700);
180 builder.create(path)?;
181 }
182 #[cfg(not(unix))]
183 {
184 std::fs::create_dir_all(path)?;
185 }
186 }
187 sync_directory_ancestors_with_directory_sync(path, synchronize_parent)
188}
189
190pub fn sync_directory_ancestors(path: &Path) -> io::Result<()> {
195 sync_directory_ancestors_with_directory_sync(path, sync_directory)
196}
197
198fn sync_directory_ancestors_with_directory_sync(
199 path: &Path,
200 mut synchronize_parent: impl FnMut(&Path) -> io::Result<()>,
201) -> io::Result<()> {
202 let resolved = std::fs::canonicalize(path)?;
206 if !std::fs::metadata(&resolved)?.is_dir() {
207 return Err(io::Error::new(
208 io::ErrorKind::NotADirectory,
209 "directory path no longer names a directory",
210 ));
211 }
212 let parents: Vec<_> = resolved.ancestors().skip(1).collect();
213 for parent in parents.into_iter().rev() {
217 synchronize_parent(parent)?;
218 }
219 Ok(())
220}
221
222pub const MAX_RECEIPT_BYTES: u64 = 16 * 1024 * 1024;
226
227pub const MAX_ATIF_BYTES: u64 = 64 * 1024 * 1024;
230
231pub const MAX_CONTROL_BYTES: u64 = 1024 * 1024;
236
237pub fn read_capped(path: &Path, max_bytes: u64) -> io::Result<Vec<u8>> {
248 use std::io::Read as _;
249 let pre = std::fs::metadata(path)?;
256 if !pre.is_file() {
257 return Err(io::Error::new(
258 io::ErrorKind::InvalidInput,
259 format!(
260 "{} is not a regular file (type: {:?})",
261 basename(path),
262 pre.file_type()
263 ),
264 ));
265 }
266 let mut open_options = std::fs::OpenOptions::new();
274 open_options.read(true);
275 #[cfg(unix)]
276 {
277 use std::os::unix::fs::OpenOptionsExt as _;
278 open_options.custom_flags(unix_o_nonblock());
279 }
280 let mut file = open_options.open(path)?;
281 let metadata = file.metadata()?;
282 if !metadata.is_file() {
283 return Err(io::Error::new(
287 io::ErrorKind::InvalidInput,
288 format!(
289 "{} is not a regular file (type: {:?})",
290 basename(path),
291 metadata.file_type()
292 ),
293 ));
294 }
295 if metadata.len() > max_bytes {
296 return Err(io::Error::new(
297 io::ErrorKind::InvalidData,
298 format!(
299 "{} is {} bytes; refusing to load more than {max_bytes}",
300 basename(path),
301 metadata.len()
302 ),
303 ));
304 }
305 let mut buf: Vec<u8> = Vec::with_capacity(usize::try_from(metadata.len()).unwrap_or(0));
306 (&mut file)
307 .take(max_bytes.saturating_add(1))
308 .read_to_end(&mut buf)?;
309 if buf.len() as u64 > max_bytes {
310 return Err(io::Error::new(
311 io::ErrorKind::InvalidData,
312 format!(
313 "{} grew past {max_bytes} bytes during read; refusing",
314 basename(path)
315 ),
316 ));
317 }
318 Ok(buf)
319}
320
321pub fn read_capped_string(path: &Path, max_bytes: u64) -> io::Result<String> {
325 let bytes = read_capped(path, max_bytes)?;
326 String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
327}
328
329pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
351 write_atomic_with_directory_sync(path, bytes, sync_directory)
352}
353
354fn write_atomic_with_directory_sync(
355 path: &Path,
356 bytes: &[u8],
357 synchronize_parent: impl FnOnce(&Path) -> io::Result<()>,
358) -> io::Result<()> {
359 use std::io::Write as _;
360 let parent = path
365 .parent()
366 .filter(|p| !p.as_os_str().is_empty())
367 .unwrap_or_else(|| Path::new("."));
368 create_dir_all_synced(parent)?;
372 let temporary = path.with_extension(format!("{}.tmp", crate::new_event_uid()));
373 let mut guard = TempPathGuard::new(temporary.clone());
374 let mut options = std::fs::OpenOptions::new();
386 options.write(true).create_new(true);
387 #[cfg(unix)]
388 {
389 use std::os::unix::fs::OpenOptionsExt as _;
390 options.mode(0o600);
391 }
392 let mut file = options.open(&temporary)?;
393 file.write_all(bytes)?;
394 file.sync_all()?;
395 std::fs::rename(&temporary, path)?;
396 guard.disarm();
397 synchronize_parent(parent)
400}
401
402pub struct TempPathGuard {
410 path: Option<std::path::PathBuf>,
411}
412
413impl TempPathGuard {
414 pub fn new(path: std::path::PathBuf) -> Self {
417 Self { path: Some(path) }
418 }
419
420 pub fn disarm(&mut self) {
423 self.path = None;
424 }
425}
426
427impl Drop for TempPathGuard {
428 fn drop(&mut self) {
429 if let Some(path) = self.path.take() {
430 let _ = std::fs::remove_file(&path);
431 }
432 }
433}
434
435pub fn sweep_orphaned_tmp(root: &Path) -> io::Result<u64> {
451 const MAX_SWEEP_DEPTH: usize = 8;
452 fn walk(dir: &Path, depth: usize, removed: &mut u64) {
453 if depth > MAX_SWEEP_DEPTH {
454 return;
455 }
456 let Ok(entries) = std::fs::read_dir(dir) else {
457 return;
458 };
459 for entry in entries.flatten() {
460 let path = entry.path();
461 let Ok(file_type) = entry.file_type() else {
462 continue;
463 };
464 if file_type.is_dir() {
465 walk(&path, depth + 1, removed);
466 } else if file_type.is_file()
467 && path
468 .file_name()
469 .and_then(std::ffi::OsStr::to_str)
470 .is_some_and(|name| name.ends_with(".tmp"))
471 && std::fs::remove_file(&path).is_ok()
472 {
473 *removed += 1;
474 }
475 }
476 }
477 match std::fs::metadata(root) {
478 Ok(_) => {}
479 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(0),
480 Err(error) => return Err(error),
481 }
482 let mut removed = 0u64;
483 walk(root, 0, &mut removed);
484 Ok(removed)
485}
486
487#[cfg(test)]
488mod tests {
489 #![allow(clippy::unwrap_used)]
490
491 use super::*;
492
493 #[test]
498 fn read_capped_string_round_trips_and_refuses_over_cap() {
499 let dir = tempfile::tempdir().unwrap();
500 let path = dir.path().join("value.txt");
501 std::fs::write(&path, "expected-content").unwrap();
502 assert_eq!(
503 read_capped_string(&path, MAX_CONTROL_BYTES).unwrap(),
504 "expected-content"
505 );
506 assert!(read_capped_string(&path, 4).is_err(), "over-cap read must refuse");
507 }
508
509 #[test]
516 #[cfg(unix)]
517 fn write_atomic_produces_owner_only_files() {
518 use std::os::unix::fs::PermissionsExt as _;
519 let dir = tempfile::tempdir().unwrap();
520 let path = dir.path().join("nested").join("payload.bin");
523 write_atomic(&path, b"secret").unwrap();
524 let file_mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
525 assert_eq!(
526 file_mode, 0o600,
527 "spool files must be owner-only; got {file_mode:o}"
528 );
529 let dir_mode = std::fs::metadata(path.parent().unwrap())
534 .unwrap()
535 .permissions()
536 .mode()
537 & 0o777;
538 assert_eq!(
539 dir_mode, 0o700,
540 "spool subdirectories must be owner-only; got {dir_mode:o}"
541 );
542 }
543
544 #[test]
552 #[cfg(unix)]
553 fn write_atomic_replaces_symlink_without_following_it() {
554 let dir = tempfile::tempdir().unwrap();
555 let victim = dir.path().join("victim-should-be-untouched");
556 std::fs::write(&victim, b"original-target-contents").unwrap();
557 let spool_target = dir.path().join("payload.bin");
558 std::os::unix::fs::symlink(&victim, &spool_target).unwrap();
559 write_atomic(&spool_target, b"fresh").unwrap();
560 assert_eq!(std::fs::read(&spool_target).unwrap(), b"fresh");
562 assert_eq!(
564 std::fs::read(&victim).unwrap(),
565 b"original-target-contents",
566 "write_atomic must not follow the symlink — it replaces at rename time"
567 );
568 }
569
570 #[test]
577 #[cfg(unix)]
578 fn unix_o_nofollow_flag_refuses_symlinks_but_permits_regular_files() {
579 use std::os::unix::fs::OpenOptionsExt as _;
580 let dir = tempfile::tempdir().unwrap();
581 let regular = dir.path().join("regular");
583 std::fs::write(®ular, b"hi").unwrap();
584 let ok = std::fs::OpenOptions::new()
585 .read(true)
586 .custom_flags(unix_o_nofollow())
587 .open(®ular);
588 assert!(ok.is_ok(), "O_NOFOLLOW must permit regular files: {ok:?}");
589 let victim = dir.path().join("victim");
591 std::fs::write(&victim, b"target").unwrap();
592 let link = dir.path().join("link");
593 std::os::unix::fs::symlink(&victim, &link).unwrap();
594 let err = std::fs::OpenOptions::new()
595 .read(true)
596 .custom_flags(unix_o_nofollow())
597 .open(&link);
598 assert!(
599 err.is_err(),
600 "O_NOFOLLOW must refuse symlinks (guards the confused-deputy \
601 class); constant value {:#x} may be wrong for this platform",
602 unix_o_nofollow()
603 );
604 }
605
606 #[test]
613 #[cfg(unix)]
614 fn unix_o_nonblock_flag_opens_fifos_without_blocking() {
615 use std::os::unix::fs::OpenOptionsExt as _;
616 let dir = tempfile::tempdir().unwrap();
617 let regular = dir.path().join("regular");
618 std::fs::write(®ular, b"hi").unwrap();
619 let ok = std::fs::OpenOptions::new()
620 .read(true)
621 .custom_flags(unix_o_nonblock())
622 .open(®ular);
623 assert!(ok.is_ok(), "O_NONBLOCK must permit regular files: {ok:?}");
624 let fifo = dir.path().join("fifo");
625 let status = std::process::Command::new("mkfifo").arg(&fifo).status().unwrap();
627 assert!(status.success(), "mkfifo failed");
628 let opened = std::fs::OpenOptions::new()
630 .read(true)
631 .custom_flags(unix_o_nonblock())
632 .open(&fifo)
633 .unwrap();
634 let metadata = opened.metadata().unwrap();
635 assert!(
636 !metadata.is_file(),
637 "handle check must classify the FIFO as non-regular"
638 );
639 }
640
641 #[test]
645 fn basename_returns_only_the_last_segment() {
646 assert_eq!(
647 basename(Path::new("/var/lib/agentvisor-ai/spool/foo.json")),
648 "foo.json"
649 );
650 assert_eq!(basename(Path::new("foo.json")), "foo.json");
651 assert_eq!(basename(Path::new("/tmp/")), "tmp");
652 assert_eq!(basename(Path::new("/")), "?");
655 #[cfg(unix)]
658 {
659 use std::ffi::OsStr;
660 use std::os::unix::ffi::OsStrExt as _;
661 let raw = std::path::PathBuf::from(OsStr::from_bytes(b"/x/\xff\xfe"));
662 assert_eq!(basename(&raw), "?");
663 }
664 }
665
666 #[test]
669 fn create_dir_all_synced_existing_dir_is_ok_but_file_collision_errors() {
670 let dir = tempfile::tempdir().unwrap();
671 let target = dir.path().join("a/b");
672 create_dir_all_synced(&target).unwrap();
673 create_dir_all_synced(&target).unwrap();
675 assert!(target.is_dir());
676
677 let file = dir.path().join("occupied");
678 std::fs::write(&file, b"x").unwrap();
679 assert!(
680 create_dir_all_synced(&file).is_err(),
681 "a file squatting on the directory path must error"
682 );
683 assert!(
684 create_dir_all_synced(&file.join("child")).is_err(),
685 "a file squatting on an ANCESTOR must error"
686 );
687 }
688
689 #[test]
690 fn ancestor_sync_never_creates_missing_directories() {
691 let dir = tempfile::tempdir().unwrap();
692 let missing = dir.path().join("missing/leaf");
693 assert_eq!(
694 sync_directory_ancestors(&missing).unwrap_err().kind(),
695 io::ErrorKind::NotFound
696 );
697 assert!(!dir.path().join("missing").exists());
698 let file = dir.path().join("occupied");
699 std::fs::write(&file, b"file").unwrap();
700 assert_eq!(
701 sync_directory_ancestors(&file).unwrap_err().kind(),
702 io::ErrorKind::NotADirectory
703 );
704 }
705
706 #[test]
707 fn directory_sync_retry_confirms_ancestors_that_already_exist() {
708 let dir = tempfile::tempdir().unwrap();
709 let boundary = std::fs::canonicalize(dir.path()).unwrap();
710 let target = dir.path().join("a/b/c");
711 for _ in 0..2 {
712 let mut visited = Vec::new();
713 let error = create_dir_all_synced_with_directory_sync(&target, |parent| {
714 visited.push(parent.to_path_buf());
715 assert!(target.is_dir(), "mkdir completes before the sync attempt");
716 if parent == boundary {
717 return Err(io::Error::other("injected ancestor sync failure"));
718 }
719 Ok(())
720 })
721 .unwrap_err();
722 assert_eq!(error.kind(), io::ErrorKind::Other);
723 assert_eq!(visited.last(), Some(&boundary));
724 assert!(
725 target.is_dir(),
726 "failed confirmation must retain the complete tree"
727 );
728 }
729 let resolved = std::fs::canonicalize(&target).unwrap();
730 let mut expected: Vec<_> = resolved.ancestors().skip(1).map(Path::to_path_buf).collect();
731 expected.reverse();
732 let mut confirmed = Vec::new();
733 create_dir_all_synced_with_directory_sync(&target, |parent| {
734 confirmed.push(parent.to_path_buf());
735 sync_directory(parent)
736 })
737 .unwrap();
738 assert_eq!(
739 confirmed, expected,
740 "retry must confirm the complete chain in order"
741 );
742 }
743
744 #[test]
745 fn concurrent_directory_creator_cannot_bypass_ancestor_confirmation() {
746 let dir = tempfile::tempdir().unwrap();
747 let boundary = std::fs::canonicalize(dir.path()).unwrap();
748 let target = dir.path().join("a/b/c");
749 let (entered_tx, entered_rx) = std::sync::mpsc::sync_channel(0);
750 let (release_tx, release_rx) = std::sync::mpsc::sync_channel(0);
751 let first_path = target.clone();
752 let first_boundary = boundary.clone();
753 let first = std::thread::spawn(move || {
754 create_dir_all_synced_with_directory_sync(&first_path, |parent| {
755 if parent == first_boundary {
756 entered_tx.send(()).unwrap();
757 release_rx.recv().unwrap();
758 return Err(io::Error::other("first creator could not sync ancestor"));
759 }
760 Ok(())
761 })
762 });
763 entered_rx
764 .recv_timeout(std::time::Duration::from_secs(30))
765 .unwrap();
766 assert!(
767 target.is_dir(),
768 "the first caller has made the whole tree visible"
769 );
770 let mut second_reached_boundary = false;
771 let second = create_dir_all_synced_with_directory_sync(&target, |parent| {
772 if parent == boundary {
773 second_reached_boundary = true;
774 return Err(io::Error::other("second creator could not sync ancestor"));
775 }
776 Ok(())
777 });
778 release_tx.send(()).unwrap();
779 assert!(first.join().unwrap().is_err());
780 assert!(
781 second_reached_boundary,
782 "an existing directory is not proof of durability"
783 );
784 assert!(
785 second.is_err(),
786 "each caller must fail closed on its own sync error"
787 );
788 create_dir_all_synced(&target).unwrap();
789 }
790
791 #[test]
792 fn directory_sync_resolves_relative_paths_without_changing_working_directory() {
793 let dir = tempfile::Builder::new()
794 .prefix(".av-relative-directory-test-")
795 .tempdir_in(std::env::current_dir().unwrap())
796 .unwrap();
797 let relative = Path::new(dir.path().file_name().unwrap()).join("nested/./leaf");
798 let mut confirmed = Vec::new();
799 create_dir_all_synced_with_directory_sync(&relative, |parent| {
800 assert!(parent.is_absolute());
801 confirmed.push(parent.to_path_buf());
802 Ok(())
803 })
804 .unwrap();
805 let resolved = std::fs::canonicalize(dir.path().join("nested/leaf")).unwrap();
806 let mut expected: Vec<_> = resolved.ancestors().skip(1).map(Path::to_path_buf).collect();
807 expected.reverse();
808 assert_eq!(confirmed, expected);
809 assert!(dir.path().join("nested/leaf").is_dir());
810 }
811
812 #[cfg(unix)]
813 #[test]
814 fn directory_sync_confirms_symlink_target_ancestry_and_rejects_invalid_targets() {
815 use std::os::unix::fs::symlink;
816 let dir = tempfile::tempdir().unwrap();
817 let real = dir.path().join("real/parent");
818 std::fs::create_dir_all(&real).unwrap();
819 let alias = dir.path().join("alias");
820 symlink(&real, &alias).unwrap();
821 let mut confirmed = Vec::new();
822 create_dir_all_synced_with_directory_sync(&alias.join("child"), |parent| {
823 confirmed.push(parent.to_path_buf());
824 Ok(())
825 })
826 .unwrap();
827 let resolved = std::fs::canonicalize(real.join("child")).unwrap();
828 let mut expected: Vec<_> = resolved.ancestors().skip(1).map(Path::to_path_buf).collect();
829 expected.reverse();
830 assert_eq!(
831 confirmed, expected,
832 "sync the resolved target, not just alias parents"
833 );
834 let occupied = dir.path().join("occupied");
835 std::fs::write(&occupied, b"file").unwrap();
836 let file_alias = dir.path().join("file-alias");
837 symlink(&occupied, &file_alias).unwrap();
838 assert!(create_dir_all_synced(&file_alias).is_err());
839 let broken = dir.path().join("broken");
840 symlink(dir.path().join("absent"), &broken).unwrap();
841 assert!(create_dir_all_synced(&broken).is_err());
842 }
843
844 #[test]
845 fn write_atomic_creates_parent_and_writes_bytes() {
846 let dir = tempfile::tempdir().unwrap();
847 let target = dir.path().join("nested/dir/output.bin");
848 write_atomic(&target, b"hello").unwrap();
849 assert_eq!(std::fs::read(&target).unwrap(), b"hello");
850 }
851
852 #[test]
853 fn write_atomic_replaces_existing_file() {
854 let dir = tempfile::tempdir().unwrap();
855 let target = dir.path().join("output.bin");
856 std::fs::write(&target, b"old").unwrap();
857 write_atomic(&target, b"new").unwrap();
858 assert_eq!(std::fs::read(&target).unwrap(), b"new");
859 }
860
861 #[test]
862 fn write_atomic_reports_post_rename_sync_failure_and_retains_recoverable_file() {
863 let dir = tempfile::tempdir().unwrap();
864 let target = dir.path().join("audit-marker.json");
865 std::fs::write(&target, b"old").unwrap();
866 let error = write_atomic_with_directory_sync(&target, b"complete new marker", |parent| {
867 assert_eq!(parent, dir.path());
868 assert_eq!(std::fs::read(&target).unwrap(), b"complete new marker");
869 Err(io::Error::other("injected directory synchronization failure"))
870 })
871 .unwrap_err();
872 assert_eq!(error.kind(), io::ErrorKind::Other);
873 assert_eq!(std::fs::read(&target).unwrap(), b"complete new marker");
874 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
875 write_atomic(&target, b"complete new marker").unwrap();
877 assert_eq!(std::fs::read(&target).unwrap(), b"complete new marker");
878 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
879 }
880
881 #[test]
882 fn write_atomic_leaves_no_temp_files_on_success() {
883 let dir = tempfile::tempdir().unwrap();
884 let target = dir.path().join("output.bin");
885 for _ in 0..8 {
886 write_atomic(&target, b"payload").unwrap();
887 }
888 let residual: Vec<_> = std::fs::read_dir(dir.path())
889 .unwrap()
890 .filter_map(Result::ok)
891 .filter(|entry| entry.file_name().to_string_lossy().contains(".tmp"))
892 .collect();
893 assert!(residual.is_empty(), "stale temp files: {residual:?}");
894 }
895
896 #[test]
903 fn temp_path_guard_unlinks_when_dropped_armed() {
904 let dir = tempfile::tempdir().unwrap();
905 let tmp = dir.path().join("aborted.tmp");
906 std::fs::write(&tmp, b"partial").unwrap();
907 {
908 let _guard = TempPathGuard::new(tmp.clone());
909 }
911 assert!(!tmp.exists(), "guard failed to unlink tmp on drop");
912 }
913
914 #[test]
915 fn temp_path_guard_leaves_file_alone_when_disarmed() {
916 let dir = tempfile::tempdir().unwrap();
917 let tmp = dir.path().join("kept.tmp");
918 std::fs::write(&tmp, b"kept").unwrap();
919 {
920 let mut guard = TempPathGuard::new(tmp.clone());
921 guard.disarm();
922 }
923 assert!(tmp.exists(), "disarmed guard must not touch the file");
924 }
925}
926
927#[cfg(test)]
928mod read_capped_tests {
929 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
930
931 use super::*;
932
933 #[test]
937 fn read_capped_roundtrips_and_enforces_the_exact_cap() {
938 let dir = tempfile::tempdir().unwrap();
939 let path = dir.path().join("payload.bin");
940 let content = b"agentvisor-read-capped-fixture".to_vec();
941 std::fs::write(&path, &content).unwrap();
942 assert_eq!(read_capped(&path, MAX_CONTROL_BYTES).unwrap(), content);
944 assert_eq!(read_capped(&path, content.len() as u64).unwrap(), content);
946 let under = read_capped(&path, content.len() as u64 - 1);
948 assert!(under.is_err(), "under-cap read must refuse, got {under:?}");
949 assert!(read_capped(dir.path(), MAX_CONTROL_BYTES).is_err());
951 }
952
953 #[test]
957 fn byte_caps_are_pinned() {
958 assert_eq!(MAX_RECEIPT_BYTES, 16 * 1024 * 1024);
959 assert_eq!(MAX_ATIF_BYTES, 64 * 1024 * 1024);
960 assert_eq!(MAX_CONTROL_BYTES, 1024 * 1024);
961 }
962
963 #[test]
964 fn sweep_orphaned_tmp_removes_only_tmp_files_recursively() {
965 let dir = tempfile::tempdir().unwrap();
966 let nested = dir.path().join("receipts").join("deep");
967 std::fs::create_dir_all(&nested).unwrap();
968 std::fs::write(dir.path().join("a.json.0198.tmp"), b"orphan").unwrap();
969 std::fs::write(nested.join("b.receipt.0199.tmp"), b"").unwrap();
970 std::fs::write(dir.path().join("keep.json"), b"real").unwrap();
971 std::fs::write(nested.join("keep.ndjson"), b"real").unwrap();
972 let removed = sweep_orphaned_tmp(dir.path()).unwrap();
973 assert_eq!(removed, 2, "exactly the two .tmp orphans");
974 assert!(dir.path().join("keep.json").exists());
975 assert!(nested.join("keep.ndjson").exists());
976 assert!(!dir.path().join("a.json.0198.tmp").exists());
977 assert!(!nested.join("b.receipt.0199.tmp").exists());
978 assert_eq!(sweep_orphaned_tmp(&dir.path().join("absent")).unwrap(), 0);
980 }
981
982 #[test]
989 fn sweep_depth_bound_is_exact() {
990 let dir = tempfile::tempdir().unwrap();
991 let mut at_bound = dir.path().to_path_buf();
992 for i in 0..8 {
993 at_bound.push(format!("d{i}"));
994 }
995 std::fs::create_dir_all(&at_bound).unwrap();
996 std::fs::write(at_bound.join("edge.0200.tmp"), b"").unwrap();
997 let mut past_bound = at_bound.clone();
998 past_bound.push("d8");
999 std::fs::create_dir_all(&past_bound).unwrap();
1000 std::fs::write(past_bound.join("deep.0201.tmp"), b"").unwrap();
1001
1002 let removed = sweep_orphaned_tmp(dir.path()).unwrap();
1003 assert_eq!(removed, 1, "exactly the at-bound orphan");
1004 assert!(
1005 !at_bound.join("edge.0200.tmp").exists(),
1006 "an orphan at depth 8 (the bound) must be swept"
1007 );
1008 assert!(
1009 past_bound.join("deep.0201.tmp").exists(),
1010 "an orphan at depth 9 (past the bound) must be left alone"
1011 );
1012 }
1013
1014 #[cfg(unix)]
1019 #[test]
1020 fn sweep_surfaces_non_notfound_root_errors() {
1021 use std::os::unix::fs::PermissionsExt as _;
1022 let dir = tempfile::tempdir().unwrap();
1023 let root = dir.path().join("locked");
1024 std::fs::create_dir(&root).unwrap();
1025 std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o000)).unwrap();
1026 let denied = std::fs::metadata(root.join("probe")).is_err() && std::fs::read_dir(&root).is_err();
1027 if denied {
1028 let unreachable_root = root.join("inner");
1035 assert!(
1036 sweep_orphaned_tmp(&unreachable_root).is_err(),
1037 "an EACCES root must surface as an error, not a silent Ok(0)"
1038 );
1039 }
1040 std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o755)).unwrap();
1041 }
1042}
1043
1044#[cfg(test)]
1045mod empty_path_tests {
1046 #![allow(clippy::unwrap_used)]
1047
1048 #[test]
1050 fn create_dir_all_synced_treats_empty_path_as_noop() {
1051 let mut called = false;
1052 super::create_dir_all_synced_with_directory_sync(std::path::Path::new(""), |_| {
1053 called = true;
1054 Ok(())
1055 })
1056 .unwrap();
1057 assert!(!called);
1058 assert!(
1059 super::create_dir_all_synced(std::path::Path::new("")).is_ok(),
1060 "empty path must be a no-op Ok, not an mkdir(\"\") error"
1061 );
1062 }
1063}