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