Skip to main content

openpgp_cert_d/
unixdir.rs

1use std::cell::OnceCell;
2use std::ffi::CStr;
3use std::ffi::c_char;
4use std::os::unix::ffi::OsStringExt;
5use std::path::Path;
6
7use crate::Tag;
8
9type Result<T> = std::result::Result<T, std::io::Error>;
10
11pub(crate) struct FileType(u8);
12
13impl FileType {
14    /// Whether a file is a directory.
15    ///
16    /// According to glibc's documentation:
17    ///
18    ///   Currently, only some filesystems (among them: Btrfs, ext2,
19    ///   ext3, and ext4) have full support for returning the file type
20    ///   in d_type.  All applications must properly handle a re‐ turn
21    ///   of DT_UNKNOWN.
22    pub fn is_dir(&self) -> bool {
23        self.0 == libc::DT_DIR
24    }
25
26    /// Whether a file's type is not known.
27    pub fn is_unknown(&self) -> bool {
28        self.0 == libc::DT_UNKNOWN
29    }
30}
31
32// Not all Unix platforms have 64-bit variants of stat64, etc.  Rust's
33// libc doesn't define a nice way to figure out if those functions are
34// available.  Eventually, we can use something like:
35//
36//   #[cfg_accessible(libc::stat64)]
37//
38// (See https://github.com/rust-lang/rust/issues/64797), but that
39// hasn't been stabilized yet.
40//
41// For now, we copy Rust's libc's strategy:
42//
43//   https://github.com/rust-lang/rust/blob/5a2dd7d4f3210629e65879aeecbe643ba3b86bb4/library/std/src/sys/pal/unix/fs.rs#L22
44#[cfg(any(all(target_os = "linux", not(target_env = "musl")), target_os = "hurd"))]
45mod libc64 {
46    pub(super) use libc::stat64 as stat;
47    pub(super) use libc::fstatat64 as fstatat;
48    pub(super) use libc::dirent64 as dirent;
49    pub(super) use libc::readdir64 as readdir;
50}
51#[cfg(not(any(all(target_os = "linux", not(target_env = "musl")), target_os = "hurd")))]
52mod libc64 {
53    pub(super) use libc::stat;
54    pub(super) use libc::fstatat;
55    pub(super) use libc::dirent;
56    pub(super) use libc::readdir;
57}
58
59/// A thin wrapper around a `libc::stat64`.
60pub(crate) struct Metadata(libc64::stat);
61
62impl Metadata {
63    /// The size.
64    pub fn size(&self) -> u64 {
65        self.0.st_size as u64
66    }
67
68    /// The modification time as the time since the Unix epoch.
69    pub fn modified(&self) -> std::time::Duration {
70        // Netbsd-like systems.  See:
71        // https://github.com/rust-lang/libc/blob/a0f5b4b21391252fe38b2df9310dc65e37b07d9f/src/unix/bsd/mod.rs#L931
72        #[cfg(any(target_os = "openbsd", target_os = "netbsd"))]
73        return std::time::Duration::new(
74            self.0.st_mtime as u64,
75            self.0.st_mtimensec as u32);
76
77        #[cfg(not(any(target_os = "openbsd", target_os = "netbsd")))]
78        return std::time::Duration::new(
79            self.0.st_mtime as u64,
80            self.0.st_mtime_nsec as u32);
81    }
82
83    /// Whether a file is a directory.
84    pub fn is_dir(&self) -> bool {
85        (self.0.st_mode & libc::S_IFMT) == libc::S_IFDIR
86    }
87}
88
89impl std::convert::From<&crate::unixdir::Metadata> for Tag {
90    fn from(m: &Metadata) -> Self {
91        let d = m.modified();
92        let size = m.size();
93
94        Tag::new(d.as_secs(), d.subsec_nanos(), size, m.is_dir())
95    }
96}
97
98impl Metadata {
99    fn fstat(dir: *mut libc::DIR,
100             nul_terminated_filename: &[u8])
101             -> Result<Self>
102    {
103        // The last character must be a NUL, i.e., this has to be a c string.
104        assert_eq!(nul_terminated_filename[nul_terminated_filename.len() - 1],
105                   0);
106
107        let dirfd = unsafe { libc::dirfd(dir) };
108        if dirfd == -1 {
109            return Err(std::io::Error::last_os_error());
110        }
111
112        let mut statbuf = std::mem::MaybeUninit::<libc64::stat>::uninit();
113
114        let result = unsafe {
115            libc64::fstatat(
116                dirfd,
117                nul_terminated_filename.as_ptr() as *const c_char,
118                statbuf.as_mut_ptr(),
119                libc::AT_SYMLINK_NOFOLLOW,
120            )
121        };
122        if result == -1 {
123            return Err(std::io::Error::last_os_error());
124        }
125
126        Ok(Metadata(unsafe { statbuf.assume_init() }))
127    }
128}
129
130/// A thin wrapper for a `libc::dirent64`.
131///
132/// [`libc::dirent64`](https://docs.rs/libc/latest/libc/struct.dirent64.html)
133pub(crate) struct DirEntry {
134    dir: *mut libc::DIR,
135
136    // Get ready for some insanity.
137    //
138    // On some platforms, `dirent.d_name` (or `dirent.d_name`) may be
139    // a fixed-sized array, but the implementation uses a different
140    // structure, which has a flex array.  This means that if we're
141    // not careful about how we use the raw pointer, we may end up
142    // copying the struct, which results in a seg fault when the
143    // underlying struct is smaller than the public struct!  A Rust
144    // libc comment corroborates this:
145    //
146    // > The dirent64 pointers that libc returns from readdir64 are
147    // > allowed to point to allocations smaller _or_ LARGER than
148    // > implied by the definition of the struct.
149    //
150    // https://github.com/rust-lang/rust/blob/5a2dd7d4f3210629e65879aeecbe643ba3b86bb4/library/std/src/sys/pal/unix/fs.rs#L733
151    //
152    // This means that we can't do the following, which copies the
153    // `struct dirent`:
154    //
155    // ```
156    // unsafe { *self.entry }.d_type
157    // ```
158    //
159    // Instead, we need to do the following, which returns a reference
160    // to the `struct dirent`:
161    //
162    // ```
163    // unsafe { (&*self.entry).d_type }
164    // ```
165    entry: *mut libc64::dirent,
166
167    name_len: OnceCell<usize>,
168    // We save the metadata inline to avoid a heap allocation.
169    metadata: OnceCell<Result<Metadata>>,
170}
171
172impl DirEntry {
173    // Return `self.entry`'s d_type, safely.
174    //
175    // See the insanity comment above.
176    fn entry_d_type(&self) -> libc::c_uchar {
177        unsafe { (&*self.entry).d_type }
178    }
179
180    // Return `self.entry`'s d_name, safely.
181    //
182    // See the insanity comment above.
183    fn entry_d_name(&self) -> *const c_char {
184        unsafe { (&*self.entry).d_name.as_ptr() as *const c_char }
185    }
186
187    /// Returns the file's type, as recorded in the directory.
188    pub fn file_type(&self) -> FileType {
189        FileType(self.entry_d_type())
190    }
191
192    /// Returns the filename.
193    ///
194    /// Note: this is not NUL terminated.
195    pub fn file_name(&self) -> &[u8] {
196        unsafe {
197            let name = self.entry_d_name();
198
199            let name_len = *self.name_len.get_or_init(|| {
200                libc::strlen(name)
201            });
202
203            std::slice::from_raw_parts(
204                name as *const u8,
205                name_len)
206        }
207    }
208
209    /// Stats the file.
210    ///
211    /// To avoid a heap allocation, the data struct is stored inline.
212    /// To avoid races, the lifetime is bound to self.
213    pub fn metadata(&self) -> Result<&Metadata> {
214        // Rewrite this to use OnceCell::get_or_try_init once that has
215        // stabilized.  Until then we do a little dance with the
216        // Result.
217        let result = self.metadata.get_or_init(|| {
218            let dirfd = unsafe { libc::dirfd(self.dir) };
219            if dirfd == -1 {
220                return Err(std::io::Error::last_os_error());
221            }
222
223            let mut statbuf = std::mem::MaybeUninit::<libc64::stat>::uninit();
224
225            let result = unsafe {
226                libc64::fstatat(
227                    dirfd,
228                    self.entry_d_name(),
229                    statbuf.as_mut_ptr(),
230                    libc::AT_SYMLINK_NOFOLLOW,
231                )
232            };
233            if result == -1 {
234                return Err(std::io::Error::last_os_error());
235            }
236
237            Ok(Metadata(unsafe { statbuf.assume_init() }))
238        });
239
240        match result {
241            Ok(metadata) => Ok(metadata),
242            Err(err) => {
243                if let Some(underlying) = err.get_ref() {
244                    // We can't clone the error, so we clone the error
245                    // kind and turn the error into a string.  It's
246                    // not great, but its good enough for us.
247                    Err(std::io::Error::new(
248                        err.kind(),
249                        underlying.to_string()))
250                } else {
251                    Err(std::io::Error::from(err.kind()))
252                }
253            },
254        }
255    }
256}
257
258pub(crate) struct Dir {
259    dir: Option<*mut libc::DIR>,
260    entry: Option<DirEntry>,
261}
262
263impl Drop for Dir {
264    fn drop(&mut self) {
265        if let Some(dir) = self.dir.take() {
266            unsafe { libc::closedir(dir) };
267        }
268        self.entry = None;
269    }
270}
271
272impl Dir {
273    pub fn open(dir: &Path) -> Result<Self> {
274        let mut dir = dir.as_os_str().to_os_string().into_vec();
275        // NUL-terminate it.
276        dir.push(0);
277        let dir = unsafe { CStr::from_ptr(dir.as_ptr() as *const c_char) };
278        let dir = unsafe { libc::opendir(dir.as_ptr().cast()) };
279        if dir.is_null() {
280            return Err(std::io::Error::last_os_error());
281        }
282
283        let dir = Dir {
284            dir: Some(dir),
285            entry: None,
286        };
287        Ok(dir)
288    }
289
290    /// Get the next directory entry.
291    ///
292    /// Returns None, if the end of directory has been reached.
293    ///
294    /// DirEntry is deallocated when the directory pointer is
295    /// advanced.  Hence, the lifetime of the returned DirEntry is
296    /// tied to the lifetime of the &mut to self.
297    pub fn readdir(&mut self) -> Option<&mut DirEntry> {
298       let dir = self.dir?;
299
300        let entry = unsafe { libc64::readdir(dir) };
301        if entry.is_null() {
302            unsafe { libc::closedir(dir) };
303            self.dir = None;
304            self.entry = None;
305            return None;
306        }
307
308        self.entry = Some(DirEntry {
309            dir,
310            entry,
311            name_len: OnceCell::default(),
312            metadata: OnceCell::default(),
313        });
314        self.entry.as_mut()
315    }
316
317    /// Stat an entry in the directory.
318    pub fn fstat(&mut self, nul_terminated_filename: &[u8]) -> Result<Metadata> {
319        let dir = self.dir.ok_or_else(|| {
320            std::io::Error::new(std::io::ErrorKind::Other, "Directory closed")
321        })?;
322
323        Metadata::fstat(dir, nul_terminated_filename)
324    }
325}