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, IntoRawFd, 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 flags = if symlink_behavior.should_follow() {
297 FchmodatFlags::FollowSymlink
298 } else {
299 FchmodatFlags::NoFollowSymlink
300 };
301
302 let mode = Mode::from_bits_truncate(mode as libc::mode_t);
303
304 let name_cstr =
305 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
306
307 fchmodat(&self.fd, name_cstr.as_c_str(), mode, flags)
308 .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
309
310 Ok(())
311 }
312
313 pub fn fchmod(&self, mode: u32) -> io::Result<()> {
315 let mode = Mode::from_bits_truncate(mode as libc::mode_t);
316
317 nix::sys::stat::fchmod(&self.fd, mode)
318 .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
319
320 Ok(())
321 }
322
323 pub fn mkdir_at(&self, name: &OsStr, mode: u32) -> io::Result<()> {
325 let name_cstr =
326 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
327 let mode = Mode::from_bits_truncate(mode as libc::mode_t);
328
329 if let Err(e) = mkdirat(self.fd.as_fd(), name_cstr.as_c_str(), mode) {
330 let err = io::Error::from_raw_os_error(e as i32);
331 return Err(SafeTraversalError::OpenFailed {
332 path: name.into(),
333 source: err,
334 }
335 .into());
336 }
337 Ok(())
338 }
339
340 pub fn open_file_at(&self, name: &OsStr) -> io::Result<fs::File> {
343 let name_cstr =
344 CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
345 let flags = OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_TRUNC | OFlag::O_CLOEXEC;
346 let mode = Mode::from_bits_truncate(0o666); let fd: OwnedFd = openat(self.fd.as_fd(), name_cstr.as_c_str(), flags, mode)
349 .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
350
351 let raw_fd = fd.into_raw_fd();
353 Ok(unsafe { fs::File::from_raw_fd(raw_fd) })
354 }
355
356 pub fn from_raw_fd(fd: RawFd) -> io::Result<Self> {
358 if fd < 0 {
359 return Err(io::Error::new(
360 io::ErrorKind::InvalidInput,
361 translate!("safe-traversal-error-invalid-fd"),
362 ));
363 }
364 let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) };
366 Ok(Self { fd: owned_fd })
367 }
368}
369
370fn find_existing_ancestor(path: &Path) -> io::Result<(PathBuf, Vec<OsString>)> {
376 let mut current = path.to_path_buf();
377 let mut components: Vec<OsString> = Vec::new();
378
379 loop {
380 match fs::symlink_metadata(¤t) {
382 Ok(meta) => {
383 if meta.is_dir() && !meta.file_type().is_symlink() {
384 components.reverse();
386 return Ok((current, components));
387 }
388 if let Some(file_name) = current.file_name() {
391 components.push(file_name.to_os_string());
392 }
393 if let Some(parent) = current.parent() {
394 if parent.as_os_str().is_empty() {
395 components.reverse();
397 return Ok((PathBuf::from("."), components));
398 }
399 current = parent.to_path_buf();
400 } else {
401 let root = if path.is_absolute() {
403 PathBuf::from("/")
404 } else {
405 PathBuf::from(".")
406 };
407 components.reverse();
408 return Ok((root, components));
409 }
410 }
411 Err(e) if e.kind() == io::ErrorKind::NotFound => {
412 if let Some(file_name) = current.file_name() {
414 components.push(file_name.to_os_string());
415 }
416 if let Some(parent) = current.parent() {
417 if parent.as_os_str().is_empty() {
418 components.reverse();
420 return Ok((PathBuf::from("."), components));
421 }
422 current = parent.to_path_buf();
423 } else {
424 let root = if path.is_absolute() {
426 PathBuf::from("/")
427 } else {
428 PathBuf::from(".")
429 };
430 components.reverse();
431 return Ok((root, components));
432 }
433 }
434 Err(e) => return Err(e),
435 }
436 }
437}
438
439fn open_or_create_subdir(parent_fd: &DirFd, name: &OsStr, mode: u32) -> io::Result<DirFd> {
453 match parent_fd.stat_at(name, SymlinkBehavior::NoFollow) {
454 Ok(stat) => {
455 let file_type = (stat.st_mode as libc::mode_t) & libc::S_IFMT;
456 match file_type {
457 libc::S_IFDIR => parent_fd.open_subdir(name, SymlinkBehavior::NoFollow),
458 libc::S_IFLNK => {
459 parent_fd.unlink_at(name, false)?;
460 parent_fd.mkdir_at(name, mode)?;
461 parent_fd.open_subdir(name, SymlinkBehavior::NoFollow)
462 }
463 _ => Err(io::Error::new(
464 io::ErrorKind::AlreadyExists,
465 format!(
466 "path component exists but is not a directory: {}",
467 name.display()
468 ),
469 )),
470 }
471 }
472 Err(e) if e.kind() == io::ErrorKind::NotFound => {
473 parent_fd.mkdir_at(name, mode)?;
474 parent_fd.open_subdir(name, SymlinkBehavior::NoFollow)
475 }
476 Err(e) => Err(e),
477 }
478}
479
480#[cfg(unix)]
505pub fn create_dir_all_safe(path: &Path, mode: u32) -> io::Result<DirFd> {
506 let (existing_ancestor, components_to_create) = find_existing_ancestor(path)?;
507 let mut dir_fd = DirFd::open(&existing_ancestor, SymlinkBehavior::Follow)?;
508
509 for component in &components_to_create {
510 dir_fd = open_or_create_subdir(&dir_fd, component.as_os_str(), mode)?;
511 }
512
513 Ok(dir_fd)
514}
515
516impl AsRawFd for DirFd {
517 fn as_raw_fd(&self) -> RawFd {
518 self.fd.as_raw_fd()
519 }
520}
521
522impl AsFd for DirFd {
523 fn as_fd(&self) -> BorrowedFd<'_> {
524 self.fd.as_fd()
525 }
526}
527
528#[derive(Debug, Clone, Hash, PartialEq, Eq)]
530pub struct FileInfo {
531 pub dev: u64,
532 pub ino: u64,
533}
534
535impl FileInfo {
536 pub fn from_stat(stat: &libc::stat) -> Self {
537 #[allow(clippy::unnecessary_cast)]
539 Self {
540 dev: stat.st_dev as u64,
541 ino: stat.st_ino as u64,
542 }
543 }
544
545 pub fn new(dev: u64, ino: u64) -> Self {
547 Self { dev, ino }
548 }
549
550 pub fn device(&self) -> u64 {
552 self.dev
553 }
554
555 pub fn inode(&self) -> u64 {
557 self.ino
558 }
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
563pub enum FileType {
564 Directory,
565 RegularFile,
566 Symlink,
567 Other,
568}
569
570impl FileType {
571 pub fn from_mode(mode: libc::mode_t) -> Self {
572 match mode & libc::S_IFMT {
573 libc::S_IFDIR => Self::Directory,
574 libc::S_IFREG => Self::RegularFile,
575 libc::S_IFLNK => Self::Symlink,
576 _ => Self::Other,
577 }
578 }
579
580 pub fn is_directory(self) -> bool {
581 matches!(self, Self::Directory)
582 }
583
584 pub fn is_regular_file(self) -> bool {
585 matches!(self, Self::RegularFile)
586 }
587
588 pub fn is_symlink(self) -> bool {
589 matches!(self, Self::Symlink)
590 }
591}
592
593#[derive(Debug, Clone)]
595pub struct Metadata {
596 stat: FileStat,
597}
598
599impl Metadata {
600 pub fn from_stat(stat: FileStat) -> Self {
601 Self { stat }
602 }
603
604 pub fn file_type(&self) -> FileType {
605 FileType::from_mode(self.stat.st_mode as libc::mode_t)
606 }
607
608 pub fn file_info(&self) -> FileInfo {
609 FileInfo::from_stat(&self.stat)
610 }
611
612 #[allow(clippy::unnecessary_cast)]
614 pub fn size(&self) -> u64 {
615 self.stat.st_size as u64
616 }
617
618 #[allow(clippy::unnecessary_cast)]
620 pub fn mode(&self) -> u32 {
621 self.stat.st_mode as u32
622 }
623
624 pub fn nlink(&self) -> u64 {
625 #[allow(clippy::unnecessary_cast)]
627 {
628 self.stat.st_nlink as u64
629 }
630 }
631
632 pub fn is_dir(&self) -> bool {
634 self.file_type().is_directory()
635 }
636
637 pub fn len(&self) -> u64 {
638 self.size()
639 }
640
641 pub fn is_empty(&self) -> bool {
642 self.len() == 0
643 }
644}
645
646impl std::os::unix::fs::MetadataExt for Metadata {
648 #[allow(clippy::unnecessary_cast)]
650 fn dev(&self) -> u64 {
651 self.stat.st_dev as u64
652 }
653
654 fn ino(&self) -> u64 {
655 #[allow(clippy::unnecessary_cast)]
657 {
658 self.stat.st_ino as u64
659 }
660 }
661
662 #[allow(clippy::unnecessary_cast)]
664 fn mode(&self) -> u32 {
665 self.stat.st_mode as u32
666 }
667
668 fn nlink(&self) -> u64 {
669 #[allow(clippy::unnecessary_cast)]
671 {
672 self.stat.st_nlink as u64
673 }
674 }
675
676 fn uid(&self) -> u32 {
677 self.stat.st_uid
678 }
679
680 fn gid(&self) -> u32 {
681 self.stat.st_gid
682 }
683
684 #[allow(clippy::unnecessary_cast)]
686 fn rdev(&self) -> u64 {
687 self.stat.st_rdev as u64
688 }
689
690 #[allow(clippy::unnecessary_cast)]
692 fn size(&self) -> u64 {
693 self.stat.st_size as u64
694 }
695
696 fn atime(&self) -> i64 {
697 #[cfg(target_pointer_width = "32")]
698 {
699 self.stat.st_atime.into()
700 }
701 #[cfg(not(target_pointer_width = "32"))]
702 {
703 self.stat.st_atime
704 }
705 }
706
707 fn atime_nsec(&self) -> i64 {
708 #[cfg(target_os = "netbsd")]
709 {
710 self.stat.st_atimensec
711 }
712
713 #[cfg(not(target_os = "netbsd"))]
714 {
715 #[cfg(target_pointer_width = "32")]
716 {
717 self.stat.st_atime_nsec.into()
718 }
719 #[cfg(not(target_pointer_width = "32"))]
720 {
721 self.stat.st_atime_nsec
722 }
723 }
724 }
725
726 fn mtime(&self) -> i64 {
727 #[cfg(target_pointer_width = "32")]
728 {
729 self.stat.st_mtime.into()
730 }
731 #[cfg(not(target_pointer_width = "32"))]
732 {
733 self.stat.st_mtime
734 }
735 }
736
737 fn mtime_nsec(&self) -> i64 {
738 #[cfg(target_os = "netbsd")]
739 {
740 self.stat.st_mtimensec
741 }
742
743 #[cfg(not(target_os = "netbsd"))]
744 {
745 #[cfg(target_pointer_width = "32")]
746 {
747 self.stat.st_mtime_nsec.into()
748 }
749 #[cfg(not(target_pointer_width = "32"))]
750 {
751 self.stat.st_mtime_nsec
752 }
753 }
754 }
755
756 fn ctime(&self) -> i64 {
757 #[cfg(target_pointer_width = "32")]
758 {
759 self.stat.st_ctime.into()
760 }
761 #[cfg(not(target_pointer_width = "32"))]
762 {
763 self.stat.st_ctime
764 }
765 }
766
767 fn ctime_nsec(&self) -> i64 {
768 #[cfg(target_os = "netbsd")]
769 {
770 self.stat.st_ctimensec
771 }
772
773 #[cfg(not(target_os = "netbsd"))]
774 {
775 #[cfg(target_pointer_width = "32")]
776 {
777 self.stat.st_ctime_nsec.into()
778 }
779 #[cfg(not(target_pointer_width = "32"))]
780 {
781 self.stat.st_ctime_nsec
782 }
783 }
784 }
785
786 #[allow(clippy::unnecessary_cast)]
788 fn blksize(&self) -> u64 {
789 self.stat.st_blksize as u64
790 }
791
792 #[allow(clippy::unnecessary_cast)]
794 fn blocks(&self) -> u64 {
795 self.stat.st_blocks as u64
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use std::fs;
803 use std::os::unix::fs::symlink;
804 use std::os::unix::io::IntoRawFd;
805 use tempfile::TempDir;
806
807 #[test]
808 fn test_dirfd_open_valid_directory() {
809 let temp_dir = TempDir::new().unwrap();
810 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
811 assert!(dir_fd.as_raw_fd() >= 0);
812 }
813
814 #[test]
815 fn test_dirfd_open_nonexistent_directory() {
816 let result = DirFd::open("/nonexistent/path".as_ref(), SymlinkBehavior::Follow);
817 assert!(result.is_err());
818 if let Err(e) = result {
819 assert!(
821 e.kind() == io::ErrorKind::NotFound || e.kind() == io::ErrorKind::PermissionDenied
822 );
823 }
824 }
825
826 #[test]
827 fn test_dirfd_open_file_not_directory() {
828 let temp_dir = TempDir::new().unwrap();
829 let file_path = temp_dir.path().join("test_file");
830 fs::write(&file_path, "test content").unwrap();
831
832 let result = DirFd::open(&file_path, SymlinkBehavior::Follow);
833 assert!(result.is_err());
834 }
835
836 #[test]
837 fn test_dirfd_open_subdir() {
838 let temp_dir = TempDir::new().unwrap();
839 let subdir = temp_dir.path().join("subdir");
840 fs::create_dir(&subdir).unwrap();
841
842 let parent_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
843 let subdir_fd = parent_fd
844 .open_subdir(OsStr::new("subdir"), SymlinkBehavior::Follow)
845 .unwrap();
846 assert!(subdir_fd.as_raw_fd() >= 0);
847 }
848
849 #[test]
850 fn test_dirfd_open_nonexistent_subdir() {
851 let temp_dir = TempDir::new().unwrap();
852 let parent_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
853
854 let result = parent_fd.open_subdir(OsStr::new("nonexistent"), SymlinkBehavior::Follow);
855 assert!(result.is_err());
856 }
857
858 #[test]
859 fn test_dirfd_stat_at() {
860 let temp_dir = TempDir::new().unwrap();
861 let file_path = temp_dir.path().join("test_file");
862 fs::write(&file_path, "test content").unwrap();
863
864 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
865 let stat = dir_fd
866 .stat_at(OsStr::new("test_file"), SymlinkBehavior::Follow)
867 .unwrap();
868
869 assert!(stat.st_size > 0);
870 assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFREG);
871 }
872
873 #[test]
874 fn test_dirfd_stat_at_symlink() {
875 let temp_dir = TempDir::new().unwrap();
876 let target_file = temp_dir.path().join("target");
877 let symlink_file = temp_dir.path().join("link");
878
879 fs::write(&target_file, "target content").unwrap();
880 symlink(&target_file, &symlink_file).unwrap();
881
882 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
883
884 let stat_follow = dir_fd
886 .stat_at(OsStr::new("link"), SymlinkBehavior::Follow)
887 .unwrap();
888 assert_eq!(stat_follow.st_mode & libc::S_IFMT, libc::S_IFREG);
889
890 let stat_nofollow = dir_fd
892 .stat_at(OsStr::new("link"), SymlinkBehavior::NoFollow)
893 .unwrap();
894 assert_eq!(stat_nofollow.st_mode & libc::S_IFMT, libc::S_IFLNK);
895 }
896
897 #[test]
898 fn test_dirfd_fstat() {
899 let temp_dir = TempDir::new().unwrap();
900 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
901 let stat = dir_fd.fstat().unwrap();
902
903 assert_eq!(stat.st_mode & libc::S_IFMT, libc::S_IFDIR);
904 }
905
906 #[test]
907 fn test_dirfd_read_dir() {
908 let temp_dir = TempDir::new().unwrap();
909 let file1 = temp_dir.path().join("file1");
910 let file2 = temp_dir.path().join("file2");
911
912 fs::write(&file1, "content1").unwrap();
913 fs::write(&file2, "content2").unwrap();
914
915 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
916 let entries = dir_fd.read_dir().unwrap();
917
918 assert_eq!(entries.len(), 2);
919 assert!(entries.contains(&OsString::from("file1")));
920 assert!(entries.contains(&OsString::from("file2")));
921 }
922
923 #[test]
924 fn test_dirfd_unlink_at_file() {
925 let temp_dir = TempDir::new().unwrap();
926 let file_path = temp_dir.path().join("test_file");
927 fs::write(&file_path, "test content").unwrap();
928
929 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
930 dir_fd.unlink_at(OsStr::new("test_file"), false).unwrap();
931
932 assert!(!file_path.exists());
933 }
934
935 #[test]
936 fn test_dirfd_unlink_at_directory() {
937 let temp_dir = TempDir::new().unwrap();
938 let subdir = temp_dir.path().join("empty_dir");
939 fs::create_dir(&subdir).unwrap();
940
941 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
942 dir_fd.unlink_at(OsStr::new("empty_dir"), true).unwrap();
943
944 assert!(!subdir.exists());
945 }
946
947 #[test]
948 fn test_from_raw_fd() {
949 let temp_dir = TempDir::new().unwrap();
950 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
951
952 let dup_fd = nix::unistd::dup(&dir_fd).unwrap();
954 let from_raw_fd = DirFd::from_raw_fd(dup_fd.into_raw_fd()).unwrap();
955
956 let stat1 = dir_fd.fstat().unwrap();
958 let stat2 = from_raw_fd.fstat().unwrap();
959 assert_eq!(stat1.st_ino, stat2.st_ino);
960 assert_eq!(stat1.st_dev, stat2.st_dev);
961 }
962
963 #[test]
964 fn test_from_raw_fd_invalid() {
965 let result = DirFd::from_raw_fd(-1);
966 assert!(result.is_err());
967 }
968
969 #[test]
970 #[allow(clippy::unnecessary_cast)]
971 fn test_file_info() {
972 let temp_dir = TempDir::new().unwrap();
973 let file_path = temp_dir.path().join("test_file");
974 fs::write(&file_path, "test content").unwrap();
975
976 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
977 let stat = dir_fd
978 .stat_at(OsStr::new("test_file"), SymlinkBehavior::Follow)
979 .unwrap();
980 let file_info = FileInfo::from_stat(&stat);
981 assert_eq!(file_info.device(), stat.st_dev as u64);
982 assert_eq!(file_info.inode(), stat.st_ino as u64);
983 }
984
985 #[test]
986 fn test_file_info_new() {
987 let file_info = FileInfo::new(123, 456);
988 assert_eq!(file_info.device(), 123);
989 assert_eq!(file_info.inode(), 456);
990 }
991
992 #[test]
993 fn test_file_type() {
994 let dir_mode = libc::S_IFDIR | 0o755;
996 let file_type = FileType::from_mode(dir_mode);
997 assert_eq!(file_type, FileType::Directory);
998 assert!(file_type.is_directory());
999 assert!(!file_type.is_regular_file());
1000 assert!(!file_type.is_symlink());
1001
1002 let file_mode = libc::S_IFREG | 0o644;
1004 let file_type = FileType::from_mode(file_mode);
1005 assert_eq!(file_type, FileType::RegularFile);
1006 assert!(!file_type.is_directory());
1007 assert!(file_type.is_regular_file());
1008 assert!(!file_type.is_symlink());
1009
1010 let link_mode = libc::S_IFLNK | 0o777;
1012 let file_type = FileType::from_mode(link_mode);
1013 assert_eq!(file_type, FileType::Symlink);
1014 assert!(!file_type.is_directory());
1015 assert!(!file_type.is_regular_file());
1016 assert!(file_type.is_symlink());
1017 }
1018
1019 #[test]
1020 #[allow(clippy::unnecessary_cast)]
1021 fn test_metadata_wrapper() {
1022 let temp_dir = TempDir::new().unwrap();
1023 let file_path = temp_dir.path().join("test_file");
1024 fs::write(&file_path, "test content with some length").unwrap();
1025
1026 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1027 let metadata = dir_fd
1028 .metadata_at(OsStr::new("test_file"), SymlinkBehavior::Follow)
1029 .unwrap();
1030
1031 assert_eq!(metadata.file_type(), FileType::RegularFile);
1032 assert!(metadata.size() > 0);
1033 assert_eq!(metadata.mode() & libc::S_IFMT as u32, libc::S_IFREG as u32);
1034 assert_eq!(metadata.nlink(), 1);
1035
1036 assert!(metadata.size() > 0);
1037 }
1038
1039 #[test]
1040 fn test_metadata_directory() {
1041 let temp_dir = TempDir::new().unwrap();
1042 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1043 let metadata = dir_fd.metadata().unwrap();
1044
1045 assert_eq!(metadata.file_type(), FileType::Directory);
1046 assert!(metadata.file_type().is_directory());
1047 }
1048
1049 #[test]
1050 fn test_path_with_null_byte() {
1051 let path_with_null = OsString::from_vec(b"test\0file".to_vec());
1052 let temp_dir = TempDir::new().unwrap();
1053 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1054
1055 let result = dir_fd.open_subdir(&path_with_null, SymlinkBehavior::Follow);
1056 assert!(result.is_err());
1057 if let Err(e) = result {
1058 assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
1060 }
1061 }
1062
1063 #[test]
1064 fn test_error_chain() {
1065 let result = DirFd::open(
1066 "/nonexistent/deeply/nested/path".as_ref(),
1067 SymlinkBehavior::Follow,
1068 );
1069 assert!(result.is_err());
1070
1071 if let Err(e) = result {
1072 let io_err: io::Error = e;
1074 assert!(
1075 io_err.kind() == io::ErrorKind::NotFound
1076 || io_err.kind() == io::ErrorKind::PermissionDenied
1077 );
1078 }
1079 }
1080
1081 #[test]
1082 fn test_mkdir_at_creates_directory() {
1083 let temp_dir = TempDir::new().unwrap();
1084 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1085
1086 dir_fd.mkdir_at(OsStr::new("new_subdir"), 0o755).unwrap();
1087
1088 assert!(temp_dir.path().join("new_subdir").is_dir());
1089 }
1090
1091 #[test]
1092 fn test_mkdir_at_fails_if_exists() {
1093 let temp_dir = TempDir::new().unwrap();
1094 let subdir = temp_dir.path().join("existing");
1095 fs::create_dir(&subdir).unwrap();
1096
1097 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1098 let result = dir_fd.mkdir_at(OsStr::new("existing"), 0o755);
1099
1100 assert!(result.is_err());
1101 }
1102
1103 #[test]
1104 fn test_open_file_at_creates_file() {
1105 let temp_dir = TempDir::new().unwrap();
1106 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1107
1108 let mut file = dir_fd.open_file_at(OsStr::new("new_file.txt")).unwrap();
1109 use std::io::Write;
1110 file.write_all(b"test content").unwrap();
1111
1112 let content = fs::read_to_string(temp_dir.path().join("new_file.txt")).unwrap();
1113 assert_eq!(content, "test content");
1114 }
1115
1116 #[test]
1117 fn test_open_file_at_truncates_existing() {
1118 let temp_dir = TempDir::new().unwrap();
1119 let file_path = temp_dir.path().join("existing.txt");
1120 fs::write(&file_path, "old content that is longer").unwrap();
1121
1122 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1123 let mut file = dir_fd.open_file_at(OsStr::new("existing.txt")).unwrap();
1124 use std::io::Write;
1125 file.write_all(b"new").unwrap();
1126 drop(file);
1127
1128 let content = fs::read_to_string(&file_path).unwrap();
1129 assert_eq!(content, "new");
1130 }
1131
1132 #[test]
1133 fn test_create_dir_all_safe_creates_nested_dirs() {
1134 let temp_dir = TempDir::new().unwrap();
1135 let nested_path = temp_dir.path().join("a/b/c");
1136
1137 let dir_fd = create_dir_all_safe(&nested_path, 0o755).unwrap();
1138 assert!(dir_fd.as_raw_fd() >= 0);
1139 assert!(nested_path.is_dir());
1140 }
1141
1142 #[test]
1143 fn test_create_dir_all_safe_existing_path() {
1144 let temp_dir = TempDir::new().unwrap();
1145 let existing_path = temp_dir.path().join("existing");
1146 fs::create_dir(&existing_path).unwrap();
1147
1148 let dir_fd = create_dir_all_safe(&existing_path, 0o755).unwrap();
1149 assert!(dir_fd.as_raw_fd() >= 0);
1150 }
1151
1152 #[test]
1153 fn test_create_dir_all_safe_replaces_symlink() {
1154 let temp_dir = TempDir::new().unwrap();
1155 let target_dir = temp_dir.path().join("target");
1156 fs::create_dir(&target_dir).unwrap();
1157
1158 let symlink_path = temp_dir.path().join("link_to_replace");
1160 symlink(&target_dir, &symlink_path).unwrap();
1161 assert!(symlink_path.is_symlink());
1162
1163 let dir_fd = create_dir_all_safe(&symlink_path, 0o755).unwrap();
1165 assert!(dir_fd.as_raw_fd() >= 0);
1166
1167 assert!(symlink_path.is_dir());
1169 assert!(!symlink_path.is_symlink());
1170 }
1171
1172 #[test]
1173 fn test_create_dir_all_safe_fails_on_file() {
1174 let temp_dir = TempDir::new().unwrap();
1175 let file_path = temp_dir.path().join("file");
1176 fs::write(&file_path, "content").unwrap();
1177
1178 let result = create_dir_all_safe(&file_path, 0o755);
1179 assert!(result.is_err());
1180 }
1181
1182 #[test]
1183 fn test_create_dir_all_safe_nested_symlink_in_path() {
1184 let temp_dir = TempDir::new().unwrap();
1185
1186 let parent = temp_dir.path().join("parent");
1189 let target = temp_dir.path().join("target");
1190 fs::create_dir(&parent).unwrap();
1191 fs::create_dir(&target).unwrap();
1192
1193 let symlink_in_path = parent.join("link");
1194 symlink(&target, &symlink_in_path).unwrap();
1195
1196 let nested_path = symlink_in_path.join("subdir");
1198 let dir_fd = create_dir_all_safe(&nested_path, 0o755).unwrap();
1199 assert!(dir_fd.as_raw_fd() >= 0);
1200
1201 assert!(!symlink_in_path.is_symlink());
1203 assert!(symlink_in_path.is_dir());
1204 assert!(nested_path.is_dir());
1205
1206 assert!(!target.join("subdir").exists());
1208 }
1209
1210 #[test]
1211 fn test_open_subdir_nofollow_fails_on_symlink() {
1212 let temp_dir = TempDir::new().unwrap();
1213 let target = temp_dir.path().join("target");
1214 fs::create_dir(&target).unwrap();
1215
1216 let link = temp_dir.path().join("link");
1217 symlink(&target, &link).unwrap();
1218
1219 let dir_fd = DirFd::open(temp_dir.path(), SymlinkBehavior::Follow).unwrap();
1220
1221 let result_follow = dir_fd.open_subdir(OsStr::new("link"), SymlinkBehavior::Follow);
1223 assert!(result_follow.is_ok());
1224
1225 let result_nofollow = dir_fd.open_subdir(OsStr::new("link"), SymlinkBehavior::NoFollow);
1227 assert!(result_nofollow.is_err());
1228 }
1229
1230 #[test]
1231 fn test_open_nofollow_fails_on_symlink() {
1232 let temp_dir = TempDir::new().unwrap();
1233 let target = temp_dir.path().join("target");
1234 fs::create_dir(&target).unwrap();
1235
1236 let link = temp_dir.path().join("link");
1237 symlink(&target, &link).unwrap();
1238
1239 let result_follow = DirFd::open(&link, SymlinkBehavior::Follow);
1241 assert!(result_follow.is_ok());
1242
1243 let result_nofollow = DirFd::open(&link, SymlinkBehavior::NoFollow);
1245 assert!(result_nofollow.is_err());
1246 }
1247}