rustix/backend/libc/fs/
dir.rs

1#[cfg(not(any(
2    solarish,
3    target_os = "aix",
4    target_os = "haiku",
5    target_os = "nto",
6    target_os = "vita"
7)))]
8use super::types::FileType;
9use crate::backend::c;
10use crate::backend::conv::owned_fd;
11use crate::fd::{AsFd, BorrowedFd, OwnedFd};
12use crate::ffi::{CStr, CString};
13use crate::fs::{fcntl_getfl, openat, Mode, OFlags};
14#[cfg(not(target_os = "vita"))]
15use crate::fs::{fstat, Stat};
16#[cfg(not(any(
17    solarish,
18    target_os = "haiku",
19    target_os = "horizon",
20    target_os = "netbsd",
21    target_os = "nto",
22    target_os = "redox",
23    target_os = "vita",
24    target_os = "wasi",
25)))]
26use crate::fs::{fstatfs, StatFs};
27#[cfg(not(any(
28    solarish,
29    target_os = "haiku",
30    target_os = "redox",
31    target_os = "vita",
32    target_os = "wasi"
33)))]
34use crate::fs::{fstatvfs, StatVfs};
35use crate::io;
36#[cfg(not(any(target_os = "fuchsia", target_os = "vita", target_os = "wasi")))]
37#[cfg(feature = "process")]
38use crate::process::fchdir;
39use alloc::borrow::ToOwned as _;
40#[cfg(not(any(linux_like, target_os = "hurd")))]
41use c::readdir as libc_readdir;
42#[cfg(any(linux_like, target_os = "hurd"))]
43use c::readdir64 as libc_readdir;
44use core::fmt;
45use core::ptr::NonNull;
46use libc_errno::{errno, set_errno, Errno};
47
48/// `DIR*`
49pub struct Dir {
50    /// The `libc` `DIR` pointer.
51    libc_dir: NonNull<c::DIR>,
52
53    /// Have we seen any errors in this iteration?
54    any_errors: bool,
55}
56
57impl Dir {
58    /// Take ownership of `fd` and construct a `Dir` that reads entries from
59    /// the given directory file descriptor.
60    #[inline]
61    pub fn new<Fd: Into<OwnedFd>>(fd: Fd) -> io::Result<Self> {
62        Self::_new(fd.into())
63    }
64
65    #[inline]
66    fn _new(fd: OwnedFd) -> io::Result<Self> {
67        let raw = owned_fd(fd);
68        unsafe {
69            let libc_dir = c::fdopendir(raw);
70
71            if let Some(libc_dir) = NonNull::new(libc_dir) {
72                Ok(Self {
73                    libc_dir,
74                    any_errors: false,
75                })
76            } else {
77                let err = io::Errno::last_os_error();
78                let _ = c::close(raw);
79                Err(err)
80            }
81        }
82    }
83
84    /// Borrow `fd` and construct a `Dir` that reads entries from the given
85    /// directory file descriptor.
86    #[inline]
87    pub fn read_from<Fd: AsFd>(fd: Fd) -> io::Result<Self> {
88        Self::_read_from(fd.as_fd())
89    }
90
91    #[inline]
92    #[allow(unused_mut)]
93    fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> {
94        let mut any_errors = false;
95
96        // Given an arbitrary `OwnedFd`, it's impossible to know whether the
97        // user holds a `dup`'d copy which could continue to modify the
98        // file description state, which would cause Undefined Behavior after
99        // our call to `fdopendir`. To prevent this, we obtain an independent
100        // `OwnedFd`.
101        let flags = fcntl_getfl(fd)?;
102        let fd_for_dir = match openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty()) {
103            Ok(fd) => fd,
104            #[cfg(not(target_os = "wasi"))]
105            Err(io::Errno::NOENT) => {
106                // If "." doesn't exist, it means the directory was removed.
107                // We treat that as iterating through a directory with no
108                // entries.
109                any_errors = true;
110                crate::io::dup(fd)?
111            }
112            Err(err) => return Err(err),
113        };
114
115        let raw = owned_fd(fd_for_dir);
116        unsafe {
117            let libc_dir = c::fdopendir(raw);
118
119            if let Some(libc_dir) = NonNull::new(libc_dir) {
120                Ok(Self {
121                    libc_dir,
122                    any_errors,
123                })
124            } else {
125                let err = io::Errno::last_os_error();
126                let _ = c::close(raw);
127                Err(err)
128            }
129        }
130    }
131
132    /// `rewinddir(self)`
133    #[inline]
134    pub fn rewind(&mut self) {
135        self.any_errors = false;
136        unsafe { c::rewinddir(self.libc_dir.as_ptr()) }
137    }
138
139    /// `seekdir(self, offset)`
140    ///
141    /// This function is only available on 64-bit platforms because it's
142    /// implemented using [`libc::seekdir`] which only supports offsets that
143    /// fit in a `c_long`.
144    ///
145    /// [`libc::seekdir`]: https://docs.rs/libc/*/arm-unknown-linux-gnueabihf/libc/fn.seekdir.html
146    #[cfg(target_pointer_width = "64")]
147    #[cfg_attr(docsrs, doc(cfg(target_pointer_width = "64")))]
148    #[doc(alias = "seekdir")]
149    #[inline]
150    pub fn seek(&mut self, offset: i64) -> io::Result<()> {
151        self.any_errors = false;
152        unsafe { c::seekdir(self.libc_dir.as_ptr(), offset) }
153        Ok(())
154    }
155
156    /// `readdir(self)`, where `None` means the end of the directory.
157    pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
158        // If we've seen errors, don't continue to try to read anything
159        // further.
160        if self.any_errors {
161            return None;
162        }
163
164        set_errno(Errno(0));
165        let dirent_ptr = unsafe { libc_readdir(self.libc_dir.as_ptr()) };
166        if dirent_ptr.is_null() {
167            let curr_errno = errno().0;
168            if curr_errno == 0 {
169                // We successfully reached the end of the stream.
170                None
171            } else {
172                // `errno` is unknown or non-zero, so an error occurred.
173                self.any_errors = true;
174                Some(Err(io::Errno(curr_errno)))
175            }
176        } else {
177            // We successfully read an entry.
178            unsafe {
179                let dirent = &*dirent_ptr;
180
181                // We have our own copy of OpenBSD's dirent; check that the
182                // layout minimally matches libc's.
183                #[cfg(target_os = "openbsd")]
184                check_dirent_layout(dirent);
185
186                let result = DirEntry {
187                    #[cfg(not(any(
188                        solarish,
189                        target_os = "aix",
190                        target_os = "haiku",
191                        target_os = "nto",
192                        target_os = "vita"
193                    )))]
194                    d_type: dirent.d_type,
195
196                    #[cfg(not(any(freebsdlike, netbsdlike, target_os = "vita")))]
197                    d_ino: dirent.d_ino,
198
199                    #[cfg(any(
200                        linux_like,
201                        solarish,
202                        target_os = "fuchsia",
203                        target_os = "hermit",
204                        target_os = "openbsd",
205                        target_os = "redox"
206                    ))]
207                    d_off: dirent.d_off,
208
209                    #[cfg(any(freebsdlike, netbsdlike))]
210                    d_fileno: dirent.d_fileno,
211
212                    name: CStr::from_ptr(dirent.d_name.as_ptr().cast()).to_owned(),
213                };
214
215                Some(Ok(result))
216            }
217        }
218    }
219
220    /// `fstat(self)`
221    #[cfg(not(any(target_os = "horizon", target_os = "vita")))]
222    #[inline]
223    pub fn stat(&self) -> io::Result<Stat> {
224        fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
225    }
226
227    /// `fstatfs(self)`
228    #[cfg(not(any(
229        solarish,
230        target_os = "haiku",
231        target_os = "horizon",
232        target_os = "netbsd",
233        target_os = "nto",
234        target_os = "redox",
235        target_os = "vita",
236        target_os = "wasi",
237    )))]
238    #[inline]
239    pub fn statfs(&self) -> io::Result<StatFs> {
240        fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
241    }
242
243    /// `fstatvfs(self)`
244    #[cfg(not(any(
245        solarish,
246        target_os = "haiku",
247        target_os = "horizon",
248        target_os = "redox",
249        target_os = "vita",
250        target_os = "wasi"
251    )))]
252    #[inline]
253    pub fn statvfs(&self) -> io::Result<StatVfs> {
254        fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
255    }
256
257    /// `fchdir(self)`
258    #[cfg(feature = "process")]
259    #[cfg(not(any(
260        target_os = "fuchsia",
261        target_os = "horizon",
262        target_os = "vita",
263        target_os = "wasi"
264    )))]
265    #[cfg_attr(docsrs, doc(cfg(feature = "process")))]
266    #[inline]
267    pub fn chdir(&self) -> io::Result<()> {
268        fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) })
269    }
270}
271
272/// `Dir` is `Send` and `Sync`, because even though it contains internal
273/// state, all methods that modify the state require a `mut &self` and
274/// can therefore not be called concurrently. Calling them from different
275/// threads sequentially is fine.
276unsafe impl Send for Dir {}
277unsafe impl Sync for Dir {}
278
279impl Drop for Dir {
280    #[inline]
281    fn drop(&mut self) {
282        unsafe { c::closedir(self.libc_dir.as_ptr()) };
283    }
284}
285
286impl Iterator for Dir {
287    type Item = io::Result<DirEntry>;
288
289    #[inline]
290    fn next(&mut self) -> Option<Self::Item> {
291        Self::read(self)
292    }
293}
294
295impl fmt::Debug for Dir {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        let mut s = f.debug_struct("Dir");
298        #[cfg(not(any(target_os = "horizon", target_os = "vita")))]
299        s.field("fd", unsafe { &c::dirfd(self.libc_dir.as_ptr()) });
300        s.finish()
301    }
302}
303
304/// `struct dirent`
305#[derive(Debug)]
306pub struct DirEntry {
307    #[cfg(not(any(
308        solarish,
309        target_os = "aix",
310        target_os = "haiku",
311        target_os = "nto",
312        target_os = "vita"
313    )))]
314    d_type: u8,
315
316    #[cfg(not(any(freebsdlike, netbsdlike, target_os = "vita")))]
317    d_ino: c::ino_t,
318
319    #[cfg(any(freebsdlike, netbsdlike))]
320    d_fileno: c::ino_t,
321
322    name: CString,
323
324    #[cfg(any(
325        linux_like,
326        solarish,
327        target_os = "fuchsia",
328        target_os = "hermit",
329        target_os = "openbsd",
330        target_os = "redox"
331    ))]
332    d_off: c::off_t,
333}
334
335impl DirEntry {
336    /// Returns the file name of this directory entry.
337    #[inline]
338    pub fn file_name(&self) -> &CStr {
339        &self.name
340    }
341
342    /// Returns the “offset” of this directory entry. This is not a true
343    /// numerical offset but an opaque cookie that identifies a position in the
344    /// given stream.
345    #[cfg(any(
346        linux_like,
347        solarish,
348        target_os = "fuchsia",
349        target_os = "hermit",
350        target_os = "openbsd",
351        target_os = "redox"
352    ))]
353    #[inline]
354    pub fn offset(&self) -> i64 {
355        self.d_off as i64
356    }
357
358    /// Returns the type of this directory entry.
359    #[cfg(not(any(
360        solarish,
361        target_os = "aix",
362        target_os = "haiku",
363        target_os = "nto",
364        target_os = "vita"
365    )))]
366    #[inline]
367    pub fn file_type(&self) -> FileType {
368        FileType::from_dirent_d_type(self.d_type)
369    }
370
371    /// Return the inode number of this directory entry.
372    #[cfg(not(any(freebsdlike, netbsdlike, target_os = "vita")))]
373    #[inline]
374    pub fn ino(&self) -> u64 {
375        self.d_ino as u64
376    }
377
378    /// Return the inode number of this directory entry.
379    #[cfg(any(freebsdlike, netbsdlike))]
380    #[inline]
381    pub fn ino(&self) -> u64 {
382        #[allow(clippy::useless_conversion)]
383        self.d_fileno.into()
384    }
385}
386
387/// libc's OpenBSD `dirent` has a private field so we can't construct it
388/// directly, so we declare it ourselves to make all fields accessible.
389#[cfg(target_os = "openbsd")]
390#[repr(C)]
391#[derive(Debug)]
392struct libc_dirent {
393    d_fileno: c::ino_t,
394    d_off: c::off_t,
395    d_reclen: u16,
396    d_type: u8,
397    d_namlen: u8,
398    __d_padding: [u8; 4],
399    d_name: [c::c_char; 256],
400}
401
402/// We have our own copy of OpenBSD's dirent; check that the layout
403/// minimally matches libc's.
404#[cfg(target_os = "openbsd")]
405fn check_dirent_layout(dirent: &c::dirent) {
406    use crate::utils::as_ptr;
407
408    // Check that the basic layouts match.
409    #[cfg(test)]
410    {
411        assert_eq_size!(libc_dirent, c::dirent);
412        assert_eq_size!(libc_dirent, c::dirent);
413    }
414
415    // Check that the field offsets match.
416    assert_eq!(
417        {
418            let z = libc_dirent {
419                d_fileno: 0_u64,
420                d_off: 0_i64,
421                d_reclen: 0_u16,
422                d_type: 0_u8,
423                d_namlen: 0_u8,
424                __d_padding: [0_u8; 4],
425                d_name: [0 as c::c_char; 256],
426            };
427            let base = as_ptr(&z) as usize;
428            (
429                (as_ptr(&z.d_fileno) as usize) - base,
430                (as_ptr(&z.d_off) as usize) - base,
431                (as_ptr(&z.d_reclen) as usize) - base,
432                (as_ptr(&z.d_type) as usize) - base,
433                (as_ptr(&z.d_namlen) as usize) - base,
434                (as_ptr(&z.d_name) as usize) - base,
435            )
436        },
437        {
438            let z = dirent;
439            let base = as_ptr(z) as usize;
440            (
441                (as_ptr(&z.d_fileno) as usize) - base,
442                (as_ptr(&z.d_off) as usize) - base,
443                (as_ptr(&z.d_reclen) as usize) - base,
444                (as_ptr(&z.d_type) as usize) - base,
445                (as_ptr(&z.d_namlen) as usize) - base,
446                (as_ptr(&z.d_name) as usize) - base,
447            )
448        }
449    );
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn dir_iterator_handles_io_errors() {
458        // create a dir, keep the FD, then delete the dir
459        let tmp = tempfile::tempdir().unwrap();
460        let fd = crate::fs::openat(
461            crate::fs::CWD,
462            tmp.path(),
463            crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC,
464            crate::fs::Mode::empty(),
465        )
466        .unwrap();
467
468        let file_fd = crate::fs::openat(
469            &fd,
470            tmp.path().join("test.txt"),
471            crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE,
472            crate::fs::Mode::RWXU,
473        )
474        .unwrap();
475
476        let mut dir = Dir::read_from(&fd).unwrap();
477
478        // Reach inside the `Dir` and replace its directory with a file, which
479        // will cause the subsequent `readdir` to fail.
480        unsafe {
481            let raw_fd = c::dirfd(dir.libc_dir.as_ptr());
482            let mut owned_fd: crate::fd::OwnedFd = crate::fd::FromRawFd::from_raw_fd(raw_fd);
483            crate::io::dup2(&file_fd, &mut owned_fd).unwrap();
484            core::mem::forget(owned_fd);
485        }
486
487        // FreeBSD and macOS seem to read some directory entries before we call
488        // `.next()`.
489        #[cfg(any(apple, freebsdlike))]
490        {
491            dir.rewind();
492        }
493
494        assert!(matches!(dir.next(), Some(Err(_))));
495        assert!(dir.next().is_none());
496    }
497}