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 mut pos = 0usize;
218 let page_size = page_size as usize;
219 while pos < len {
220 unsafe {
221 std::ptr::read_volatile((ptr as *const u8).add(pos));
222 }
223 pos = pos.saturating_add(page_size);
224 }
225 }
226 Ok(())
227 };
228
229 if unsafe { libc::munmap(ptr, len) } == -1 {
230 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
231 return Err(CoreError::sys(code, "munmap"));
232 }
233 result
234}
235
236#[cfg(not(any(target_os = "linux", target_os = "android")))]
237fn mmap_madvise_raw(
238 _fd: libc::c_int,
239 _offset: u64,
240 _len: usize,
241 _touch: bool,
242) -> Result<(), CoreError> {
243 Err(CoreError::sys(libc::ENOSYS, "mmap"))
244}
245
246#[cfg(any(target_os = "linux", target_os = "android"))]
247fn readahead_raw(fd: libc::c_int, offset: u64, len: usize) -> Result<(), CoreError> {
248 if offset > libc::off64_t::MAX as u64 {
249 return Err(CoreError::sys(libc::EINVAL, "readahead"));
250 }
251
252 let count = len as libc::size_t;
253 let offset = offset as libc::off64_t;
254
255 loop {
256 let ret = unsafe { libc::syscall(readahead_syscall_number(), fd, offset, count) };
257 if ret == -1 {
258 let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
259 if code == libc::EINTR {
260 continue;
261 }
262 return Err(CoreError::sys(code, "readahead"));
263 }
264 return Ok(());
265 }
266}
267
268#[cfg(not(any(target_os = "linux", target_os = "android")))]
269fn readahead_raw(_fd: libc::c_int, _offset: u64, _len: usize) -> Result<(), CoreError> {
270 Err(CoreError::sys(libc::ENOSYS, "readahead"))
271}
272
273#[cfg(target_os = "linux")]
274#[inline(always)]
275const fn readahead_syscall_number() -> libc::c_long {
276 libc::SYS_readahead
277}
278
279#[cfg(all(target_os = "android", target_arch = "aarch64"))]
280#[inline(always)]
281const fn readahead_syscall_number() -> libc::c_long {
282 213
283}
284
285#[cfg(all(target_os = "android", target_arch = "arm"))]
286#[inline(always)]
287const fn readahead_syscall_number() -> libc::c_long {
288 225
289}
290
291#[cfg(all(target_os = "android", target_arch = "x86_64"))]
292#[inline(always)]
293const fn readahead_syscall_number() -> libc::c_long {
294 187
295}
296
297#[cfg(all(target_os = "android", target_arch = "x86"))]
298#[inline(always)]
299const fn readahead_syscall_number() -> libc::c_long {
300 225
301}
302
303const TEMP_ATTEMPTS: usize = 32;
304
305fn cstr(s: &str) -> Result<CString, CoreError> {
306 CString::new(s).map_err(|_| CoreError::sys(libc::EINVAL, "path:nul_byte"))
307}
308
309fn openat_raw(dirfd: RawFd, name: &str, flags: i32, mode: u32) -> Result<RawFd, CoreError> {
310 let c = cstr(name)?;
311 let fd = unsafe { libc::openat(dirfd, c.as_ptr(), flags, mode as libc::c_uint) };
314 if fd < 0 {
315 Err(std::io::Error::last_os_error().into())
316 } else {
317 Ok(fd)
318 }
319}
320
321fn openat_dir(dirfd: RawFd, name: &str) -> Result<OwnedFd, CoreError> {
322 let fd = openat_raw(
323 dirfd,
324 name,
325 libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
326 0,
327 )?;
328 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
330}
331
332fn walk_dir(path: &Path, create_mode: Option<u32>) -> Result<OwnedFd, CoreError> {
339 let abs = if path.is_absolute() {
340 path.to_path_buf()
341 } else {
342 std::env::current_dir()?.join(path)
343 };
344 let mut dir = openat_dir(libc::AT_FDCWD, "/")?;
345 for comp in abs.components() {
346 use std::path::Component;
347 match comp {
348 Component::RootDir | Component::CurDir => {}
349 Component::Normal(name) => {
350 let name = name
351 .to_str()
352 .ok_or_else(|| CoreError::sys(libc::EINVAL, "path:non_utf8"))?;
353 let next = match openat_dir(dir.as_raw_fd(), name) {
354 Ok(fd) => fd,
355 Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create_mode.is_some() => {
356 let c = cstr(name)?;
357 if unsafe {
361 libc::mkdirat(
362 dir.as_raw_fd(),
363 c.as_ptr(),
364 create_mode.unwrap() as libc::mode_t,
365 )
366 } != 0
367 {
368 if std::io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST)
372 {
373 openat_dir(dir.as_raw_fd(), name)?
374 } else {
375 return Err(std::io::Error::last_os_error().into());
376 }
377 } else {
378 openat_dir(dir.as_raw_fd(), name)?
379 }
380 }
381 Err(e) => return Err(e),
382 };
383 dir = next;
384 }
385 Component::ParentDir => {
386 return Err(CoreError::sys(libc::EINVAL, "path:parent_component"));
387 }
388 Component::Prefix(_) => unreachable!("non-Windows path"),
389 }
390 }
391 Ok(dir)
392}
393
394fn open_dir_nofollow(path: &Path) -> Result<OwnedFd, CoreError> {
397 walk_dir(path, None)
398}
399
400fn fstat(fd: RawFd) -> Result<libc::stat, CoreError> {
401 let mut st: libc::stat = unsafe { std::mem::zeroed() };
402 if unsafe { libc::fstat(fd, &mut st) } != 0 {
404 Err(std::io::Error::last_os_error().into())
405 } else {
406 Ok(st)
407 }
408}
409
410fn unlink_name(dirfd: RawFd, name: &str) -> Result<(), CoreError> {
411 let c = cstr(name)?;
412 if unsafe { libc::unlinkat(dirfd, c.as_ptr(), 0) } != 0 {
415 Err(std::io::Error::last_os_error().into())
416 } else {
417 Ok(())
418 }
419}
420
421fn basename(target: &Path) -> Result<String, CoreError> {
422 target
423 .file_name()
424 .map(|n| n.to_string_lossy().into_owned())
425 .ok_or_else(|| CoreError::sys(libc::EINVAL, "path:no_file_name"))
426}
427
428fn parent_dir(target: &Path) -> &Path {
429 target
430 .parent()
431 .filter(|p| !p.as_os_str().is_empty())
432 .unwrap_or_else(|| Path::new("."))
433}
434
435fn open_parent_nofollow(target: &Path) -> Result<OwnedFd, CoreError> {
443 let dir = open_dir_nofollow(parent_dir(target))?;
444 let st = fstat(dir.as_raw_fd())?;
445 let euid = unsafe { libc::geteuid() };
446 if st.st_uid != euid {
447 return Err(CoreError::sys(libc::EACCES, "parent_dir_owner"));
448 }
449 Ok(dir)
450}
451
452struct TmpGuard {
453 dirfd: RawFd,
454 name: String,
455}
456
457impl Drop for TmpGuard {
458 fn drop(&mut self) {
459 let _ = unlink_name(self.dirfd, &self.name);
460 }
461}
462
463pub fn write_atomic(path: impl AsRef<Path>, content: &[u8]) -> Result<(), CoreError> {
471 let target = path.as_ref();
472 let file_name = basename(target)?;
473 let dir = open_parent_nofollow(target)?;
474 let dirfd = dir.as_raw_fd();
475
476 for attempt in 0..TEMP_ATTEMPTS {
477 let tmp_name = format!(".{file_name}.{}.{attempt}", std::process::id());
478 match openat_raw(
479 dirfd,
480 &tmp_name,
481 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC,
482 0o600,
483 ) {
484 Ok(raw) => {
485 let mut file = unsafe { fs::File::from_raw_fd(raw) };
488 let _guard = TmpGuard {
489 dirfd,
490 name: tmp_name.clone(),
491 };
492 file.write_all(content)?;
493 file.sync_all()?;
494 drop(file);
495 let c_tmp = cstr(&tmp_name)?;
496 let c_final = cstr(&file_name)?;
497 if unsafe { libc::renameat(dirfd, c_tmp.as_ptr(), dirfd, c_final.as_ptr()) } != 0 {
501 return Err(std::io::Error::last_os_error().into());
502 }
503 std::mem::forget(_guard);
505 return Ok(());
506 }
507 Err(e) if e.raw_os_error() == Some(libc::EEXIST) => continue,
508 Err(e) => return Err(e),
509 }
510 }
511
512 Err(CoreError::sys(libc::EEXIST, "temp:exhausted"))
513}
514
515pub fn read_nofollow(path: impl AsRef<Path>) -> Result<String, CoreError> {
522 let target = path.as_ref();
523 let file_name = basename(target)?;
524 let dir = open_parent_nofollow(target)?;
525 let fd = openat_raw(
526 dir.as_raw_fd(),
527 &file_name,
528 libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
529 0,
530 )?;
531 let mut file = unsafe { fs::File::from_raw_fd(fd) };
533 let mut content = String::new();
534 file.read_to_string(&mut content)?;
535 Ok(content)
536}
537
538pub fn open_append_nofollow(path: impl AsRef<Path>) -> Result<fs::File, CoreError> {
544 let target = path.as_ref();
545 let file_name = basename(target)?;
546 let dir = open_parent_nofollow(target)?;
547 let fd = openat_raw(
548 dir.as_raw_fd(),
549 &file_name,
550 libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND | libc::O_NOFOLLOW | libc::O_CLOEXEC,
551 0o644,
552 )?;
553 Ok(unsafe { fs::File::from_raw_fd(fd) })
555}
556
557pub fn remove_nofollow(path: impl AsRef<Path>) -> Result<(), CoreError> {
561 let target = path.as_ref();
562 let file_name = basename(target)?;
563 let dir = open_parent_nofollow(target)?;
564 unlink_name(dir.as_raw_fd(), &file_name)
565}
566
567pub fn ensure_state_dir(dir: impl AsRef<Path>) -> Result<OwnedFd, CoreError> {
572 let dir = dir.as_ref();
573 let fd = walk_dir(dir, Some(0o700))?;
577 let st = fstat(fd.as_raw_fd())?;
578 let euid = unsafe { libc::geteuid() };
579 if st.st_uid != euid {
580 return Err(CoreError::sys(libc::EACCES, "state_dir_owner"));
581 }
582 Ok(fd)
583}
584
585#[cfg(test)]
586mod tests {
587 #[cfg(target_os = "linux")]
588 #[test]
589 fn test_readahead_syscall_number_linux_matches_libc() {
590 assert_eq!(super::readahead_syscall_number(), libc::SYS_readahead);
591 }
592
593 #[cfg(target_os = "linux")]
594 #[test]
595 fn test_mmap_madvise_touch_past_eof_does_not_sigbus() {
596 use std::os::unix::io::{AsRawFd, FromRawFd};
597
598 let dir =
602 std::env::temp_dir().join(format!("coreshift_mmap_past_eof_{}", std::process::id()));
603 let _ = std::fs::remove_file(&dir);
604 std::fs::write(&dir, b"x").unwrap();
605
606 let f = std::fs::File::open(&dir).unwrap();
607 let fd = f.as_raw_fd();
608 let dup = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
610 assert!(dup >= 0);
611 let owned = unsafe { std::fs::File::from_raw_fd(dup) };
612
613 let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
614 let result = super::mmap_madvise(owned.as_raw_fd(), 0, (page * 4) as usize, true);
615 drop(owned);
616 let _ = std::fs::remove_file(&dir);
617
618 assert!(result.is_ok(), "touch past EOF must not SIGBUS: {result:?}");
621 }
622
623 #[cfg(all(target_os = "android", target_arch = "aarch64"))]
624 #[test]
625 fn test_readahead_syscall_number_android_aarch64() {
626 assert_eq!(super::readahead_syscall_number(), 213);
627 }
628
629 #[cfg(all(target_os = "android", target_arch = "arm"))]
630 #[test]
631 fn test_readahead_syscall_number_android_arm() {
632 assert_eq!(super::readahead_syscall_number(), 225);
633 }
634
635 #[cfg(all(target_os = "android", target_arch = "x86_64"))]
636 #[test]
637 fn test_readahead_syscall_number_android_x86_64() {
638 assert_eq!(super::readahead_syscall_number(), 187);
639 }
640
641 #[cfg(all(target_os = "android", target_arch = "x86"))]
642 #[test]
643 fn test_readahead_syscall_number_android_x86() {
644 assert_eq!(super::readahead_syscall_number(), 225);
645 }
646}
647
648#[cfg(test)]
649mod safe_fs_tests {
650
651 use super::*;
652 use std::os::unix::fs::symlink;
653 use std::path::PathBuf;
654
655 fn tmpdir(name: &str) -> PathBuf {
656 let d = std::env::temp_dir().join(format!(
657 "coreshift_safe_fs_dir_{}_{name}",
658 std::process::id()
659 ));
660 let _ = fs::remove_dir_all(&d);
661 fs::create_dir_all(&d).unwrap();
662 d
663 }
664
665 #[test]
666 fn test_write_atomic_creates_regular_file() {
667 let dir = tmpdir("w");
668 let p = dir.join("out.txt");
669
670 write_atomic(&p, b"hello").unwrap();
671 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
672 assert!(fs::symlink_metadata(&p).unwrap().file_type().is_file());
673 }
674
675 #[test]
676 fn test_write_atomic_replaces_existing_symlink_not_target() {
677 let dir = tmpdir("s1");
678 let target = dir.join("victim");
679 let link = dir.join("link");
680
681 fs::write(&target, b"precious").unwrap();
682 symlink(&target, &link).unwrap();
683
684 write_atomic(&link, b"new").unwrap();
687
688 assert_eq!(fs::read_to_string(&target).unwrap(), "precious");
689 assert_eq!(fs::read_to_string(&link).unwrap(), "new");
690 assert!(fs::symlink_metadata(&link).unwrap().file_type().is_file());
691 }
692
693 #[test]
694 fn test_read_nofollow_refuses_symlink() {
695 let dir = tmpdir("r");
696 let target = dir.join("victim2");
697 let link = dir.join("link2");
698
699 fs::write(&target, b"secret").unwrap();
700 symlink(&target, &link).unwrap();
701
702 assert_eq!(read_nofollow(&target).unwrap(), "secret");
703 assert!(read_nofollow(&link).is_err());
704 }
705
706 #[test]
707 fn test_open_append_nofollow_refuses_symlink() {
708 let dir = tmpdir("a");
709 let target = dir.join("target3");
710 let link = dir.join("link3");
711
712 fs::write(&target, b"x").unwrap();
713 symlink(&target, &link).unwrap();
714
715 assert!(open_append_nofollow(&target).is_ok());
717 assert!(open_append_nofollow(&link).is_err());
719
720 let _ = fs::remove_file(&target);
721 let _ = fs::remove_file(&link);
722 }
723
724 #[test]
725 fn test_write_atomic_refuses_symlinked_parent() {
726 let dir = tmpdir("parent_symlink");
730 let elsewhere = tmpdir("parent_dest");
731 let link = dir.join("coreshift");
732 symlink(&elsewhere, &link).unwrap();
733
734 assert!(write_atomic(link.join("payload.txt"), b"boom").is_err());
735 assert!(!elsewhere.join("payload.txt").exists());
736 assert!(!link.join("payload.txt").exists());
737 }
738
739 #[test]
740 fn test_read_nofollow_refuses_symlinked_parent() {
741 let dir = tmpdir("read_parent_symlink");
742 let elsewhere = tmpdir("read_parent_dest");
743 fs::write(elsewhere.join("conf"), b"injected").unwrap();
744 let link = dir.join("coreshift");
745 symlink(&elsewhere, &link).unwrap();
746
747 assert!(read_nofollow(link.join("conf")).is_err());
748 }
749
750 #[test]
751 fn test_open_append_nofollow_refuses_symlinked_parent() {
752 let dir = tmpdir("append_parent_symlink");
753 let elsewhere = tmpdir("append_parent_dest");
754 let link = dir.join("coreshift");
755 symlink(&elsewhere, &link).unwrap();
756
757 assert!(open_append_nofollow(link.join("daemon.log")).is_err());
758 assert!(!elsewhere.join("daemon.log").exists());
759 }
760
761 #[test]
762 fn test_ensure_state_dir_refuses_symlink() {
763 let dir = tmpdir("state_symlink");
764 let elsewhere = tmpdir("state_dest");
765 let link = dir.join("state");
766 symlink(&elsewhere, &link).unwrap();
767
768 assert!(ensure_state_dir(&link).is_err());
769 let real = tmpdir("state_real");
771 assert!(ensure_state_dir(&real).is_ok());
772 }
773
774 #[test]
775 fn test_remove_nofollow_removes_entry_not_target() {
776 let dir = tmpdir("unlink");
777 let target = dir.join("victim4");
778 let link = dir.join("link4");
779 fs::write(&target, b"keep").unwrap();
780 symlink(&target, &link).unwrap();
781
782 remove_nofollow(&link).unwrap();
783 assert!(!link.exists());
784 assert_eq!(fs::read_to_string(&target).unwrap(), "keep");
785 }
786
787 #[test]
788 fn test_write_atomic_requires_parent_to_exist() {
789 let dir = tmpdir("missing_parent");
790 let p = dir.join("nope").join("file.txt");
791
792 assert!(write_atomic(&p, b"x").is_err());
793 assert!(!p.exists());
794 }
795
796 #[test]
797 fn test_ops_refuse_foreign_owned_parent() {
798 if unsafe { libc::geteuid() } != 0 {
803 return;
804 }
805 let dir = tmpdir("foreign_owner");
806 let path = dir.join("f");
807 let owned = cstr(&dir.to_string_lossy()).unwrap();
808 assert_eq!(unsafe { libc::chown(owned.as_ptr(), 65534, 65534) }, 0);
810
811 assert!(write_atomic(&path, b"x").is_err());
812 assert!(read_nofollow(&path).is_err());
813 assert!(open_append_nofollow(&path).is_err());
814 assert!(remove_nofollow(&path).is_err());
815 assert!(ensure_state_dir(&dir).is_err());
816 assert!(!path.exists());
817 }
818
819 fn temp_leftovers(dir: &Path, file_name: &str) -> Vec<String> {
820 let prefix = format!(".{file_name}.");
821 fs::read_dir(dir)
822 .map(|rd| {
823 rd.filter_map(|e| e.ok())
824 .filter_map(|e| e.file_name().to_str().map(str::to_owned))
825 .filter(|n| n.starts_with(&prefix))
826 .collect()
827 })
828 .unwrap_or_default()
829 }
830
831 #[test]
832 fn test_write_atomic_cleans_temp_on_rename_failure() {
833 let dir = tmpdir("rename_fail");
837 let dest = dir.join("dest");
838 fs::create_dir_all(&dest).unwrap();
839 fs::write(dest.join("keep"), b"x").unwrap();
840
841 assert!(write_atomic(&dest, b"boom").is_err());
842 assert_eq!(fs::read_to_string(dest.join("keep")).unwrap(), "x");
843 assert!(
844 temp_leftovers(&dir, "dest").is_empty(),
845 "temp file must be cleaned up"
846 );
847 }
848
849 #[test]
850 fn test_write_atomic_leaves_no_temp_on_success() {
851 let dir = tmpdir("no_temp_success");
852 let p = dir.join("out.txt");
853
854 write_atomic(&p, b"hello").unwrap();
855 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
856 assert!(
857 temp_leftovers(&dir, "out.txt").is_empty(),
858 "no temp left behind"
859 );
860 }
861
862 #[test]
863 fn test_write_atomic_retries_when_temp_name_exists() {
864 let dir = tmpdir("temp_collision");
865 let p = dir.join("out.txt");
866 let pid = std::process::id();
867 let collided = dir.join(format!(".out.txt.{pid}.0"));
868 fs::write(&collided, b"not mine").unwrap();
869
870 write_atomic(&p, b"hello").unwrap();
871 assert_eq!(fs::read_to_string(&p).unwrap(), "hello");
872 assert_eq!(fs::read_to_string(&collided).unwrap(), "not mine");
875 let leftovers = temp_leftovers(&dir, "out.txt");
876 assert_eq!(
877 leftovers.len(),
878 1,
879 "only the pre-existing colliding temp remains"
880 );
881 assert_eq!(leftovers[0], format!(".out.txt.{pid}.0"));
882 }
883
884 #[test]
885 fn test_ops_refuse_foreign_owned_writable_parent() {
886 use std::os::unix::fs::MetadataExt;
892 let euid = unsafe { libc::geteuid() };
893 if euid == 0 {
894 return;
895 }
896 let tmp = std::env::temp_dir();
897 let meta = match fs::symlink_metadata(&tmp) {
898 Ok(m) => m,
899 Err(_) => return,
900 };
901 if meta.uid() == euid {
902 return; }
904 if meta.mode() & 0o002 == 0 && meta.mode() & 0o020 == 0 {
905 return; }
907 let p = tmp.join(format!(
908 "coreshift_fs_foreign_{}_{}",
909 std::process::id(),
910 "out"
911 ));
912 let _ = fs::remove_file(&p);
913 fs::write(&p, b"probe").unwrap();
914
915 assert!(read_nofollow(&p).is_err());
916 assert!(open_append_nofollow(&p).is_err());
917 assert!(write_atomic(&p, b"boom").is_err());
918 assert!(remove_nofollow(&p).is_err());
919 assert!(ensure_state_dir(&tmp).is_err());
920 assert_eq!(fs::read_to_string(&p).unwrap(), "probe");
921 let _ = fs::remove_file(&p);
922 }
923
924 #[test]
925 fn test_ensure_state_dir_creates_fresh_dir() {
926 let base = tmpdir("fresh_base");
927 let nested = base.join("a").join("b").join("state");
928
929 let fd = ensure_state_dir(&nested).unwrap();
930 assert!(nested.is_dir());
931 drop(fd);
932 assert!(ensure_state_dir(&nested).is_ok());
933 }
934
935 #[test]
936 fn test_open_append_nofollow_refuses_dangling_symlink() {
937 let dir = tmpdir("dangling_append");
938 let missing = dir.join("not_there.txt");
939 let link = dir.join("linkd");
940 symlink(&missing, &link).unwrap();
941
942 assert!(open_append_nofollow(&link).is_err());
943 assert!(
944 !missing.exists(),
945 "must not create the target through a dangling link"
946 );
947 }
948
949 #[test]
950 fn test_read_nofollow_refuses_dangling_symlink() {
951 let dir = tmpdir("dangling_read");
952 let missing = dir.join("not_there2.txt");
953 let link = dir.join("linkd2");
954 symlink(&missing, &link).unwrap();
955
956 assert!(read_nofollow(&link).is_err());
957 assert!(!missing.exists());
958 }
959
960 #[test]
961 fn test_ops_refuse_regular_file_parent() {
962 let dir = tmpdir("regfile_parent");
965 let f = dir.join("notadir");
966 fs::write(&f, b"x").unwrap();
967
968 assert!(write_atomic(f.join("out"), b"y").is_err());
969 assert!(read_nofollow(f.join("out")).is_err());
970 assert!(open_append_nofollow(f.join("out")).is_err());
971 assert!(remove_nofollow(f.join("out")).is_err());
972 assert!(ensure_state_dir(&f).is_err());
973 assert_eq!(fs::read_to_string(&f).unwrap(), "x");
974 }
975}