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