artifactize_tools/platform/files/
unix.rs1use std::{
4 ffi::{CString, OsStr, OsString},
5 fs::{self, File, OpenOptions},
6 io,
7 os::unix::{ffi::OsStrExt, fs::OpenOptionsExt},
8 path::{Path, PathBuf},
9};
10
11use rustix::{
12 fs::{AtFlags, Dir, FileType, Mode, OFlags, openat, statat},
13 io::Errno,
14};
15
16use super::FileKind;
17
18#[cfg(target_os = "macos")]
19mod macos;
20#[cfg(target_os = "macos")]
21pub use macos::{case_sensitive, exact_name};
22
23pub fn canonicalize(path: &Path) -> io::Result<PathBuf> {
25 fs::canonicalize(path)
26}
27
28pub fn open_no_follow(options: &mut OpenOptions, path: &Path) -> io::Result<File> {
30 options
31 .custom_flags((OFlags::NOFOLLOW | OFlags::NONBLOCK).bits() as i32)
32 .open(path)
33}
34
35pub fn open_nonblocking(path: &Path) -> io::Result<File> {
37 OpenOptions::new()
38 .read(true)
39 .custom_flags(OFlags::NONBLOCK.bits() as i32)
40 .open(path)
41}
42
43pub fn open_directory(path: &Path) -> io::Result<File> {
45 File::open(path)
46}
47
48pub struct EntryName(CString);
50
51impl EntryName {
52 pub fn new(name: &OsStr) -> Option<Self> {
53 let bytes = name.as_bytes();
54 (!bytes.is_empty() && !matches!(bytes, b"." | b"..") && !bytes.contains(&b'/'))
55 .then(|| CString::new(bytes).ok().map(Self))
56 .flatten()
57 }
58}
59
60pub fn open_entry(directory: &File, name: &EntryName) -> io::Result<File> {
62 Ok(openat(
64 directory,
65 &name.0,
66 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
67 Mode::empty(),
68 )?
69 .into())
70}
71
72pub fn is_link_refusal(error: &io::Error) -> bool {
75 let link = if cfg!(any(target_os = "freebsd", target_os = "dragonfly")) {
76 Errno::MLINK
77 } else {
78 Errno::LOOP
79 };
80 error.raw_os_error() == Some(link.raw_os_error())
81}
82
83pub fn read_dir(directory: &File) -> io::Result<impl Iterator<Item = io::Result<DirEntry<'_>>>> {
86 let fd = openat(
88 directory,
89 c".",
90 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
91 Mode::empty(),
92 )?;
93 Ok(ReadDir {
94 stream: Dir::new(fd)?,
95 directory,
96 state: Scan::Reading,
97 })
98}
99
100#[derive(Clone, Copy, PartialEq, Eq)]
102enum Scan {
103 Reading,
104 Done,
105}
106
107struct ReadDir<'a> {
108 stream: Dir,
109 directory: &'a File,
110 state: Scan,
111}
112
113impl<'a> Iterator for ReadDir<'a> {
114 type Item = io::Result<DirEntry<'a>>;
115
116 fn next(&mut self) -> Option<Self::Item> {
117 while self.state == Scan::Reading {
118 let entry = match self.stream.next() {
119 Some(Ok(entry)) => entry,
120 Some(Err(error)) => {
121 self.state = Scan::Done;
122 return Some(Err(error.into()));
123 }
124 None => {
125 self.state = Scan::Done;
126 return None;
127 }
128 };
129 let name = entry.file_name().to_bytes();
130 if name == b"." || name == b".." {
131 continue;
132 }
133 return Some(Ok(DirEntry {
134 directory: self.directory,
135 name: OsStr::from_bytes(name).to_owned(),
136 }));
137 }
138 None
139 }
140}
141
142pub struct DirEntry<'a> {
143 directory: &'a File,
144 name: OsString,
145}
146
147impl DirEntry<'_> {
148 pub fn file_name(&self) -> OsString {
149 self.name.clone()
150 }
151
152 pub fn file_type(&self) -> io::Result<FileKind> {
153 entry_kind(self.directory, &self.name)
154 }
155}
156
157pub fn entry_kind(directory: &File, name: &OsStr) -> io::Result<FileKind> {
159 let name = EntryName::new(name).ok_or(io::ErrorKind::InvalidInput)?;
160 let stat = statat(directory, &name.0, AtFlags::SYMLINK_NOFOLLOW)?;
162 Ok(match FileType::from_raw_mode(stat.st_mode) {
163 FileType::RegularFile => FileKind::File,
164 FileType::Directory => FileKind::Directory,
165 FileType::Symlink => FileKind::Symlink,
166 _ => FileKind::Other,
167 })
168}
169
170#[cfg(not(target_os = "macos"))]
172pub fn exact_name(_file: &File, _name: &OsStr) -> io::Result<bool> {
173 Ok(true)
174}
175
176#[cfg(not(target_os = "macos"))]
178pub fn case_sensitive(_directory: &File) -> io::Result<bool> {
179 Ok(true)
180}