1use crate::CoreError;
12use std::ffi::CString;
13use std::fs;
14use std::io::{Read, Write};
15use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
16use std::path::Path;
17use std::time::UNIX_EPOCH;
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct PathFingerprint {
21 pub len: u64,
22 pub modified_ns: u128,
23}
24
25pub fn path_fingerprint(path: &Path) -> Result<PathFingerprint, CoreError> {
31 let metadata = std::fs::metadata(path).map_err(|err| {
32 CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "path_fingerprint")
33 })?;
34 let modified_ns = metadata
35 .modified()
36 .ok()
37 .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
38 .map(|duration| duration.as_nanos())
39 .unwrap_or_default();
40 Ok(PathFingerprint {
41 len: metadata.len(),
42 modified_ns,
43 })
44}
45
46pub fn path_exists(path: &str) -> bool {
53 match std::ffi::CString::new(path) {
54 Ok(c) => unsafe { libc::access(c.as_ptr(), libc::F_OK) == 0 },
55 Err(_) => false,
56 }
57}
58
59pub fn path_lstat_exists(path: &str) -> bool {
63 match std::ffi::CString::new(path) {
64 Ok(c) => unsafe {
65 let mut stat = std::mem::zeroed();
66 libc::lstat(c.as_ptr(), &mut stat) == 0
67 },
68 Err(_) => false,
69 }
70}
71
72pub fn read_to_string(path: &str) -> Result<String, CoreError> {
83 std::fs::read_to_string(path)
84 .map_err(|err| CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), "read_to_string"))
85}
86
87pub fn readahead(fd: impl AsRawFd, offset: u64, len: usize) -> Result<(), CoreError> {
102 readahead_raw(fd.as_raw_fd(), offset, len)
103}
104
105pub const FADV_NORMAL: i32 = libc::POSIX_FADV_NORMAL;
108pub const FADV_RANDOM: i32 = libc::POSIX_FADV_RANDOM;
109pub const FADV_SEQUENTIAL: i32 = libc::POSIX_FADV_SEQUENTIAL;
110pub const FADV_WILLNEED: i32 = libc::POSIX_FADV_WILLNEED;
111pub const FADV_DONTNEED: i32 = libc::POSIX_FADV_DONTNEED;
112pub const FADV_NOREUSE: i32 = libc::POSIX_FADV_NOREUSE;
113
114pub fn fadvise(fd: impl AsRawFd, offset: u64, len: usize, advice: i32) -> Result<(), CoreError> {
127 let ret = unsafe {
128 libc::posix_fadvise(
129 fd.as_raw_fd(),
130 offset as libc::off_t,
131 len as libc::off_t,
132 advice,
133 )
134 };
135 if ret == 0 {
136 Ok(())
137 } else {
138 Err(CoreError::sys(ret, "posix_fadvise"))
139 }
140}
141
142pub fn mmap_madvise(
153 fd: impl AsRawFd,
154 offset: u64,
155 len: usize,
156 touch: bool,
157) -> Result<(), CoreError> {
158 mmap_madvise_raw(fd.as_raw_fd(), offset, len, touch)
159}
160
161#[cfg(any(target_os = "linux", target_os = "android"))]
162fn mmap_madvise_raw(
163 fd: libc::c_int,
164 offset: u64,
165 len: usize,
166 touch: bool,
167) -> Result<(), CoreError> {
168 if len == 0 {
169 return Ok(());
170 }
171
172 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
173 if page_size <= 0 {
174 return Err(CoreError::sys(libc::EINVAL, "sysconf(_SC_PAGESIZE)"));
175 }
176 let page_size = page_size as u64;
177 if offset % page_size != 0 || offset > libc::off_t::MAX as u64 {
178 return Err(CoreError::sys(libc::EINVAL, "mmap"));
179 }
180
181 let file_len = {
185 let mut st: libc::stat = unsafe { std::mem::zeroed() };
186 if unsafe { libc::fstat(fd, &mut st) } == -1 {
187 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
188 return Err(CoreError::sys(code, "fstat"));
189 }
190 st.st_size.max(0) as u64
191 };
192 let len = len.min(file_len.saturating_sub(offset) as usize);
193 if len == 0 {
194 return Ok(());
195 }
196
197 let ptr = unsafe {
198 libc::mmap(
199 std::ptr::null_mut(),
200 len,
201 libc::PROT_READ,
202 libc::MAP_PRIVATE,
203 fd,
204 offset as libc::off_t,
205 )
206 };
207 if ptr == libc::MAP_FAILED {
208 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
209 return Err(CoreError::sys(code, "mmap"));
210 }
211
212 let result = if unsafe { libc::madvise(ptr, len, libc::MADV_WILLNEED) } == -1 {
213 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
214 Err(CoreError::sys(code, "madvise"))
215 } else {
216 if touch {
217 let page_size = page_size as usize;
226 let mut pos = 0usize;
227 while pos < len {
228 let mut st: libc::stat = unsafe { std::mem::zeroed() };
229 if unsafe { libc::fstat(fd, &mut st) } == -1 {
230 break;
231 }
232 let file_len = st.st_size.max(0) as u64;
233 let page_start = offset.saturating_add(pos as u64);
234 if page_start >= file_len {
235 break;
236 }
237 unsafe {
238 std::ptr::read_volatile((ptr as *const u8).add(pos));
239 }
240 pos = pos.saturating_add(page_size);
241 }
242 }
243 Ok(())
244 };
245
246 if unsafe { libc::munmap(ptr, len) } == -1 {
247 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
248 return Err(CoreError::sys(code, "munmap"));
249 }
250 result
251}
252
253#[cfg(not(any(target_os = "linux", target_os = "android")))]
254fn mmap_madvise_raw(
255 _fd: libc::c_int,
256 _offset: u64,
257 _len: usize,
258 _touch: bool,
259) -> Result<(), CoreError> {
260 Err(CoreError::sys(libc::ENOSYS, "mmap"))
261}
262
263#[cfg(any(target_os = "linux", target_os = "android"))]
264fn readahead_raw(fd: libc::c_int, offset: u64, len: usize) -> Result<(), CoreError> {
265 if offset > libc::off64_t::MAX as u64 {
266 return Err(CoreError::sys(libc::EINVAL, "readahead"));
267 }
268
269 let count = len as libc::size_t;
270 let offset = offset as libc::off64_t;
271
272 loop {
273 let ret = unsafe { libc::syscall(readahead_syscall_number(), fd, offset, count) };
274 if ret == -1 {
275 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
276 if code == libc::EINTR {
277 continue;
278 }
279 return Err(CoreError::sys(code, "readahead"));
280 }
281 return Ok(());
282 }
283}
284
285#[cfg(not(any(target_os = "linux", target_os = "android")))]
286fn readahead_raw(_fd: libc::c_int, _offset: u64, _len: usize) -> Result<(), CoreError> {
287 Err(CoreError::sys(libc::ENOSYS, "readahead"))
288}
289
290#[cfg(target_os = "linux")]
291#[inline(always)]
292const fn readahead_syscall_number() -> libc::c_long {
293 libc::SYS_readahead
294}
295
296#[cfg(all(target_os = "android", target_arch = "aarch64"))]
297#[inline(always)]
298const fn readahead_syscall_number() -> libc::c_long {
299 213
300}
301
302#[cfg(all(target_os = "android", target_arch = "arm"))]
303#[inline(always)]
304const fn readahead_syscall_number() -> libc::c_long {
305 225
306}
307
308#[cfg(all(target_os = "android", target_arch = "x86_64"))]
309#[inline(always)]
310const fn readahead_syscall_number() -> libc::c_long {
311 187
312}
313
314#[cfg(all(target_os = "android", target_arch = "x86"))]
315#[inline(always)]
316const fn readahead_syscall_number() -> libc::c_long {
317 225
318}
319
320const TEMP_ATTEMPTS: usize = 32;
321
322fn cstr(s: &str) -> Result<CString, CoreError> {
323 CString::new(s).map_err(|_| CoreError::sys(libc::EINVAL, "path:nul_byte"))
324}
325
326fn openat_raw(dirfd: RawFd, name: &str, flags: i32, mode: u32) -> Result<RawFd, CoreError> {
327 let c = cstr(name)?;
328 let fd = unsafe { libc::openat(dirfd, c.as_ptr(), flags, mode as libc::c_uint) };
331 if fd < 0 {
332 Err(std::io::Error::last_os_error().into())
333 } else {
334 Ok(fd)
335 }
336}
337
338fn openat_dir(dirfd: RawFd, name: &str) -> Result<OwnedFd, CoreError> {
339 let fd = openat_raw(
340 dirfd,
341 name,
342 libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
343 0,
344 )?;
345 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
347}
348
349fn walk_dir(path: &Path, create_mode: Option<u32>) -> Result<OwnedFd, CoreError> {
356 let abs = if path.is_absolute() {
357 path.to_path_buf()
358 } else {
359 std::env::current_dir()?.join(path)
360 };
361 let mut dir = openat_dir(libc::AT_FDCWD, "/")?;
362 for comp in abs.components() {
363 use std::path::Component;
364 match comp {
365 Component::RootDir | Component::CurDir => {}
366 Component::Normal(name) => {
367 let name = name
368 .to_str()
369 .ok_or_else(|| CoreError::sys(libc::EINVAL, "path:non_utf8"))?;
370 let next = match openat_dir(dir.as_raw_fd(), name) {
371 Ok(fd) => fd,
372 Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create_mode.is_some() => {
373 let c = cstr(name)?;
374 if unsafe {
378 libc::mkdirat(
379 dir.as_raw_fd(),
380 c.as_ptr(),
381 create_mode.unwrap() as libc::mode_t,
382 )
383 } != 0
384 {
385 if std::io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST)
389 {
390 openat_dir(dir.as_raw_fd(), name)?
391 } else {
392 return Err(std::io::Error::last_os_error().into());
393 }
394 } else {
395 openat_dir(dir.as_raw_fd(), name)?
396 }
397 }
398 Err(e) => return Err(e),
399 };
400 dir = next;
401 }
402 Component::ParentDir => {
403 return Err(CoreError::sys(libc::EINVAL, "path:parent_component"));
404 }
405 Component::Prefix(_) => unreachable!("non-Windows path"),
406 }
407 }
408 Ok(dir)
409}
410
411fn open_dir_nofollow(path: &Path) -> Result<OwnedFd, CoreError> {
414 walk_dir(path, None)
415}
416
417fn fstat(fd: RawFd) -> Result<libc::stat, CoreError> {
418 let mut st: libc::stat = unsafe { std::mem::zeroed() };
419 if unsafe { libc::fstat(fd, &mut st) } != 0 {
421 Err(std::io::Error::last_os_error().into())
422 } else {
423 Ok(st)
424 }
425}
426
427fn unlink_name(dirfd: RawFd, name: &str) -> Result<(), CoreError> {
428 let c = cstr(name)?;
429 if unsafe { libc::unlinkat(dirfd, c.as_ptr(), 0) } != 0 {
432 Err(std::io::Error::last_os_error().into())
433 } else {
434 Ok(())
435 }
436}
437
438fn basename(target: &Path) -> Result<String, CoreError> {
439 target
440 .file_name()
441 .map(|n| n.to_string_lossy().into_owned())
442 .ok_or_else(|| CoreError::sys(libc::EINVAL, "path:no_file_name"))
443}
444
445fn parent_dir(target: &Path) -> &Path {
446 target
447 .parent()
448 .filter(|p| !p.as_os_str().is_empty())
449 .unwrap_or_else(|| Path::new("."))
450}
451
452fn open_parent_nofollow(target: &Path) -> Result<OwnedFd, CoreError> {
460 let dir = open_dir_nofollow(parent_dir(target))?;
461 let st = fstat(dir.as_raw_fd())?;
462 let euid = unsafe { libc::geteuid() };
463 if st.st_uid != euid {
464 return Err(CoreError::sys(libc::EACCES, "parent_dir_owner"));
465 }
466 Ok(dir)
467}
468
469struct TmpGuard {
470 dirfd: RawFd,
471 name: String,
472}
473
474impl Drop for TmpGuard {
475 fn drop(&mut self) {
476 let _ = unlink_name(self.dirfd, &self.name);
477 }
478}
479
480pub fn write_atomic(path: impl AsRef<Path>, content: &[u8]) -> Result<(), CoreError> {
488 let target = path.as_ref();
489 let file_name = basename(target)?;
490 let dir = open_parent_nofollow(target)?;
491 let dirfd = dir.as_raw_fd();
492
493 for attempt in 0..TEMP_ATTEMPTS {
494 let tmp_name = format!(".{file_name}.{}.{attempt}", std::process::id());
495 match openat_raw(
496 dirfd,
497 &tmp_name,
498 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC,
499 0o600,
500 ) {
501 Ok(raw) => {
502 let mut file = unsafe { fs::File::from_raw_fd(raw) };
505 let _guard = TmpGuard {
506 dirfd,
507 name: tmp_name.clone(),
508 };
509 file.write_all(content)?;
510 file.sync_all()?;
511 drop(file);
512 let c_tmp = cstr(&tmp_name)?;
513 let c_final = cstr(&file_name)?;
514 if unsafe { libc::renameat(dirfd, c_tmp.as_ptr(), dirfd, c_final.as_ptr()) } != 0 {
518 return Err(std::io::Error::last_os_error().into());
519 }
520 std::mem::forget(_guard);
522 return Ok(());
523 }
524 Err(e) if e.raw_os_error() == Some(libc::EEXIST) => continue,
525 Err(e) => return Err(e),
526 }
527 }
528
529 Err(CoreError::sys(libc::EEXIST, "temp:exhausted"))
530}
531
532pub fn read_nofollow(path: impl AsRef<Path>) -> Result<String, CoreError> {
539 let target = path.as_ref();
540 let file_name = basename(target)?;
541 let dir = open_parent_nofollow(target)?;
542 let fd = openat_raw(
543 dir.as_raw_fd(),
544 &file_name,
545 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
546 0,
547 )?;
548 let mut file = unsafe { fs::File::from_raw_fd(fd) };
550 let mut content = String::new();
551 file.read_to_string(&mut content)?;
552 Ok(content)
553}
554
555pub fn open_append_nofollow(path: impl AsRef<Path>) -> Result<fs::File, CoreError> {
561 let target = path.as_ref();
562 let file_name = basename(target)?;
563 let dir = open_parent_nofollow(target)?;
564 let fd = openat_raw(
565 dir.as_raw_fd(),
566 &file_name,
567 libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND | libc::O_NOFOLLOW | libc::O_CLOEXEC,
568 0o644,
569 )?;
570 Ok(unsafe { fs::File::from_raw_fd(fd) })
572}
573
574pub fn remove_nofollow(path: impl AsRef<Path>) -> Result<(), CoreError> {
578 let target = path.as_ref();
579 let file_name = basename(target)?;
580 let dir = open_parent_nofollow(target)?;
581 unlink_name(dir.as_raw_fd(), &file_name)
582}
583
584pub fn ensure_state_dir(dir: impl AsRef<Path>) -> Result<OwnedFd, CoreError> {
589 let dir = dir.as_ref();
590 let fd = walk_dir(dir, Some(0o700))?;
594 let st = fstat(fd.as_raw_fd())?;
595 let euid = unsafe { libc::geteuid() };
596 if st.st_uid != euid {
597 return Err(CoreError::sys(libc::EACCES, "state_dir_owner"));
598 }
599 Ok(fd)
600}
601
602#[cfg(test)]
603mod tests {
604 #[cfg(target_os = "linux")]
605 #[test]
606 fn test_readahead_syscall_number_linux_matches_libc() {
607 assert_eq!(super::readahead_syscall_number(), libc::SYS_readahead);
608 }
609
610 #[cfg(target_os = "linux")]
611 #[test]
612 fn test_mmap_madvise_touch_past_eof_does_not_sigbus() {
613 use std::os::unix::io::{AsRawFd, FromRawFd};
614
615 let dir =
619 std::env::temp_dir().join(format!("coreshift_mmap_past_eof_{}", std::process::id()));
620 let _ = std::fs::remove_file(&dir);
621 std::fs::write(&dir, b"x").unwrap();
622
623 let f = std::fs::File::open(&dir).unwrap();
624 let fd = f.as_raw_fd();
625 let dup = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
627 assert!(dup >= 0);
628 let owned = unsafe { std::fs::File::from_raw_fd(dup) };
629
630 let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
631 let result = super::mmap_madvise(owned.as_raw_fd(), 0, (page * 4) as usize, true);
632 drop(owned);
633 let _ = std::fs::remove_file(&dir);
634
635 assert!(result.is_ok(), "touch past EOF must not SIGBUS: {result:?}");
638 }
639
640 #[cfg(all(target_os = "android", target_arch = "aarch64"))]
641 #[test]
642 fn test_readahead_syscall_number_android_aarch64() {
643 assert_eq!(super::readahead_syscall_number(), 213);
644 }
645
646 #[cfg(all(target_os = "android", target_arch = "arm"))]
647 #[test]
648 fn test_readahead_syscall_number_android_arm() {
649 assert_eq!(super::readahead_syscall_number(), 225);
650 }
651
652 #[cfg(all(target_os = "android", target_arch = "x86_64"))]
653 #[test]
654 fn test_readahead_syscall_number_android_x86_64() {
655 assert_eq!(super::readahead_syscall_number(), 187);
656 }
657
658 #[cfg(all(target_os = "android", target_arch = "x86"))]
659 #[test]
660 fn test_readahead_syscall_number_android_x86() {
661 assert_eq!(super::readahead_syscall_number(), 225);
662 }
663}
664
665#[cfg(test)]
666mod safe_fs_tests {
667
668 use super::*;
669 use std::os::unix::fs::symlink;
670 use std::path::PathBuf;
671
672 fn tmpdir(name: &str) -> PathBuf {
673 let d = std::env::temp_dir().join(format!(
674 "coreshift_safe_fs_dir_{}_{name}",
675 std::process::id()
676 ));
677 let _ = fs::remove_dir_all(&d);
678 fs::create_dir_all(&d).unwrap();
679 d
680 }
681
682 #[test]
683 fn test_write_atomic_creates_regular_file() {
684 let dir = tmpdir("w");
685 let p = dir.join("out.txt");
686
687 write_atomic(&p, b"hello").unwrap();
688 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
689 assert!(fs::symlink_metadata(&p).unwrap().file_type().is_file());
690 }
691
692 #[test]
693 fn test_write_atomic_replaces_existing_symlink_not_target() {
694 let dir = tmpdir("s1");
695 let target = dir.join("victim");
696 let link = dir.join("link");
697
698 fs::write(&target, b"precious").unwrap();
699 symlink(&target, &link).unwrap();
700
701 write_atomic(&link, b"new").unwrap();
704
705 assert_eq!(fs::read_to_string(&target).unwrap(), "precious");
706 assert_eq!(fs::read_to_string(&link).unwrap(), "new");
707 assert!(fs::symlink_metadata(&link).unwrap().file_type().is_file());
708 }
709
710 #[test]
711 fn test_read_nofollow_refuses_symlink() {
712 let dir = tmpdir("r");
713 let target = dir.join("victim2");
714 let link = dir.join("link2");
715
716 fs::write(&target, b"secret").unwrap();
717 symlink(&target, &link).unwrap();
718
719 assert_eq!(read_nofollow(&target).unwrap(), "secret");
720 assert!(read_nofollow(&link).is_err());
721 }
722
723 #[test]
724 fn test_open_append_nofollow_refuses_symlink() {
725 let dir = tmpdir("a");
726 let target = dir.join("target3");
727 let link = dir.join("link3");
728
729 fs::write(&target, b"x").unwrap();
730 symlink(&target, &link).unwrap();
731
732 assert!(open_append_nofollow(&target).is_ok());
734 assert!(open_append_nofollow(&link).is_err());
736
737 let _ = fs::remove_file(&target);
738 let _ = fs::remove_file(&link);
739 }
740
741 #[test]
742 fn test_write_atomic_refuses_symlinked_parent() {
743 let dir = tmpdir("parent_symlink");
747 let elsewhere = tmpdir("parent_dest");
748 let link = dir.join("coreshift");
749 symlink(&elsewhere, &link).unwrap();
750
751 assert!(write_atomic(link.join("payload.txt"), b"boom").is_err());
752 assert!(!elsewhere.join("payload.txt").exists());
753 assert!(!link.join("payload.txt").exists());
754 }
755
756 #[test]
757 fn test_read_nofollow_refuses_symlinked_parent() {
758 let dir = tmpdir("read_parent_symlink");
759 let elsewhere = tmpdir("read_parent_dest");
760 fs::write(elsewhere.join("conf"), b"injected").unwrap();
761 let link = dir.join("coreshift");
762 symlink(&elsewhere, &link).unwrap();
763
764 assert!(read_nofollow(link.join("conf")).is_err());
765 }
766
767 #[test]
768 fn test_open_append_nofollow_refuses_symlinked_parent() {
769 let dir = tmpdir("append_parent_symlink");
770 let elsewhere = tmpdir("append_parent_dest");
771 let link = dir.join("coreshift");
772 symlink(&elsewhere, &link).unwrap();
773
774 assert!(open_append_nofollow(link.join("daemon.log")).is_err());
775 assert!(!elsewhere.join("daemon.log").exists());
776 }
777
778 #[test]
779 fn test_ensure_state_dir_refuses_symlink() {
780 let dir = tmpdir("state_symlink");
781 let elsewhere = tmpdir("state_dest");
782 let link = dir.join("state");
783 symlink(&elsewhere, &link).unwrap();
784
785 assert!(ensure_state_dir(&link).is_err());
786 let real = tmpdir("state_real");
788 assert!(ensure_state_dir(&real).is_ok());
789 }
790
791 #[test]
792 fn test_remove_nofollow_removes_entry_not_target() {
793 let dir = tmpdir("unlink");
794 let target = dir.join("victim4");
795 let link = dir.join("link4");
796 fs::write(&target, b"keep").unwrap();
797 symlink(&target, &link).unwrap();
798
799 remove_nofollow(&link).unwrap();
800 assert!(!link.exists());
801 assert_eq!(fs::read_to_string(&target).unwrap(), "keep");
802 }
803
804 #[test]
805 fn test_write_atomic_requires_parent_to_exist() {
806 let dir = tmpdir("missing_parent");
807 let p = dir.join("nope").join("file.txt");
808
809 assert!(write_atomic(&p, b"x").is_err());
810 assert!(!p.exists());
811 }
812
813 #[test]
814 fn test_ops_refuse_foreign_owned_parent() {
815 if unsafe { libc::geteuid() } != 0 {
820 return;
821 }
822 let dir = tmpdir("foreign_owner");
823 let path = dir.join("f");
824 let owned = cstr(&dir.to_string_lossy()).unwrap();
825 assert_eq!(unsafe { libc::chown(owned.as_ptr(), 65534, 65534) }, 0);
827
828 assert!(write_atomic(&path, b"x").is_err());
829 assert!(read_nofollow(&path).is_err());
830 assert!(open_append_nofollow(&path).is_err());
831 assert!(remove_nofollow(&path).is_err());
832 assert!(ensure_state_dir(&dir).is_err());
833 assert!(!path.exists());
834 }
835
836 fn temp_leftovers(dir: &Path, file_name: &str) -> Vec<String> {
837 let prefix = format!(".{file_name}.");
838 fs::read_dir(dir)
839 .map(|rd| {
840 rd.filter_map(|e| e.ok())
841 .filter_map(|e| e.file_name().to_str().map(str::to_owned))
842 .filter(|n| n.starts_with(&prefix))
843 .collect()
844 })
845 .unwrap_or_default()
846 }
847
848 #[test]
849 fn test_write_atomic_cleans_temp_on_rename_failure() {
850 let dir = tmpdir("rename_fail");
854 let dest = dir.join("dest");
855 fs::create_dir_all(&dest).unwrap();
856 fs::write(dest.join("keep"), b"x").unwrap();
857
858 assert!(write_atomic(&dest, b"boom").is_err());
859 assert_eq!(fs::read_to_string(dest.join("keep")).unwrap(), "x");
860 assert!(
861 temp_leftovers(&dir, "dest").is_empty(),
862 "temp file must be cleaned up"
863 );
864 }
865
866 #[test]
867 fn test_write_atomic_leaves_no_temp_on_success() {
868 let dir = tmpdir("no_temp_success");
869 let p = dir.join("out.txt");
870
871 write_atomic(&p, b"hello").unwrap();
872 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
873 assert!(
874 temp_leftovers(&dir, "out.txt").is_empty(),
875 "no temp left behind"
876 );
877 }
878
879 #[test]
880 fn test_write_atomic_retries_when_temp_name_exists() {
881 let dir = tmpdir("temp_collision");
882 let p = dir.join("out.txt");
883 let pid = std::process::id();
884 let collided = dir.join(format!(".out.txt.{pid}.0"));
885 fs::write(&collided, b"not mine").unwrap();
886
887 write_atomic(&p, b"hello").unwrap();
888 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
889 assert_eq!(fs::read_to_string(&collided).unwrap(), "not mine");
892 let leftovers = temp_leftovers(&dir, "out.txt");
893 assert_eq!(
894 leftovers.len(),
895 1,
896 "only the pre-existing colliding temp remains"
897 );
898 assert_eq!(leftovers[0], format!(".out.txt.{pid}.0"));
899 }
900
901 #[test]
902 fn test_ops_refuse_foreign_owned_writable_parent() {
903 use std::os::unix::fs::MetadataExt;
909 let euid = unsafe { libc::geteuid() };
910 if euid == 0 {
911 return;
912 }
913 let tmp = std::env::temp_dir();
914 let meta = match fs::symlink_metadata(&tmp) {
915 Ok(m) => m,
916 Err(_) => return,
917 };
918 if meta.uid() == euid {
919 return; }
921 if meta.mode() & 0o002 == 0 && meta.mode() & 0o020 == 0 {
922 return; }
924 let p = tmp.join(format!(
925 "coreshift_fs_foreign_{}_{}",
926 std::process::id(),
927 "out"
928 ));
929 let _ = fs::remove_file(&p);
930 fs::write(&p, b"probe").unwrap();
931
932 assert!(read_nofollow(&p).is_err());
933 assert!(open_append_nofollow(&p).is_err());
934 assert!(write_atomic(&p, b"boom").is_err());
935 assert!(remove_nofollow(&p).is_err());
936 assert!(ensure_state_dir(&tmp).is_err());
937 assert_eq!(fs::read_to_string(&p).unwrap(), "probe");
938 let _ = fs::remove_file(&p);
939 }
940
941 #[test]
942 fn test_ensure_state_dir_creates_fresh_dir() {
943 let base = tmpdir("fresh_base");
944 let nested = base.join("a").join("b").join("state");
945
946 let fd = ensure_state_dir(&nested).unwrap();
947 assert!(nested.is_dir());
948 drop(fd);
949 assert!(ensure_state_dir(&nested).is_ok());
950 }
951
952 #[test]
953 fn test_open_append_nofollow_refuses_dangling_symlink() {
954 let dir = tmpdir("dangling_append");
955 let missing = dir.join("not_there.txt");
956 let link = dir.join("linkd");
957 symlink(&missing, &link).unwrap();
958
959 assert!(open_append_nofollow(&link).is_err());
960 assert!(
961 !missing.exists(),
962 "must not create the target through a dangling link"
963 );
964 }
965
966 #[test]
967 fn test_read_nofollow_refuses_dangling_symlink() {
968 let dir = tmpdir("dangling_read");
969 let missing = dir.join("not_there2.txt");
970 let link = dir.join("linkd2");
971 symlink(&missing, &link).unwrap();
972
973 assert!(read_nofollow(&link).is_err());
974 assert!(!missing.exists());
975 }
976
977 #[test]
978 fn test_ops_refuse_regular_file_parent() {
979 let dir = tmpdir("regfile_parent");
982 let f = dir.join("notadir");
983 fs::write(&f, b"x").unwrap();
984
985 assert!(write_atomic(f.join("out"), b"y").is_err());
986 assert!(read_nofollow(f.join("out")).is_err());
987 assert!(open_append_nofollow(f.join("out")).is_err());
988 assert!(remove_nofollow(f.join("out")).is_err());
989 assert!(ensure_state_dir(&f).is_err());
990 assert_eq!(fs::read_to_string(&f).unwrap(), "x");
991 }
992}