1#[cfg(unix)]
11use libc::{
12 S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP, S_IROTH,
13 S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR,
14 mkfifo, mode_t,
15};
16use std::collections::HashSet;
17use std::collections::VecDeque;
18use std::env;
19#[cfg(unix)]
20use std::ffi::CString;
21use std::ffi::{OsStr, OsString};
22use std::fs;
23use std::fs::read_dir;
24use std::hash::Hash;
25use std::io::Stdin;
26use std::io::{Error, ErrorKind, Result as IOResult};
27#[cfg(unix)]
28use std::os::fd::AsFd;
29#[cfg(unix)]
30use std::os::unix::fs::MetadataExt;
31use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf};
32#[cfg(target_os = "windows")]
33use winapi_util::AsHandleRef;
34
35#[cfg(unix)]
39#[macro_export]
40macro_rules! has {
41 ($mode:expr, $perm:expr) => {
42 $mode & $perm != 0
43 };
44}
45
46pub struct FileInformation(
48 #[cfg(unix)] nix::sys::stat::FileStat,
49 #[cfg(windows)] winapi_util::file::Information,
50);
51
52impl FileInformation {
53 #[cfg(unix)]
55 pub fn from_file(file: &impl AsFd) -> IOResult<Self> {
56 let stat = nix::sys::stat::fstat(file)?;
57 Ok(Self(stat))
58 }
59
60 #[cfg(target_os = "windows")]
62 pub fn from_file(file: &impl AsHandleRef) -> IOResult<Self> {
63 let info = winapi_util::file::information(file.as_handle_ref())?;
64 Ok(Self(info))
65 }
66
67 pub fn from_path(path: impl AsRef<Path>, dereference: bool) -> IOResult<Self> {
72 #[cfg(unix)]
73 {
74 let stat = if dereference {
75 nix::sys::stat::stat(path.as_ref())
76 } else {
77 nix::sys::stat::lstat(path.as_ref())
78 };
79 Ok(Self(stat?))
80 }
81 #[cfg(target_os = "windows")]
82 {
83 use std::fs::OpenOptions;
84 use std::os::windows::prelude::*;
85 let mut open_options = OpenOptions::new();
86 let mut custom_flags = 0;
87 if !dereference {
88 custom_flags |=
89 windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
90 }
91 custom_flags |= windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
92 open_options.custom_flags(custom_flags);
93 let file = open_options.read(true).open(path.as_ref())?;
94 Self::from_file(&file)
95 }
96 }
97
98 pub fn file_size(&self) -> u64 {
99 #[cfg(unix)]
100 {
101 assert!(self.0.st_size >= 0, "File size is negative");
102 self.0.st_size.try_into().unwrap()
103 }
104 #[cfg(target_os = "windows")]
105 {
106 self.0.file_size()
107 }
108 }
109
110 #[cfg(windows)]
111 pub fn file_index(&self) -> u64 {
112 self.0.file_index()
113 }
114
115 pub fn number_of_links(&self) -> u64 {
116 #[cfg(all(
117 unix,
118 not(target_vendor = "apple"),
119 not(target_os = "aix"),
120 not(target_os = "android"),
121 not(target_os = "freebsd"),
122 not(target_os = "netbsd"),
123 not(target_os = "openbsd"),
124 not(target_os = "illumos"),
125 not(target_os = "solaris"),
126 not(target_arch = "aarch64"),
127 not(target_arch = "riscv64"),
128 not(target_arch = "loongarch64"),
129 not(target_arch = "sparc64"),
130 target_pointer_width = "64"
131 ))]
132 return self.0.st_nlink;
133 #[cfg(all(
134 unix,
135 any(
136 target_vendor = "apple",
137 target_os = "android",
138 target_os = "freebsd",
139 target_os = "netbsd",
140 target_os = "openbsd",
141 target_os = "illumos",
142 target_os = "solaris",
143 target_arch = "aarch64",
144 target_arch = "riscv64",
145 target_arch = "loongarch64",
146 target_arch = "sparc64",
147 not(target_pointer_width = "64")
148 )
149 ))]
150 return self.0.st_nlink.into();
151 #[cfg(target_os = "aix")]
152 return self.0.st_nlink.try_into().unwrap();
153 #[cfg(windows)]
154 return self.0.number_of_links();
155 }
156
157 #[cfg(unix)]
158 pub fn inode(&self) -> u64 {
159 #[cfg(all(
160 not(any(target_os = "freebsd", target_os = "netbsd")),
161 target_pointer_width = "64"
162 ))]
163 return self.0.st_ino;
164 #[cfg(any(
165 target_os = "freebsd",
166 target_os = "netbsd",
167 not(target_pointer_width = "64")
168 ))]
169 return self.0.st_ino.into();
170 }
171}
172
173#[cfg(unix)]
174impl PartialEq for FileInformation {
175 fn eq(&self, other: &Self) -> bool {
176 self.0.st_dev == other.0.st_dev && self.0.st_ino == other.0.st_ino
177 }
178}
179
180#[cfg(target_os = "windows")]
181impl PartialEq for FileInformation {
182 fn eq(&self, other: &Self) -> bool {
183 self.0.volume_serial_number() == other.0.volume_serial_number()
184 && self.0.file_index() == other.0.file_index()
185 }
186}
187
188impl Eq for FileInformation {}
189
190impl Hash for FileInformation {
191 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
192 #[cfg(unix)]
193 {
194 self.0.st_dev.hash(state);
195 self.0.st_ino.hash(state);
196 }
197 #[cfg(target_os = "windows")]
198 {
199 self.0.volume_serial_number().hash(state);
200 self.0.file_index().hash(state);
201 }
202 }
203}
204
205#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub enum MissingHandling {
208 Normal,
210
211 Existing,
213
214 Missing,
216}
217
218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
220pub enum ResolveMode {
221 None,
223
224 Physical,
226
227 Logical,
229}
230
231pub fn normalize_path(path: &Path) -> PathBuf {
238 let mut components = path.components().peekable();
239 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
240 components.next();
241 PathBuf::from(c.as_os_str())
242 } else {
243 PathBuf::new()
244 };
245
246 for component in components {
247 match component {
248 Component::Prefix(..) => unreachable!(),
249 Component::RootDir => {
250 ret.push(component.as_os_str());
251 }
252 Component::CurDir => {}
253 Component::ParentDir => {
254 ret.pop();
255 }
256 Component::Normal(c) => {
257 ret.push(c);
258 }
259 }
260 }
261 ret
262}
263
264fn resolve_symlink<P: AsRef<Path>>(path: P) -> IOResult<Option<PathBuf>> {
265 let result = if fs::symlink_metadata(&path)?.file_type().is_symlink() {
266 Some(fs::read_link(&path)?)
267 } else {
268 None
269 };
270 Ok(result)
271}
272
273enum OwningComponent {
274 Prefix(OsString),
275 RootDir,
276 CurDir,
277 ParentDir,
278 Normal(OsString),
279}
280
281impl OwningComponent {
282 fn as_os_str(&self) -> &OsStr {
283 match self {
284 Self::Prefix(s) => s.as_os_str(),
285 Self::RootDir => Component::RootDir.as_os_str(),
286 Self::CurDir => Component::CurDir.as_os_str(),
287 Self::ParentDir => Component::ParentDir.as_os_str(),
288 Self::Normal(s) => s.as_os_str(),
289 }
290 }
291}
292
293impl<'a> From<Component<'a>> for OwningComponent {
294 fn from(comp: Component<'a>) -> Self {
295 match comp {
296 Component::Prefix(_) => Self::Prefix(comp.as_os_str().to_os_string()),
297 Component::RootDir => Self::RootDir,
298 Component::CurDir => Self::CurDir,
299 Component::ParentDir => Self::ParentDir,
300 Component::Normal(s) => Self::Normal(s.to_os_string()),
301 }
302 }
303}
304
305#[allow(clippy::cognitive_complexity)]
332pub fn canonicalize<P: AsRef<Path>>(
333 original: P,
334 miss_mode: MissingHandling,
335 res_mode: ResolveMode,
336) -> IOResult<PathBuf> {
337 const SYMLINKS_TO_LOOK_FOR_LOOPS: i32 = 20;
338 let original = original.as_ref();
339 let has_to_be_directory =
340 (miss_mode == MissingHandling::Normal || miss_mode == MissingHandling::Existing) && {
341 let path_str = original.to_string_lossy();
342 path_str.ends_with(MAIN_SEPARATOR) || path_str.ends_with('/')
343 };
344 let original = if original.is_absolute() {
345 original.to_path_buf()
346 } else {
347 let current_dir = env::current_dir()?;
348 dunce::canonicalize(current_dir)?.join(original)
349 };
350 let path = if res_mode == ResolveMode::Logical {
351 normalize_path(&original)
352 } else {
353 original
354 };
355 let mut parts: VecDeque<OwningComponent> = path.components().map(|part| part.into()).collect();
356 let mut result = PathBuf::new();
357 let mut followed_symlinks = 0;
358 let mut visited_files = HashSet::new();
359 while let Some(part) = parts.pop_front() {
360 match part {
361 OwningComponent::Prefix(s) => {
362 result.push(s);
363 continue;
364 }
365 OwningComponent::RootDir | OwningComponent::Normal(..) => {
366 result.push(part.as_os_str());
367 }
368 OwningComponent::CurDir => {}
369 OwningComponent::ParentDir => {
370 result.pop();
371 }
372 }
373 if res_mode == ResolveMode::None {
374 continue;
375 }
376 match resolve_symlink(&result) {
377 Ok(Some(link_path)) => {
378 for link_part in link_path.components().rev() {
379 parts.push_front(link_part.into());
380 }
381 if followed_symlinks < SYMLINKS_TO_LOOK_FOR_LOOPS {
382 followed_symlinks += 1;
383 } else {
384 let file_info =
385 FileInformation::from_path(result.parent().unwrap(), false).unwrap();
386 let mut path_to_follow = PathBuf::new();
387 for part in &parts {
388 path_to_follow.push(part.as_os_str());
389 }
390 if !visited_files.insert((file_info, path_to_follow)) {
391 return Err(Error::new(
392 ErrorKind::InvalidInput,
393 "Too many levels of symbolic links",
394 )); }
396 }
397 result.pop();
398 }
399 Err(e) => {
400 if miss_mode == MissingHandling::Existing
401 || (miss_mode == MissingHandling::Normal && !parts.is_empty())
402 {
403 return Err(e);
404 }
405 }
406 _ => {}
407 }
408 }
409 match miss_mode {
411 MissingHandling::Existing => {
412 if has_to_be_directory {
413 read_dir(&result)?;
414 }
415 }
416 MissingHandling::Normal => {
417 if result.exists() {
418 if has_to_be_directory {
419 read_dir(&result)?;
420 }
421 } else if let Some(parent) = result.parent() {
422 read_dir(parent)?;
423 }
424 }
425 MissingHandling::Missing => {}
426 }
427 Ok(result)
428}
429
430#[cfg(not(unix))]
431pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String {
433 let write = if metadata.permissions().readonly() {
434 '-'
435 } else {
436 'w'
437 };
438
439 if display_file_type {
440 let file_type = if metadata.is_symlink() {
441 'l'
442 } else if metadata.is_dir() {
443 'd'
444 } else {
445 '-'
446 };
447
448 format!("{file_type}r{write}xr{write}xr{write}x")
449 } else {
450 format!("r{write}xr{write}xr{write}x")
451 }
452}
453
454#[cfg(unix)]
455pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String {
457 let mode: mode_t = metadata.mode() as mode_t;
458 display_permissions_unix(mode, display_file_type)
459}
460
461#[cfg(unix)]
476fn get_file_display(mode: mode_t) -> char {
477 match mode & S_IFMT {
478 S_IFDIR => 'd',
479 S_IFCHR => 'c',
480 S_IFBLK => 'b',
481 S_IFREG => '-',
482 S_IFIFO => 'p',
483 S_IFLNK => 'l',
484 S_IFSOCK => 's',
485 _ => '?',
487 }
488}
489
490#[allow(clippy::if_not_else)]
492#[allow(clippy::cognitive_complexity)]
493#[cfg(unix)]
494pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String {
496 let mut result;
497 if display_file_type {
498 result = String::with_capacity(10);
499 result.push(get_file_display(mode));
500 } else {
501 result = String::with_capacity(9);
502 }
503
504 result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' });
505 result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' });
506 result.push(if has!(mode, S_ISUID as mode_t) {
507 if has!(mode, S_IXUSR) { 's' } else { 'S' }
508 } else if has!(mode, S_IXUSR) {
509 'x'
510 } else {
511 '-'
512 });
513
514 result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' });
515 result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' });
516 result.push(if has!(mode, S_ISGID as mode_t) {
517 if has!(mode, S_IXGRP) { 's' } else { 'S' }
518 } else if has!(mode, S_IXGRP) {
519 'x'
520 } else {
521 '-'
522 });
523
524 result.push(if has!(mode, S_IROTH) { 'r' } else { '-' });
525 result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' });
526 result.push(if has!(mode, S_ISVTX as mode_t) {
527 if has!(mode, S_IXOTH) { 't' } else { 'T' }
528 } else if has!(mode, S_IXOTH) {
529 'x'
530 } else {
531 '-'
532 });
533
534 result
535}
536
537pub fn dir_strip_dot_for_creation(path: &Path) -> PathBuf {
542 let path_str = path.to_string_lossy();
543
544 if path_str.ends_with("/.") || path_str.ends_with("/./") {
545 Path::new(&path).components().collect()
547 } else {
548 path.to_path_buf()
549 }
550}
551
552pub fn paths_refer_to_same_file<P: AsRef<Path>>(p1: P, p2: P, dereference: bool) -> bool {
555 infos_refer_to_same_file(
556 FileInformation::from_path(p1, dereference),
557 FileInformation::from_path(p2, dereference),
558 )
559}
560
561pub fn infos_refer_to_same_file(
564 info1: IOResult<FileInformation>,
565 info2: IOResult<FileInformation>,
566) -> bool {
567 if let Ok(info1) = info1 {
568 if let Ok(info2) = info2 {
569 return info1 == info2;
570 }
571 }
572 false
573}
574
575pub fn make_path_relative_to<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, to: P2) -> PathBuf {
577 let path = path.as_ref();
578 let to = to.as_ref();
579 let common_prefix_size = path
580 .components()
581 .zip(to.components())
582 .take_while(|(first, second)| first == second)
583 .count();
584 let path_suffix = path
585 .components()
586 .skip(common_prefix_size)
587 .map(|x| x.as_os_str());
588 let mut components: Vec<_> = to
589 .components()
590 .skip(common_prefix_size)
591 .map(|_| Component::ParentDir.as_os_str())
592 .chain(path_suffix)
593 .collect();
594 if components.is_empty() {
595 components.push(Component::CurDir.as_os_str());
596 }
597 components.iter().collect()
598}
599
600pub fn is_symlink_loop(path: &Path) -> bool {
612 let mut visited_symlinks = HashSet::new();
613 let mut current_path = path.to_path_buf();
614
615 while let (Ok(metadata), Ok(link)) = (
616 current_path.symlink_metadata(),
617 fs::read_link(¤t_path),
618 ) {
619 if !metadata.file_type().is_symlink() {
620 return false;
621 }
622 if !visited_symlinks.insert(current_path.clone()) {
623 return true;
624 }
625 current_path = link;
626 }
627
628 false
629}
630
631#[cfg(not(unix))]
632pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool {
634 false
635}
636
637#[cfg(unix)]
648pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool {
649 let (Ok(source_metadata), Ok(target_metadata)) =
650 (fs::symlink_metadata(source), fs::symlink_metadata(target))
651 else {
652 return false;
653 };
654
655 source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
656}
657
658#[cfg(not(unix))]
659pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &Path) -> bool {
660 false
661}
662
663#[cfg(unix)]
674pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool {
675 let (Ok(source_metadata), Ok(target_metadata)) =
676 (fs::metadata(source), fs::symlink_metadata(target))
677 else {
678 return false;
679 };
680
681 source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
682}
683
684#[cfg(unix)]
694pub fn path_ends_with_terminator(path: &Path) -> bool {
695 use std::os::unix::prelude::OsStrExt;
696 path.as_os_str()
697 .as_bytes()
698 .last()
699 .is_some_and(|&byte| byte == b'/')
700}
701
702#[cfg(windows)]
703pub fn path_ends_with_terminator(path: &Path) -> bool {
704 use std::os::windows::prelude::OsStrExt;
705 path.as_os_str()
706 .encode_wide()
707 .last()
708 .is_some_and(|wide| wide == b'/'.into() || wide == b'\\'.into())
709}
710
711pub fn is_stdin_directory(stdin: &Stdin) -> bool {
721 #[cfg(unix)]
722 {
723 use nix::sys::stat::fstat;
724 let mode = fstat(stdin.as_fd()).unwrap().st_mode as mode_t;
725 mode & S_IFMT == S_IFDIR
728 }
729
730 #[cfg(windows)]
731 {
732 use std::os::windows::io::AsRawHandle;
733 let handle = stdin.as_raw_handle();
734 if let Ok(metadata) = fs::metadata(format!("{}", handle as usize)) {
735 return metadata.is_dir();
736 }
737 false
738 }
739}
740
741pub mod sane_blksize {
742
743 #[cfg(not(target_os = "windows"))]
744 use std::os::unix::fs::MetadataExt;
745 use std::{fs::metadata, path::Path};
746
747 pub const DEFAULT: u64 = 512;
748 pub const MAX: u64 = (u32::MAX / 8 + 1) as u64;
749
750 pub fn sane_blksize(st_blksize: u64) -> u64 {
755 match st_blksize {
756 0 => DEFAULT,
757 1..=MAX => st_blksize,
758 _ => DEFAULT,
759 }
760 }
761
762 pub fn sane_blksize_from_metadata(_metadata: &std::fs::Metadata) -> u64 {
767 #[cfg(not(target_os = "windows"))]
768 {
769 sane_blksize(_metadata.blksize())
770 }
771
772 #[cfg(target_os = "windows")]
773 {
774 DEFAULT
775 }
776 }
777
778 pub fn sane_blksize_from_path(path: &Path) -> u64 {
783 match metadata(path) {
784 Ok(metadata) => sane_blksize_from_metadata(&metadata),
785 Err(_) => DEFAULT,
786 }
787 }
788}
789
790pub fn get_filename(file: &Path) -> Option<&str> {
806 file.file_name().and_then(|filename| filename.to_str())
807}
808
809#[cfg(unix)]
830pub fn make_fifo(path: &Path) -> std::io::Result<()> {
831 let name = CString::new(path.to_str().unwrap()).unwrap();
832 let err = unsafe { mkfifo(name.as_ptr(), 0o666) };
833 if err == -1 {
834 Err(std::io::Error::from_raw_os_error(err))
835 } else {
836 Ok(())
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
844 #[cfg(unix)]
845 use std::io::Write;
846 #[cfg(unix)]
847 use std::os::unix;
848 #[cfg(unix)]
849 use std::os::unix::fs::FileTypeExt;
850 #[cfg(unix)]
851 use tempfile::{NamedTempFile, tempdir};
852
853 struct NormalizePathTestCase<'a> {
854 path: &'a str,
855 test: &'a str,
856 }
857
858 const NORMALIZE_PATH_TESTS: [NormalizePathTestCase; 8] = [
859 NormalizePathTestCase {
860 path: "./foo/bar.txt",
861 test: "foo/bar.txt",
862 },
863 NormalizePathTestCase {
864 path: "bar/../foo/bar.txt",
865 test: "foo/bar.txt",
866 },
867 NormalizePathTestCase {
868 path: "foo///bar.txt",
869 test: "foo/bar.txt",
870 },
871 NormalizePathTestCase {
872 path: "foo///bar",
873 test: "foo/bar",
874 },
875 NormalizePathTestCase {
876 path: "foo//./bar",
877 test: "foo/bar",
878 },
879 NormalizePathTestCase {
880 path: "/foo//./bar",
881 test: "/foo/bar",
882 },
883 NormalizePathTestCase {
884 path: r"C:/you/later/",
885 test: "C:/you/later",
886 },
887 NormalizePathTestCase {
888 path: "\\networkShare/a//foo//./bar",
889 test: "\\networkShare/a/foo/bar",
890 },
891 ];
892
893 #[test]
894 fn test_normalize_path() {
895 for test in &NORMALIZE_PATH_TESTS {
896 let path = Path::new(test.path);
897 let normalized = normalize_path(path);
898 assert_eq!(
899 test.test
900 .replace('/', std::path::MAIN_SEPARATOR.to_string().as_str()),
901 normalized.to_str().expect("Path is not valid utf-8!")
902 );
903 }
904 }
905
906 #[cfg(unix)]
907 #[test]
908 fn test_display_permissions() {
909 assert_eq!(
911 "drwxr-xr-x",
912 display_permissions_unix(S_IFDIR | 0o755, true)
913 );
914 assert_eq!(
915 "rwxr-xr-x",
916 display_permissions_unix(S_IFDIR | 0o755, false)
917 );
918 assert_eq!(
919 "-rw-r--r--",
920 display_permissions_unix(S_IFREG | 0o644, true)
921 );
922 assert_eq!(
923 "srw-r-----",
924 display_permissions_unix(S_IFSOCK | 0o640, true)
925 );
926 assert_eq!(
927 "lrw-r-xr-x",
928 display_permissions_unix(S_IFLNK | 0o655, true)
929 );
930 assert_eq!("?rw-r-xr-x", display_permissions_unix(0o655, true));
931
932 assert_eq!(
933 "brwSr-xr-x",
934 display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o655, true)
935 );
936 assert_eq!(
937 "brwsr-xr-x",
938 display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o755, true)
939 );
940
941 assert_eq!(
942 "prw---sr--",
943 display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o614, true)
944 );
945 assert_eq!(
946 "prw---Sr--",
947 display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o604, true)
948 );
949
950 assert_eq!(
951 "c---r-xr-t",
952 display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o055, true)
953 );
954 assert_eq!(
955 "c---r-xr-T",
956 display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o054, true)
957 );
958 }
959
960 #[cfg(unix)]
961 #[test]
962 fn test_is_symlink_loop_no_loop() {
963 let temp_dir = tempdir().unwrap();
964 let file_path = temp_dir.path().join("file.txt");
965 let symlink_path = temp_dir.path().join("symlink");
966
967 fs::write(&file_path, "test content").unwrap();
968 unix::fs::symlink(&file_path, &symlink_path).unwrap();
969
970 assert!(!is_symlink_loop(&symlink_path));
971 }
972
973 #[cfg(unix)]
974 #[test]
975 fn test_is_symlink_loop_direct_loop() {
976 let temp_dir = tempdir().unwrap();
977 let symlink_path = temp_dir.path().join("loop");
978
979 unix::fs::symlink(&symlink_path, &symlink_path).unwrap();
980
981 assert!(is_symlink_loop(&symlink_path));
982 }
983
984 #[cfg(unix)]
985 #[test]
986 fn test_is_symlink_loop_indirect_loop() {
987 let temp_dir = tempdir().unwrap();
988 let symlink1_path = temp_dir.path().join("symlink1");
989 let symlink2_path = temp_dir.path().join("symlink2");
990
991 unix::fs::symlink(&symlink1_path, &symlink2_path).unwrap();
992 unix::fs::symlink(&symlink2_path, &symlink1_path).unwrap();
993
994 assert!(is_symlink_loop(&symlink1_path));
995 }
996
997 #[cfg(unix)]
998 #[test]
999 fn test_are_hardlinks_to_same_file_same_file() {
1000 let mut temp_file = NamedTempFile::new().unwrap();
1001 writeln!(temp_file, "Test content").unwrap();
1002
1003 let path1 = temp_file.path();
1004 let path2 = temp_file.path();
1005
1006 assert!(are_hardlinks_to_same_file(path1, path2));
1007 }
1008
1009 #[cfg(unix)]
1010 #[test]
1011 fn test_are_hardlinks_to_same_file_different_files() {
1012 let mut temp_file1 = NamedTempFile::new().unwrap();
1013 writeln!(temp_file1, "Test content 1").unwrap();
1014
1015 let mut temp_file2 = NamedTempFile::new().unwrap();
1016 writeln!(temp_file2, "Test content 2").unwrap();
1017
1018 let path1 = temp_file1.path();
1019 let path2 = temp_file2.path();
1020
1021 assert!(!are_hardlinks_to_same_file(path1, path2));
1022 }
1023
1024 #[cfg(unix)]
1025 #[test]
1026 fn test_are_hardlinks_to_same_file_hard_link() {
1027 let mut temp_file = NamedTempFile::new().unwrap();
1028 writeln!(temp_file, "Test content").unwrap();
1029 let path1 = temp_file.path();
1030
1031 let path2 = temp_file.path().with_extension("hardlink");
1032 fs::hard_link(path1, &path2).unwrap();
1033
1034 assert!(are_hardlinks_to_same_file(path1, &path2));
1035 }
1036
1037 #[cfg(unix)]
1038 #[test]
1039 fn test_get_file_display() {
1040 assert_eq!(get_file_display(S_IFDIR | 0o755), 'd');
1041 assert_eq!(get_file_display(S_IFCHR | 0o644), 'c');
1042 assert_eq!(get_file_display(S_IFBLK | 0o600), 'b');
1043 assert_eq!(get_file_display(S_IFREG | 0o777), '-');
1044 assert_eq!(get_file_display(S_IFIFO | 0o666), 'p');
1045 assert_eq!(get_file_display(S_IFLNK | 0o777), 'l');
1046 assert_eq!(get_file_display(S_IFSOCK | 0o600), 's');
1047 assert_eq!(get_file_display(0o777), '?');
1048 }
1049
1050 #[test]
1051 fn test_path_ends_with_terminator() {
1052 assert!(path_ends_with_terminator(Path::new("/some/path/")));
1054
1055 #[cfg(windows)]
1057 assert!(path_ends_with_terminator(Path::new("C:\\some\\path\\")));
1058
1059 assert!(!path_ends_with_terminator(Path::new("/some/path")));
1061 assert!(!path_ends_with_terminator(Path::new("C:\\some\\path")));
1062
1063 assert!(!path_ends_with_terminator(Path::new("")));
1065
1066 assert!(path_ends_with_terminator(Path::new("/")));
1068 #[cfg(windows)]
1069 assert!(path_ends_with_terminator(Path::new("C:\\")));
1070 }
1071
1072 #[test]
1073 fn test_sane_blksize() {
1074 assert_eq!(512, sane_blksize::sane_blksize(0));
1075 assert_eq!(512, sane_blksize::sane_blksize(512));
1076 assert_eq!(4096, sane_blksize::sane_blksize(4096));
1077 assert_eq!(0x2000_0000, sane_blksize::sane_blksize(0x2000_0000));
1078 assert_eq!(512, sane_blksize::sane_blksize(0x2000_0001));
1079 }
1080 #[test]
1081 fn test_get_file_name() {
1082 let file_path = PathBuf::from("~/foo.txt");
1083 assert!(matches!(get_filename(&file_path), Some("foo.txt")));
1084 }
1085
1086 #[cfg(unix)]
1087 #[test]
1088 fn test_make_fifo() {
1089 let tempdir = tempdir().unwrap();
1091 let path = tempdir.path().join("f");
1092 assert!(make_fifo(&path).is_ok());
1093
1094 assert!(std::fs::metadata(&path).unwrap().file_type().is_fifo());
1096
1097 let path2 = path.clone();
1103 std::thread::spawn(move || assert!(std::fs::write(&path2, b"foo").is_ok()));
1104 assert_eq!(std::fs::read(&path).unwrap(), b"foo");
1105 }
1106}