1#[cfg(test)]
16use std::os::unix::ffi::OsStringExt;
17
18use std::ffi::{CString, OsStr, OsString};
19use std::fs;
20use std::io;
21use std::os::unix::ffi::OsStrExt;
22use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
23use std::path::{Path, PathBuf};
24
25use nix::dir::Dir;
26use nix::fcntl::{OFlag, openat};
27use nix::libc;
28use nix::sys::stat::{FchmodatFlags, FileStat, Mode, fchmodat, fstatat, mkdirat};
29use nix::unistd::{Gid, Uid, UnlinkatFlags, fchown, fchownat, unlinkat};
30use os_display::Quotable;
31
32use crate::translate;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum SymlinkBehavior {
40 #[default]
42 Follow,
43 NoFollow,
45}
46
47impl SymlinkBehavior {
48 #[inline]
50 pub fn should_follow(self) -> bool {
51 matches!(self, Self::Follow)
52 }
53}
54
55impl From<bool> for SymlinkBehavior {
56 fn from(follow: bool) -> Self {
57 if follow { Self::Follow } else { Self::NoFollow }
58 }
59}
60
61#[derive(thiserror::Error, Debug)]
63pub enum SafeTraversalError {
64 #[error("{}", translate!("safe-traversal-error-path-contains-null"))]
65 PathContainsNull,
66
67 #[error("{}", translate!("safe-traversal-error-open-failed", "path" => path.quote(), "source" => source))]
68 OpenFailed {
69 path: PathBuf,
70 #[source]
71 source: io::Error,
72 },
73
74 #[error("{}", translate!("safe-traversal-error-stat-failed", "path" => path.quote(), "source" => source))]
75 StatFailed {
76 path: PathBuf,
77 #[source]
78 source: io::Error,
79 },
80
81 #[error("{}", translate!("safe-traversal-error-read-dir-failed", "path" => path.quote(), "source" => source))]
82 ReadDirFailed {
83 path: PathBuf,
84 #[source]
85 source: io::Error,
86 },
87
88 #[error("{}", translate!("safe-traversal-error-unlink-failed", "path" => path.quote(), "source" => source))]
89 UnlinkFailed {
90 path: PathBuf,
91 #[source]
92 source: io::Error,
93 },
94}
95
96impl From<SafeTraversalError> for io::Error {
97 fn from(err: SafeTraversalError) -> Self {
98 match err {
99 SafeTraversalError::PathContainsNull => Self::new(
100 io::ErrorKind::InvalidInput,
101 translate!("safe-traversal-error-path-contains-null"),
102 ),
103 SafeTraversalError::OpenFailed { source, .. } => source,
104 SafeTraversalError::StatFailed { source, .. } => source,
105 SafeTraversalError::ReadDirFailed { source, .. } => source,
106 SafeTraversalError::UnlinkFailed { source, .. } => source,
107 }
108 }
109}
110
111fn read_dir_entries(fd: &OwnedFd) -> io::Result<Vec<OsString>> {
113 let mut entries = Vec::new();
114
115 let dup_fd = nix::unistd::dup(fd).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
117 let mut dir = Dir::from_fd(dup_fd).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
118 for entry_result in dir.iter() {
119 let entry = entry_result.map_err(|e| io::Error::from_raw_os_error(e as i32))?;
120 let name = entry.file_name();
121 let name_os = OsStr::from_bytes(name.to_bytes());
122 if name_os != "." && name_os != ".." {
123 entries.push(name_os.to_os_string());
124 }
125 }
126
127 Ok(entries)
128}
129
130pub struct DirFd {
132 fd: OwnedFd,
133}
134
135impl DirFd {
136 pub fn open(path: &Path, symlink_behavior: SymlinkBehavior) -> io::Result<Self> {
142 let mut flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC;
143 if !symlink_behavior.should_follow() {
144 flags |= OFlag::O_NOFOLLOW;
145 }
146 let fd = nix::fcntl::open(path, flags, Mode::empty()).map_err(|e| {
147 SafeTraversalError::OpenFailed {
148 path: path.into(),
149 source: io::Error::from_raw_os_error(e as i32),
150 }
151 })?;
152 Ok(Self { fd })
153 }
154
155 pub fn open_subdir(&self, name: &OsStr, symlink_behavior: SymlinkBehavior) -> io::Result<Self> {
161 let name_cstr =
162 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
163 let mut flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC;
164 if !symlink_behavior.should_follow() {
165 flags |= OFlag::O_NOFOLLOW;
166 }
167 let fd = openat(&self.fd, name_cstr.as_c_str(), flags, Mode::empty()).map_err(|e| {
168 SafeTraversalError::OpenFailed {
169 path: name.into(),
170 source: io::Error::from_raw_os_error(e as i32),
171 }
172 })?;
173 Ok(Self { fd })
174 }
175
176 pub fn stat_at(&self, name: &OsStr, symlink_behavior: SymlinkBehavior) -> io::Result<FileStat> {
178 let name_cstr =
179 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
180
181 let flags = if symlink_behavior.should_follow() {
182 nix::fcntl::AtFlags::empty()
183 } else {
184 nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW
185 };
186
187 let stat = fstatat(&self.fd, name_cstr.as_c_str(), flags).map_err(|e| {
188 SafeTraversalError::StatFailed {
189 path: name.into(),
190 source: io::Error::from_raw_os_error(e as i32),
191 }
192 })?;
193
194 Ok(stat)
195 }
196
197 pub fn metadata_at(
199 &self,
200 name: &OsStr,
201 symlink_behavior: SymlinkBehavior,
202 ) -> io::Result<Metadata> {
203 self.stat_at(name, symlink_behavior)
204 .map(Metadata::from_stat)
205 }
206
207 pub fn metadata(&self) -> io::Result<Metadata> {
209 self.fstat().map(Metadata::from_stat)
210 }
211
212 pub fn fstat(&self) -> io::Result<FileStat> {
214 let stat = nix::sys::stat::fstat(&self.fd).map_err(|e| SafeTraversalError::StatFailed {
215 path: translate!("safe-traversal-current-directory").into(),
216 source: io::Error::from_raw_os_error(e as i32),
217 })?;
218 Ok(stat)
219 }
220
221 pub fn read_dir(&self) -> io::Result<Vec<OsString>> {
223 read_dir_entries(&self.fd).map_err(|e| {
224 SafeTraversalError::ReadDirFailed {
225 path: translate!("safe-traversal-directory").into(),
226 source: e,
227 }
228 .into()
229 })
230 }
231
232 pub fn unlink_at(&self, name: &OsStr, is_dir: bool) -> io::Result<()> {
234 let name_cstr =
235 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
236 let flags = if is_dir {
237 UnlinkatFlags::RemoveDir
238 } else {
239 UnlinkatFlags::NoRemoveDir
240 };
241
242 unlinkat(&self.fd, name_cstr.as_c_str(), flags).map_err(|e| {
243 SafeTraversalError::UnlinkFailed {
244 path: name.into(),
245 source: io::Error::from_raw_os_error(e as i32),
246 }
247 })?;
248
249 Ok(())
250 }
251
252 pub fn chown_at(
255 &self,
256 name: &OsStr,
257 uid: Option<u32>,
258 gid: Option<u32>,
259 symlink_behavior: SymlinkBehavior,
260 ) -> io::Result<()> {
261 let name_cstr =
262 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
263
264 let flags = if symlink_behavior.should_follow() {
265 nix::fcntl::AtFlags::empty()
266 } else {
267 nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW
268 };
269
270 let uid = uid.map(Uid::from_raw);
271 let gid = gid.map(Gid::from_raw);
272
273 fchownat(&self.fd, name_cstr.as_c_str(), uid, gid, flags)
274 .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
275
276 Ok(())
277 }
278
279 pub fn fchown(&self, uid: Option<u32>, gid: Option<u32>) -> io::Result<()> {
281 let uid = uid.map(Uid::from_raw);
282 let gid = gid.map(Gid::from_raw);
283
284 fchown(&self.fd, uid, gid).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
285
286 Ok(())
287 }
288
289 pub fn chmod_at(
291 &self,
292 name: &OsStr,
293 mode: u32,
294 symlink_behavior: SymlinkBehavior,
295 ) -> io::Result<()> {
296 let name_cstr =
297 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
298
299 #[cfg(all(
306 target_os = "linux",
307 any(
308 target_arch = "x86_64",
309 target_arch = "x86",
310 target_arch = "arm",
311 target_arch = "aarch64",
312 target_arch = "riscv64",
313 target_arch = "riscv32",
314 ),
315 ))]
316 if matches!(symlink_behavior, SymlinkBehavior::NoFollow) {
317 use std::sync::atomic::{AtomicBool, Ordering};
318
319 static FCHMODAT2_UNAVAILABLE: AtomicBool = AtomicBool::new(false);
322
323 if !FCHMODAT2_UNAVAILABLE.load(Ordering::Relaxed) {
324 const SYS_FCHMODAT2: libc::c_long = 452;
326 let res = unsafe {
332 libc::syscall(
333 SYS_FCHMODAT2,
334 self.fd.as_raw_fd(),
335 name_cstr.as_ptr(),
336 mode as libc::mode_t,
337 libc::AT_SYMLINK_NOFOLLOW,
338 )
339 };
340 if res == 0 {
341 return Ok(());
342 }
343 let err = io::Error::last_os_error();
344 match err.raw_os_error() {
345 Some(libc::ENOSYS) => {
346 FCHMODAT2_UNAVAILABLE.store(true, Ordering::Relaxed);
347 }
349 _ => return Err(err),
350 }
351 }
352 }
353
354 let nix_mode = Mode::from_bits_truncate(mode as libc::mode_t);
357
358 let flags = if symlink_behavior.should_follow() {
359 FchmodatFlags::FollowSymlink
360 } else {
361 FchmodatFlags::NoFollowSymlink
362 };
363
364 match fchmodat(&self.fd, name_cstr.as_c_str(), nix_mode, flags) {
365 Ok(()) => Ok(()),
366 Err(e)
367 if !symlink_behavior.should_follow()
368 && (e == nix::errno::Errno::EOPNOTSUPP || e == nix::errno::Errno::ENOTSUP) =>
369 {
370 #[cfg(target_os = "linux")]
374 {
375 self.chmod_at_via_opath(name_cstr.as_c_str(), mode)
376 }
377 #[cfg(not(target_os = "linux"))]
378 {
379 Err(io::Error::from_raw_os_error(e as i32))
380 }
381 }
382 Err(e) => Err(io::Error::from_raw_os_error(e as i32)),
383 }
384 }
385
386 #[cfg(target_os = "linux")]
394 fn chmod_at_via_opath(&self, name: &std::ffi::CStr, mode: u32) -> io::Result<()> {
395 use rustix::fs::{Mode, OFlags, chmod, openat};
396
397 let fd = openat(
398 &self.fd,
399 name,
400 OFlags::PATH | OFlags::NOFOLLOW | OFlags::CLOEXEC,
401 Mode::empty(),
402 )
403 .map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))?;
404
405 let proc_path = format!("/proc/self/fd/{}\0", fd.as_raw_fd());
406 let proc_cstr = std::ffi::CStr::from_bytes_with_nul(proc_path.as_bytes())
407 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid proc path"))?;
408
409 chmod(proc_cstr, Mode::from_bits_truncate(mode))
410 .map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
411 }
412
413 pub fn fchmod(&self, mode: u32) -> io::Result<()> {
415 let mode = Mode::from_bits_truncate(mode as libc::mode_t);
416
417 nix::sys::stat::fchmod(&self.fd, mode)
418 .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
419
420 Ok(())
421 }
422
423 pub fn mkdir_at(&self, name: &OsStr, mode: u32) -> io::Result<()> {
425 let name_cstr =
426 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
427 let mode = Mode::from_bits_truncate(mode as libc::mode_t);
428
429 if let Err(e) = mkdirat(self.fd.as_fd(), name_cstr.as_c_str(), mode) {
430 let err = io::Error::from_raw_os_error(e as i32);
431 return Err(SafeTraversalError::OpenFailed {
432 path: name.into(),
433 source: err,
434 }
435 .into());
436 }
437 Ok(())
438 }
439
440 pub fn open_file_at(&self, name: &OsStr) -> io::Result<fs::File> {
443 let name_cstr =
444 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
445 let flags = OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_TRUNC | OFlag::O_CLOEXEC;
446 let mode = Mode::from_bits_truncate(0o666); let fd: OwnedFd = openat(self.fd.as_fd(), name_cstr.as_c_str(), flags, mode)
449 .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
450
451 Ok(fs::File::from(fd))
452 }
453
454 pub fn from_raw_fd(fd: RawFd) -> io::Result<Self> {
456 if fd < 0 {
457 return Err(io::Error::new(
458 io::ErrorKind::InvalidInput,
459 translate!("safe-traversal-error-invalid-fd"),
460 ));
461 }
462 let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) };
464 Ok(Self { fd: owned_fd })
465 }
466}
467
468fn find_existing_ancestor(path: &Path) -> io::Result<(PathBuf, Vec<OsString>)> {
474 let mut current = path.to_path_buf();
475 let mut components: Vec<OsString> = Vec::new();
476
477 loop {
478 match fs::metadata(¤t) {
481 Ok(meta) => {
482 if meta.is_dir() {
483 components.reverse();
485 return Ok((current, components));
486 }
487 if let Some(file_name) = current.file_name() {
489 components.push(file_name.to_os_string());
490 }
491 if let Some(parent) = current.parent() {
492 if parent.as_os_str().is_empty() {
493 components.reverse();
495 return Ok((PathBuf::from("."), components));
496 }
497 current = parent.to_path_buf();
498 } else {
499 let root = if path.is_absolute() {
501 PathBuf::from("/")
502 } else {
503 PathBuf::from(".")
504 };
505 components.reverse();
506 return Ok((root, components));
507 }
508 }
509 Err(e) if e.kind() == io::ErrorKind::NotFound => {
510 if let Some(file_name) = current.file_name() {
512 components.push(file_name.to_os_string());
513 }
514 if let Some(parent) = current.parent() {
515 if parent.as_os_str().is_empty() {
516 components.reverse();
518 return Ok((PathBuf::from("."), components));
519 }
520 current = parent.to_path_buf();
521 } else {
522 let root = if path.is_absolute() {
524 PathBuf::from("/")
525 } else {
526 PathBuf::from(".")
527 };
528 components.reverse();
529 return Ok((root, components));
530 }
531 }
532 Err(e) => return Err(e),
533 }
534 }
535}
536
537fn open_or_create_subdir(parent_fd: &DirFd, name: &OsStr, mode: u32) -> io::Result<DirFd> {
551 match parent_fd.stat_at(name, SymlinkBehavior::NoFollow) {
552 Ok(stat) => {
553 let file_type = (stat.st_mode as libc::mode_t) & libc::S_IFMT;
554 match file_type {
555 libc::S_IFDIR => parent_fd.open_subdir(name, SymlinkBehavior::NoFollow),
556 libc::S_IFLNK => {
557 parent_fd.open_subdir(name, SymlinkBehavior::Follow)
561 }
562 _ => Err(io::Error::new(
563 io::ErrorKind::AlreadyExists,
564 format!(
565 "path component exists but is not a directory: {}",
566 name.display()
567 ),
568 )),
569 }
570 }
571 Err(e) if e.kind() == io::ErrorKind::NotFound => {
572 parent_fd.mkdir_at(name, mode)?;
573 parent_fd.open_subdir(name, SymlinkBehavior::NoFollow)
574 }
575 Err(e) => Err(e),
576 }
577}
578
579#[cfg(unix)]
606pub fn create_dir_all_safe(path: &Path, mode: u32) -> io::Result<DirFd> {
607 let (existing_ancestor, components_to_create) = find_existing_ancestor(path)?;
608 let mut dir_fd = DirFd::open(&existing_ancestor, SymlinkBehavior::Follow)?;
609
610 for component in &components_to_create {
611 dir_fd = open_or_create_subdir(&dir_fd, component.as_os_str(), mode)?;
612 }
613
614 Ok(dir_fd)
615}
616
617impl AsRawFd for DirFd {
618 fn as_raw_fd(&self) -> RawFd {
619 self.fd.as_raw_fd()
620 }
621}
622
623impl AsFd for DirFd {
624 fn as_fd(&self) -> BorrowedFd<'_> {
625 self.fd.as_fd()
626 }
627}
628
629#[derive(Debug, Clone, Hash, PartialEq, Eq)]
631pub struct FileInfo {
632 pub dev: u64,
633 pub ino: u64,
634}
635
636impl FileInfo {
637 pub fn from_stat(stat: &libc::stat) -> Self {
638 #[allow(clippy::unnecessary_cast)]
640 Self {
641 dev: stat.st_dev as u64,
642 ino: stat.st_ino as u64,
643 }
644 }
645
646 pub fn new(dev: u64, ino: u64) -> Self {
648 Self { dev, ino }
649 }
650
651 pub fn device(&self) -> u64 {
653 self.dev
654 }
655
656 pub fn inode(&self) -> u64 {
658 self.ino
659 }
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq)]
664pub enum FileType {
665 Directory,
666 RegularFile,
667 Symlink,
668 Other,
669}
670
671impl FileType {
672 pub fn from_mode(mode: libc::mode_t) -> Self {
673 match mode & libc::S_IFMT {
674 libc::S_IFDIR => Self::Directory,
675 libc::S_IFREG => Self::RegularFile,
676 libc::S_IFLNK => Self::Symlink,
677 _ => Self::Other,
678 }
679 }
680
681 pub fn is_directory(self) -> bool {
682 matches!(self, Self::Directory)
683 }
684
685 pub fn is_regular_file(self) -> bool {
686 matches!(self, Self::RegularFile)
687 }
688
689 pub fn is_symlink(self) -> bool {
690 matches!(self, Self::Symlink)
691 }
692}
693
694#[derive(Debug, Clone)]
696pub struct Metadata {
697 stat: FileStat,
698}
699
700impl Metadata {
701 pub fn from_stat(stat: FileStat) -> Self {
702 Self { stat }
703 }
704
705 pub fn file_type(&self) -> FileType {
706 FileType::from_mode(self.stat.st_mode as libc::mode_t)
707 }
708
709 pub fn file_info(&self) -> FileInfo {
710 FileInfo::from_stat(&self.stat)
711 }
712
713 #[allow(clippy::unnecessary_cast)]
715 pub fn size(&self) -> u64 {
716 self.stat.st_size as u64
717 }
718
719 #[allow(clippy::unnecessary_cast)]
721 pub fn mode(&self) -> u32 {
722 self.stat.st_mode as u32
723 }
724
725 pub fn nlink(&self) -> u64 {
726 #[allow(clippy::unnecessary_cast)]
728 {
729 self.stat.st_nlink as u64
730 }
731 }
732
733 pub fn is_dir(&self) -> bool {
735 self.file_type().is_directory()
736 }
737
738 pub fn len(&self) -> u64 {
739 self.size()
740 }
741
742 pub fn is_empty(&self) -> bool {
743 self.len() == 0
744 }
745}
746
747impl std::os::unix::fs::MetadataExt for Metadata {
749 #[allow(clippy::unnecessary_cast)]
751 fn dev(&self) -> u64 {
752 self.stat.st_dev as u64
753 }
754
755 fn ino(&self) -> u64 {
756 #[allow(clippy::unnecessary_cast)]
758 {
759 self.stat.st_ino as u64
760 }
761 }
762
763 #[allow(clippy::unnecessary_cast)]
765 fn mode(&self) -> u32 {
766 self.stat.st_mode as u32
767 }
768
769 fn nlink(&self) -> u64 {
770 #[allow(clippy::unnecessary_cast)]
772 {
773 self.stat.st_nlink as u64
774 }
775 }
776
777 fn uid(&self) -> u32 {
778 self.stat.st_uid
779 }
780
781 fn gid(&self) -> u32 {
782 self.stat.st_gid
783 }
784
785 #[allow(clippy::unnecessary_cast)]
787 fn rdev(&self) -> u64 {
788 self.stat.st_rdev as u64
789 }
790
791 #[allow(clippy::unnecessary_cast)]
793 fn size(&self) -> u64 {
794 self.stat.st_size as u64
795 }
796
797 fn atime(&self) -> i64 {
798 #[cfg(target_pointer_width = "32")]
799 {
800 self.stat.st_atime.into()
801 }
802 #[cfg(not(target_pointer_width = "32"))]
803 {
804 self.stat.st_atime
805 }
806 }
807
808 fn atime_nsec(&self) -> i64 {
809 #[cfg(target_os = "netbsd")]
810 {
811 self.stat.st_atimensec
812 }
813
814 #[cfg(not(target_os = "netbsd"))]
815 {
816 #[cfg(target_pointer_width = "32")]
817 {
818 self.stat.st_atime_nsec.into()
819 }
820 #[cfg(not(target_pointer_width = "32"))]
821 {
822 self.stat.st_atime_nsec
823 }
824 }
825 }
826
827 fn mtime(&self) -> i64 {
828 #[cfg(target_pointer_width = "32")]
829 {
830 self.stat.st_mtime.into()
831 }
832 #[cfg(not(target_pointer_width = "32"))]
833 {
834 self.stat.st_mtime
835 }
836 }
837
838 fn mtime_nsec(&self) -> i64 {
839 #[cfg(target_os = "netbsd")]
840 {
841 self.stat.st_mtimensec
842 }
843
844 #[cfg(not(target_os = "netbsd"))]
845 {
846 #[cfg(target_pointer_width = "32")]
847 {
848 self.stat.st_mtime_nsec.into()
849 }
850 #[cfg(not(target_pointer_width = "32"))]
851 {
852 self.stat.st_mtime_nsec
853 }
854 }
855 }
856
857 fn ctime(&self) -> i64 {
858 #[cfg(target_pointer_width = "32")]
859 {
860 self.stat.st_ctime.into()
861 }
862 #[cfg(not(target_pointer_width = "32"))]
863 {
864 self.stat.st_ctime
865 }
866 }
867
868 fn ctime_nsec(&self) -> i64 {
869 #[cfg(target_os = "netbsd")]
870 {
871 self.stat.st_ctimensec
872 }
873
874 #[cfg(not(target_os = "netbsd"))]
875 {
876 #[cfg(target_pointer_width = "32")]
877 {
878 self.stat.st_ctime_nsec.into()
879 }
880 #[cfg(not(target_pointer_width = "32"))]
881 {
882 self.stat.st_ctime_nsec
883 }
884 }
885 }
886
887 #[allow(clippy::unnecessary_cast)]
889 fn blksize(&self) -> u64 {
890 self.stat.st_blksize as u64
891 }
892
893 #[allow(clippy::unnecessary_cast)]
895 fn blocks(&self) -> u64 {
896 self.stat.st_blocks as u64
897 }
898}
899
900#[cfg(test)]
901mod tests {
902 use super::*;
903 use std::fs;
904 use std::os::unix::fs::MetadataExt;
905 use std::os::unix::fs::symlink;
906 use std::os::unix::io::IntoRawFd;
907 use tempfile::TempDir;
908
909 #[test]
910 fn test_dirfd_open_valid_directory() {
911 let temp_dir = TempDir::new().unwrap();
912 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
913 assert!(dir_fd.as_raw_fd() >= 0);
914 }
915
916 #[test]
917 fn test_dirfd_open_nonexistent_directory() {
918 let result = DirFd::open("/nonexistent/path".as_ref(), SymlinkBehavior::Follow);
919 assert!(result.is_err());
920 if let Err(e) = result {
921 assert!(
923 e.kind() == io::ErrorKind::NotFound || e.kind() == io::ErrorKind::PermissionDenied
924 );
925 }
926 }
927
928 #[test]
929 fn test_dirfd_open_file_not_directory() {
930 let temp_dir = TempDir::new().unwrap();
931 let file_path = temp_dir.path().join("test_file");
932 fs::write(&file_path, "test content").unwrap();
933
934 let result = DirFd::open(&file_path, SymlinkBehavior::Follow);
935 assert!(result.is_err());
936 }
937
938 #[test]
939 fn test_dirfd_open_subdir() {
940 let temp_dir = TempDir::new().unwrap();
941 let subdir = temp_dir.path().join("subdir");
942 fs::create_dir(&subdir).unwrap();
943
944 let parent_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
945 let subdir_fd = parent_fd
946 .open_subdir(OsStr::new("subdir"), SymlinkBehavior::Follow)
947 .unwrap();
948 assert!(subdir_fd.as_raw_fd() >= 0);
949 }
950
951 #[test]
952 fn test_dirfd_open_nonexistent_subdir() {
953 let temp_dir = TempDir::new().unwrap();
954 let parent_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
955
956 let result = parent_fd.open_subdir(OsStr::new("nonexistent"), SymlinkBehavior::Follow);
957 assert!(result.is_err());
958 }
959
960 #[test]
961 fn test_dirfd_stat_at() {
962 let temp_dir = TempDir::new().unwrap();
963 let file_path = temp_dir.path().join("test_file");
964 fs::write(&file_path, "test content").unwrap();
965
966 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
967 let stat = dir_fd
968 .stat_at(OsStr::new("test_file"), SymlinkBehavior::Follow)
969 .unwrap();
970
971 assert!(stat.st_size > 0);
972 assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFREG);
973 }
974
975 #[test]
976 fn test_dirfd_stat_at_symlink() {
977 let temp_dir = TempDir::new().unwrap();
978 let target_file = temp_dir.path().join("target");
979 let symlink_file = temp_dir.path().join("link");
980
981 fs::write(&target_file, "target content").unwrap();
982 symlink(&target_file, &symlink_file).unwrap();
983
984 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
985
986 let stat_follow = dir_fd
988 .stat_at(OsStr::new("link"), SymlinkBehavior::Follow)
989 .unwrap();
990 assert_eq!(stat_follow.st_mode & libc::S_IFMT, libc::S_IFREG);
991
992 let stat_nofollow = dir_fd
994 .stat_at(OsStr::new("link"), SymlinkBehavior::NoFollow)
995 .unwrap();
996 assert_eq!(stat_nofollow.st_mode & libc::S_IFMT, libc::S_IFLNK);
997 }
998
999 #[test]
1000 fn test_dirfd_fstat() {
1001 let temp_dir = TempDir::new().unwrap();
1002 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1003 let stat = dir_fd.fstat().unwrap();
1004
1005 assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFDIR);
1006 }
1007
1008 #[test]
1009 fn test_dirfd_read_dir() {
1010 let temp_dir = TempDir::new().unwrap();
1011 let file1 = temp_dir.path().join("file1");
1012 let file2 = temp_dir.path().join("file2");
1013
1014 fs::write(&file1, "content1").unwrap();
1015 fs::write(&file2, "content2").unwrap();
1016
1017 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1018 let entries = dir_fd.read_dir().unwrap();
1019
1020 assert_eq!(entries.len(), 2);
1021 assert!(entries.contains(&OsString::from("file1")));
1022 assert!(entries.contains(&OsString::from("file2")));
1023 }
1024
1025 #[test]
1026 fn test_dirfd_unlink_at_file() {
1027 let temp_dir = TempDir::new().unwrap();
1028 let file_path = temp_dir.path().join("test_file");
1029 fs::write(&file_path, "test content").unwrap();
1030
1031 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1032 dir_fd.unlink_at(OsStr::new("test_file"), false).unwrap();
1033
1034 assert!(!file_path.exists());
1035 }
1036
1037 #[test]
1038 fn test_dirfd_unlink_at_directory() {
1039 let temp_dir = TempDir::new().unwrap();
1040 let subdir = temp_dir.path().join("empty_dir");
1041 fs::create_dir(&subdir).unwrap();
1042
1043 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1044 dir_fd.unlink_at(OsStr::new("empty_dir"), true).unwrap();
1045
1046 assert!(!subdir.exists());
1047 }
1048
1049 #[test]
1050 fn test_from_raw_fd() {
1051 let temp_dir = TempDir::new().unwrap();
1052 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1053
1054 let dup_fd = nix::unistd::dup(&dir_fd).unwrap();
1056 let from_raw_fd = DirFd::from_raw_fd(dup_fd.into_raw_fd()).unwrap();
1057
1058 let stat1 = dir_fd.fstat().unwrap();
1060 let stat2 = from_raw_fd.fstat().unwrap();
1061 assert_eq!(stat1.st_ino, stat2.st_ino);
1062 assert_eq!(stat1.st_dev, stat2.st_dev);
1063 }
1064
1065 #[test]
1066 fn test_from_raw_fd_invalid() {
1067 let result = DirFd::from_raw_fd(-1);
1068 assert!(result.is_err());
1069 }
1070
1071 #[test]
1072 #[allow(clippy::unnecessary_cast)]
1073 fn test_file_info() {
1074 let temp_dir = TempDir::new().unwrap();
1075 let file_path = temp_dir.path().join("test_file");
1076 fs::write(&file_path, "test content").unwrap();
1077
1078 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1079 let stat = dir_fd
1080 .stat_at(OsStr::new("test_file"), SymlinkBehavior::Follow)
1081 .unwrap();
1082 let file_info = FileInfo::from_stat(&stat);
1083 assert_eq!(file_info.device(), stat.st_dev as u64);
1084 assert_eq!(file_info.inode(), stat.st_ino as u64);
1085 }
1086
1087 #[test]
1088 fn test_file_info_new() {
1089 let file_info = FileInfo::new(123, 456);
1090 assert_eq!(file_info.device(), 123);
1091 assert_eq!(file_info.inode(), 456);
1092 }
1093
1094 #[test]
1095 fn test_file_type() {
1096 let dir_mode = libc::S_IFDIR | 0o755;
1098 let file_type = FileType::from_mode(dir_mode);
1099 assert_eq!(file_type, FileType::Directory);
1100 assert!(file_type.is_directory());
1101 assert!(!file_type.is_regular_file());
1102 assert!(!file_type.is_symlink());
1103
1104 let file_mode = libc::S_IFREG | 0o644;
1106 let file_type = FileType::from_mode(file_mode);
1107 assert_eq!(file_type, FileType::RegularFile);
1108 assert!(!file_type.is_directory());
1109 assert!(file_type.is_regular_file());
1110 assert!(!file_type.is_symlink());
1111
1112 let link_mode = libc::S_IFLNK | 0o777;
1114 let file_type = FileType::from_mode(link_mode);
1115 assert_eq!(file_type, FileType::Symlink);
1116 assert!(!file_type.is_directory());
1117 assert!(!file_type.is_regular_file());
1118 assert!(file_type.is_symlink());
1119 }
1120
1121 #[test]
1122 #[allow(clippy::unnecessary_cast)]
1123 fn test_metadata_wrapper() {
1124 let temp_dir = TempDir::new().unwrap();
1125 let file_path = temp_dir.path().join("test_file");
1126 fs::write(&file_path, "test content with some length").unwrap();
1127
1128 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1129 let metadata = dir_fd
1130 .metadata_at(OsStr::new("test_file"), SymlinkBehavior::Follow)
1131 .unwrap();
1132
1133 assert_eq!(metadata.file_type(), FileType::RegularFile);
1134 assert!(metadata.size() > 0);
1135 assert_eq!(metadata.mode() & libc::S_IFMT as u32, libc::S_IFREG as u32);
1136 assert_eq!(metadata.nlink(), 1);
1137
1138 assert!(metadata.size() > 0);
1139 }
1140
1141 #[test]
1142 fn test_metadata_directory() {
1143 let temp_dir = TempDir::new().unwrap();
1144 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1145 let metadata = dir_fd.metadata().unwrap();
1146
1147 assert_eq!(metadata.file_type(), FileType::Directory);
1148 assert!(metadata.file_type().is_directory());
1149 }
1150
1151 #[test]
1152 fn test_path_with_null_byte() {
1153 let path_with_null = OsString::from_vec(b"test\0file".to_vec());
1154 let temp_dir = TempDir::new().unwrap();
1155 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1156
1157 let result = dir_fd.open_subdir(&path_with_null, SymlinkBehavior::Follow);
1158 assert!(result.is_err());
1159 if let Err(e) = result {
1160 assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
1162 }
1163 }
1164
1165 #[test]
1166 fn test_error_chain() {
1167 let result = DirFd::open(
1168 "/nonexistent/deeply/nested/path".as_ref(),
1169 SymlinkBehavior::Follow,
1170 );
1171 assert!(result.is_err());
1172
1173 if let Err(e) = result {
1174 let io_err: io::Error = e;
1176 assert!(
1177 io_err.kind() == io::ErrorKind::NotFound
1178 || io_err.kind() == io::ErrorKind::PermissionDenied
1179 );
1180 }
1181 }
1182
1183 #[test]
1184 fn test_mkdir_at_creates_directory() {
1185 let temp_dir = TempDir::new().unwrap();
1186 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1187
1188 dir_fd.mkdir_at(OsStr::new("new_subdir"), 0o755).unwrap();
1189
1190 assert!(temp_dir.path().join("new_subdir").is_dir());
1191 }
1192
1193 #[test]
1194 fn test_mkdir_at_fails_if_exists() {
1195 let temp_dir = TempDir::new().unwrap();
1196 let subdir = temp_dir.path().join("existing");
1197 fs::create_dir(&subdir).unwrap();
1198
1199 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1200 let result = dir_fd.mkdir_at(OsStr::new("existing"), 0o755);
1201
1202 assert!(result.is_err());
1203 }
1204
1205 #[test]
1206 fn test_open_file_at_creates_file() {
1207 use std::io::Write;
1208
1209 let temp_dir = TempDir::new().unwrap();
1210 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1211
1212 let mut file = dir_fd.open_file_at(OsStr::new("new_file.txt")).unwrap();
1213 file.write_all(b"test content").unwrap();
1214
1215 let content = fs::read_to_string(temp_dir.path().join("new_file.txt")).unwrap();
1216 assert_eq!(content, "test content");
1217 }
1218
1219 #[test]
1220 fn test_open_file_at_truncates_existing() {
1221 use std::io::Write;
1222
1223 let temp_dir = TempDir::new().unwrap();
1224 let file_path = temp_dir.path().join("existing.txt");
1225 fs::write(&file_path, "old content that is longer").unwrap();
1226
1227 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1228 let mut file = dir_fd.open_file_at(OsStr::new("existing.txt")).unwrap();
1229 file.write_all(b"new").unwrap();
1230 drop(file);
1231
1232 let content = fs::read_to_string(&file_path).unwrap();
1233 assert_eq!(content, "new");
1234 }
1235
1236 #[test]
1237 fn test_create_dir_all_safe_creates_nested_dirs() {
1238 let temp_dir = TempDir::new().unwrap();
1239 let nested_path = temp_dir.path().join("a/b/c");
1240
1241 let dir_fd = create_dir_all_safe(&nested_path, 0o755).unwrap();
1242 assert!(dir_fd.as_raw_fd() >= 0);
1243 assert!(nested_path.is_dir());
1244 }
1245
1246 #[test]
1247 fn test_create_dir_all_safe_existing_path() {
1248 let temp_dir = TempDir::new().unwrap();
1249 let existing_path = temp_dir.path().join("existing");
1250 fs::create_dir(&existing_path).unwrap();
1251
1252 let dir_fd = create_dir_all_safe(&existing_path, 0o755).unwrap();
1253 assert!(dir_fd.as_raw_fd() >= 0);
1254 }
1255
1256 #[test]
1257 fn test_create_dir_all_safe_follows_symlink() {
1258 let temp_dir = TempDir::new().unwrap();
1259 let target_dir = temp_dir.path().join("target");
1260 fs::create_dir(&target_dir).unwrap();
1261
1262 let symlink_path = temp_dir.path().join("link");
1264 symlink(&target_dir, &symlink_path).unwrap();
1265 assert!(symlink_path.is_symlink());
1266
1267 let dir_fd = create_dir_all_safe(&symlink_path, 0o755).unwrap();
1269 assert!(dir_fd.as_raw_fd() >= 0);
1270
1271 assert!(symlink_path.is_symlink());
1273 assert!(symlink_path.is_dir()); }
1275
1276 #[test]
1277 fn test_create_dir_all_safe_fails_on_file() {
1278 let temp_dir = TempDir::new().unwrap();
1279 let file_path = temp_dir.path().join("file");
1280 fs::write(&file_path, "content").unwrap();
1281
1282 let result = create_dir_all_safe(&file_path, 0o755);
1283 assert!(result.is_err());
1284 }
1285
1286 #[test]
1287 fn test_create_dir_all_safe_nested_symlink_in_path() {
1288 let temp_dir = TempDir::new().unwrap();
1289
1290 let parent = temp_dir.path().join("parent");
1293 let target = temp_dir.path().join("target");
1294 fs::create_dir(&parent).unwrap();
1295 fs::create_dir(&target).unwrap();
1296
1297 let symlink_in_path = parent.join("link");
1298 symlink(&target, &symlink_in_path).unwrap();
1299
1300 let nested_path = symlink_in_path.join("subdir");
1302 let dir_fd = create_dir_all_safe(&nested_path, 0o755).unwrap();
1303 assert!(dir_fd.as_raw_fd() >= 0);
1304
1305 assert!(symlink_in_path.is_symlink());
1307 assert!(symlink_in_path.is_dir()); assert!(target.join("subdir").exists());
1311 }
1312
1313 #[test]
1314 fn test_open_subdir_nofollow_fails_on_symlink() {
1315 let temp_dir = TempDir::new().unwrap();
1316 let target = temp_dir.path().join("target");
1317 fs::create_dir(&target).unwrap();
1318
1319 let link = temp_dir.path().join("link");
1320 symlink(&target, &link).unwrap();
1321
1322 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1323
1324 let result_follow = dir_fd.open_subdir(OsStr::new("link"), SymlinkBehavior::Follow);
1326 assert!(result_follow.is_ok());
1327
1328 let result_nofollow = dir_fd.open_subdir(OsStr::new("link"), SymlinkBehavior::NoFollow);
1330 assert!(result_nofollow.is_err());
1331 }
1332
1333 #[test]
1337 fn test_chmod_at_nofollow_preserves_target_mode() {
1338 let temp_dir = TempDir::new().unwrap();
1339
1340 let sentinel = temp_dir.path().join("sentinel");
1342 fs::write(&sentinel, "victim").unwrap();
1343 let sentinel_mode = fs::symlink_metadata(&sentinel).unwrap().mode();
1344
1345 let subdir = temp_dir.path().join("subdir");
1347 fs::create_dir(&subdir).unwrap();
1348 let link = subdir.join("link");
1349 symlink(&sentinel, &link).unwrap();
1350
1351 let dir_fd = DirFd::open(&subdir, SymlinkBehavior::Follow).unwrap();
1353 let result = dir_fd.chmod_at(OsStr::new("link"), 0o777, SymlinkBehavior::NoFollow);
1354
1355 if let Ok(()) = result {
1359 let new_sentinel_mode = fs::symlink_metadata(&sentinel).unwrap().mode();
1361 assert_eq!(
1362 new_sentinel_mode, sentinel_mode,
1363 "sentinel mode should not change when chmod'ing symlink with NoFollow"
1364 );
1365 }
1366 }
1369
1370 #[test]
1371 fn test_open_nofollow_fails_on_symlink() {
1372 let temp_dir = TempDir::new().unwrap();
1373 let target = temp_dir.path().join("target");
1374 fs::create_dir(&target).unwrap();
1375
1376 let link = temp_dir.path().join("link");
1377 symlink(&target, &link).unwrap();
1378
1379 let result_follow = DirFd::open(&link, SymlinkBehavior::Follow);
1381 assert!(result_follow.is_ok());
1382
1383 let result_nofollow = DirFd::open(&link, SymlinkBehavior::NoFollow);
1385 assert!(result_nofollow.is_err());
1386 }
1387}