Skip to main content

artifactize_tools/platform/files/
unix.rs

1//! Unix pinned opens and descriptor-based directory listings.
2
3use 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
23/// The absolute path of an existing file with every link resolved.
24pub fn canonicalize(path: &Path) -> io::Result<PathBuf> {
25    fs::canonicalize(path)
26}
27
28/// Open without following a link in the last component or blocking on a FIFO.
29pub 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
35/// Open for reading without blocking on a FIFO, so its type can be checked first.
36pub 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
43/// Open a directory by path, such as the root a scoped walk starts from.
44pub fn open_directory(path: &Path) -> io::Result<File> {
45    File::open(path)
46}
47
48/// One path component, ready for `openat`.
49pub 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
60/// Open one entry of a pinned directory read-only, without following a symlink.
61pub fn open_entry(directory: &File, name: &EntryName) -> io::Result<File> {
62    // NONBLOCK avoids waiting on a FIFO before its type can be rejected.
63    Ok(openat(
64        directory,
65        &name.0,
66        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
67        Mode::empty(),
68    )?
69    .into())
70}
71
72/// Whether `open_entry` failed because the entry is a symlink: `O_NOFOLLOW` reports `ELOOP`
73/// on Linux and macOS, and `EMLINK` on FreeBSD and DragonFly.
74pub 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
83/// The entries of a pinned directory, listed through its descriptor rather than a path
84/// that could have been replaced by a link.
85pub fn read_dir(directory: &File) -> io::Result<impl Iterator<Item = io::Result<DirEntry<'_>>>> {
86    // Open a new description, not dup: directory offsets must be independent across scans.
87    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/// A failed or exhausted stream never reads the directory again.
101#[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
157/// The type of one entry of a pinned directory, without following it.
158pub fn entry_kind(directory: &File, name: &OsStr) -> io::Result<FileKind> {
159    let name = EntryName::new(name).ok_or(io::ErrorKind::InvalidInput)?;
160    // Inspect the entry itself, never its target.
161    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/// Other Unix platforms keep their existing case-sensitive open behavior.
171#[cfg(not(target_os = "macos"))]
172pub fn exact_name(_file: &File, _name: &OsStr) -> io::Result<bool> {
173    Ok(true)
174}
175
176/// Preserve the existing case-sensitive logical namespace on other Unix systems.
177#[cfg(not(target_os = "macos"))]
178pub fn case_sensitive(_directory: &File) -> io::Result<bool> {
179    Ok(true)
180}