1use super::super::c;
2use super::super::conv::owned_fd;
3#[cfg(not(any(target_os = "haiku", target_os = "illumos", target_os = "solaris")))]
4use super::types::FileType;
5use crate::fd::{AsFd, BorrowedFd};
6use crate::ffi::CStr;
7#[cfg(target_os = "wasi")]
8use crate::ffi::CString;
9use crate::fs::{fcntl_getfl, fstat, openat, Mode, OFlags, Stat};
10#[cfg(not(any(
11    target_os = "haiku",
12    target_os = "illumos",
13    target_os = "netbsd",
14    target_os = "redox",
15    target_os = "solaris",
16    target_os = "wasi",
17)))]
18use crate::fs::{fstatfs, StatFs};
19#[cfg(not(any(
20    target_os = "haiku",
21    target_os = "illumos",
22    target_os = "redox",
23    target_os = "solaris",
24    target_os = "wasi",
25)))]
26use crate::fs::{fstatvfs, StatVfs};
27use crate::io;
28#[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))]
29use crate::process::fchdir;
30#[cfg(target_os = "wasi")]
31use alloc::borrow::ToOwned;
32#[cfg(not(any(
33    target_os = "android",
34    target_os = "emscripten",
35    target_os = "l4re",
36    target_os = "linux",
37    target_os = "openbsd",
38)))]
39use c::dirent as libc_dirent;
40#[cfg(not(any(
41    target_os = "android",
42    target_os = "emscripten",
43    target_os = "l4re",
44    target_os = "linux",
45)))]
46use c::readdir as libc_readdir;
47#[cfg(any(
48    target_os = "android",
49    target_os = "emscripten",
50    target_os = "l4re",
51    target_os = "linux",
52))]
53use c::{dirent64 as libc_dirent, readdir64 as libc_readdir};
54use core::fmt;
55use core::mem::zeroed;
56use core::ptr::NonNull;
57use libc_errno::{errno, set_errno, Errno};
58
59/// `DIR*`
60#[repr(transparent)]
61pub struct Dir(NonNull<c::DIR>);
62
63impl Dir {
64    /// Construct a `Dir` that reads entries from the given directory
65    /// file descriptor.
66    #[inline]
67    pub fn read_from<Fd: AsFd>(fd: Fd) -> io::Result<Self> {
68        Self::_read_from(fd.as_fd())
69    }
70
71    #[inline]
72    fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> {
73        // Given an arbitrary `OwnedFd`, it's impossible to know whether the
74        // user holds a `dup`'d copy which could continue to modify the
75        // file description state, which would cause Undefined Behavior after
76        // our call to `fdopendir`. To prevent this, we obtain an independent
77        // `OwnedFd`.
78        let flags = fcntl_getfl(fd)?;
79        let fd_for_dir = openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?;
80
81        let raw = owned_fd(fd_for_dir);
82        unsafe {
83            let libc_dir = c::fdopendir(raw);
84
85            if let Some(libc_dir) = NonNull::new(libc_dir) {
86                Ok(Self(libc_dir))
87            } else {
88                let err = io::Errno::last_os_error();
89                let _ = c::close(raw);
90                Err(err)
91            }
92        }
93    }
94
95    /// `rewinddir(self)`
96    #[inline]
97    pub fn rewind(&mut self) {
98        unsafe { c::rewinddir(self.0.as_ptr()) }
99    }
100
101    /// `readdir(self)`, where `None` means the end of the directory.
102    pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
103        set_errno(Errno(0));
104        let dirent_ptr = unsafe { libc_readdir(self.0.as_ptr()) };
105        if dirent_ptr.is_null() {
106            let curr_errno = errno().0;
107            if curr_errno == 0 {
108                // We successfully reached the end of the stream.
109                None
110            } else {
111                // `errno` is unknown or non-zero, so an error occurred.
112                Some(Err(io::Errno(curr_errno)))
113            }
114        } else {
115            // We successfully read an entry.
116            unsafe {
117                // We have our own copy of OpenBSD's dirent; check that the
118                // layout minimally matches libc's.
119                #[cfg(target_os = "openbsd")]
120                check_dirent_layout(&*dirent_ptr);
121
122                let result = DirEntry {
123                    dirent: read_dirent(&*dirent_ptr.cast()),
124
125                    #[cfg(target_os = "wasi")]
126                    name: CStr::from_ptr((*dirent_ptr).d_name.as_ptr()).to_owned(),
127                };
128
129                Some(Ok(result))
130            }
131        }
132    }
133
134    /// `fstat(self)`
135    #[inline]
136    pub fn stat(&self) -> io::Result<Stat> {
137        fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
138    }
139
140    /// `fstatfs(self)`
141    #[cfg(not(any(
142        target_os = "haiku",
143        target_os = "illumos",
144        target_os = "netbsd",
145        target_os = "redox",
146        target_os = "solaris",
147        target_os = "wasi",
148    )))]
149    #[inline]
150    pub fn statfs(&self) -> io::Result<StatFs> {
151        fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
152    }
153
154    /// `fstatvfs(self)`
155    #[cfg(not(any(
156        target_os = "haiku",
157        target_os = "illumos",
158        target_os = "redox",
159        target_os = "solaris",
160        target_os = "wasi",
161    )))]
162    #[inline]
163    pub fn statvfs(&self) -> io::Result<StatVfs> {
164        fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
165    }
166
167    /// `fchdir(self)`
168    #[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))]
169    #[inline]
170    pub fn chdir(&self) -> io::Result<()> {
171        fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) })
172    }
173}
174
175// A `dirent` pointer returned from `readdir` may not point to a full `dirent`
176// struct, as the name is NUL-terminated and memory may not be allocated for
177// the full extent of the struct. Copy the fields one at a time.
178unsafe fn read_dirent(input: &libc_dirent) -> libc_dirent {
179    #[cfg(not(any(
180        target_os = "aix",
181        target_os = "haiku",
182        target_os = "illumos",
183        target_os = "solaris"
184    )))]
185    let d_type = input.d_type;
186
187    #[cfg(not(any(
188        target_os = "aix",
189        target_os = "dragonfly",
190        target_os = "freebsd",
191        target_os = "haiku",
192        target_os = "ios",
193        target_os = "macos",
194        target_os = "netbsd",
195        target_os = "wasi",
196    )))]
197    let d_off = input.d_off;
198
199    #[cfg(target_os = "aix")]
200    let d_offset = input.d_offset;
201
202    #[cfg(not(any(
203        target_os = "dragonfly",
204        target_os = "freebsd",
205        target_os = "netbsd",
206        target_os = "openbsd",
207    )))]
208    let d_ino = input.d_ino;
209
210    #[cfg(any(
211        target_os = "dragonfly",
212        target_os = "freebsd",
213        target_os = "netbsd",
214        target_os = "openbsd"
215    ))]
216    let d_fileno = input.d_fileno;
217
218    #[cfg(not(any(target_os = "dragonfly", target_os = "wasi")))]
219    let d_reclen = input.d_reclen;
220
221    #[cfg(any(
222        target_os = "dragonfly",
223        target_os = "freebsd",
224        target_os = "netbsd",
225        target_os = "openbsd",
226        target_os = "ios",
227        target_os = "macos",
228    ))]
229    let d_namlen = input.d_namlen;
230
231    #[cfg(any(target_os = "ios", target_os = "macos"))]
232    let d_seekoff = input.d_seekoff;
233
234    #[cfg(target_os = "haiku")]
235    let d_dev = input.d_dev;
236    #[cfg(target_os = "haiku")]
237    let d_pdev = input.d_pdev;
238    #[cfg(target_os = "haiku")]
239    let d_pino = input.d_pino;
240
241    // Construct the input. Rust will give us an error if any OS has a input
242    // with a field that we missed here. And we can avoid blindly copying the
243    // whole `d_name` field, which may not be entirely allocated.
244    #[cfg_attr(target_os = "wasi", allow(unused_mut))]
245    #[cfg(not(any(target_os = "freebsd", target_os = "dragonfly")))]
246    let mut dirent = libc_dirent {
247        #[cfg(not(any(
248            target_os = "aix",
249            target_os = "haiku",
250            target_os = "illumos",
251            target_os = "solaris"
252        )))]
253        d_type,
254        #[cfg(not(any(
255            target_os = "aix",
256            target_os = "freebsd",  // Until FreeBSD 12
257            target_os = "haiku",
258            target_os = "ios",
259            target_os = "macos",
260            target_os = "netbsd",
261            target_os = "wasi",
262        )))]
263        d_off,
264        #[cfg(target_os = "aix")]
265        d_offset,
266        #[cfg(not(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd")))]
267        d_ino,
268        #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))]
269        d_fileno,
270        #[cfg(not(target_os = "wasi"))]
271        d_reclen,
272        #[cfg(any(
273            target_os = "aix",
274            target_os = "freebsd",
275            target_os = "ios",
276            target_os = "macos",
277            target_os = "netbsd",
278            target_os = "openbsd",
279        ))]
280        d_namlen,
281        #[cfg(any(target_os = "ios", target_os = "macos"))]
282        d_seekoff,
283        // The `d_name` field is NUL-terminated, and we need to be careful not
284        // to read bytes past the NUL, even though they're within the nominal
285        // extent of the `struct dirent`, because they may not be allocated. So
286        // don't read it from `dirent_ptr`.
287        //
288        // In theory this could use `MaybeUninit::uninit().assume_init()`, but
289        // that [invokes undefined behavior].
290        //
291        // [invokes undefined behavior]: https://doc.rust-lang.org/stable/core/mem/union.MaybeUninit.html#initialization-invariant
292        d_name: zeroed(),
293        #[cfg(target_os = "openbsd")]
294        __d_padding: zeroed(),
295        #[cfg(target_os = "haiku")]
296        d_dev,
297        #[cfg(target_os = "haiku")]
298        d_pdev,
299        #[cfg(target_os = "haiku")]
300        d_pino,
301    };
302    /*
303    pub d_ino: ino_t,
304    pub d_pino: i64,
305    pub d_reclen: ::c_ushort,
306    pub d_name: [::c_char; 1024], // Max length is _POSIX_PATH_MAX
307                                  // */
308
309    // On dragonfly and FreeBSD 12, `dirent` has some non-public padding fields
310    // so we can't directly initialize it.
311    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
312    let mut dirent = {
313        let mut dirent: libc_dirent = zeroed();
314        dirent.d_fileno = d_fileno;
315        dirent.d_namlen = d_namlen;
316        dirent.d_type = d_type;
317        #[cfg(target_os = "freebsd")]
318        {
319            dirent.d_reclen = d_reclen;
320        }
321        dirent
322    };
323
324    // Copy from d_name, reading up to and including the first NUL.
325    #[cfg(not(target_os = "wasi"))]
326    {
327        let name_len = CStr::from_ptr(input.d_name.as_ptr())
328            .to_bytes_with_nul()
329            .len();
330        dirent.d_name[..name_len].copy_from_slice(&input.d_name[..name_len]);
331    }
332
333    dirent
334}
335
336/// `Dir` implements `Send` but not `Sync`, because we use `readdir` which is
337/// not guaranteed to be thread-safe. Users can wrap this in a `Mutex` if they
338/// need `Sync`, which is effectively what'd need to do to implement `Sync`
339/// ourselves.
340unsafe impl Send for Dir {}
341
342impl Drop for Dir {
343    #[inline]
344    fn drop(&mut self) {
345        unsafe { c::closedir(self.0.as_ptr()) };
346    }
347}
348
349impl Iterator for Dir {
350    type Item = io::Result<DirEntry>;
351
352    #[inline]
353    fn next(&mut self) -> Option<Self::Item> {
354        Self::read(self)
355    }
356}
357
358impl fmt::Debug for Dir {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("Dir")
361            .field("fd", unsafe { &c::dirfd(self.0.as_ptr()) })
362            .finish()
363    }
364}
365
366/// `struct dirent`
367#[derive(Debug)]
368pub struct DirEntry {
369    dirent: libc_dirent,
370
371    #[cfg(target_os = "wasi")]
372    name: CString,
373}
374
375impl DirEntry {
376    /// Returns the file name of this directory entry.
377    #[inline]
378    pub fn file_name(&self) -> &CStr {
379        #[cfg(not(target_os = "wasi"))]
380        unsafe {
381            CStr::from_ptr(self.dirent.d_name.as_ptr())
382        }
383
384        #[cfg(target_os = "wasi")]
385        &self.name
386    }
387
388    /// Returns the type of this directory entry.
389    #[cfg(not(any(
390        target_os = "aix",
391        target_os = "haiku",
392        target_os = "illumos",
393        target_os = "solaris"
394    )))]
395    #[inline]
396    pub fn file_type(&self) -> FileType {
397        FileType::from_dirent_d_type(self.dirent.d_type)
398    }
399
400    /// Return the inode number of this directory entry.
401    #[cfg(not(any(
402        target_os = "dragonfly",
403        target_os = "freebsd",
404        target_os = "netbsd",
405        target_os = "openbsd",
406    )))]
407    #[inline]
408    pub fn ino(&self) -> u64 {
409        self.dirent.d_ino as u64
410    }
411
412    /// Return the inode number of this directory entry.
413    #[cfg(any(
414        target_os = "dragonfly",
415        target_os = "freebsd",
416        target_os = "netbsd",
417        target_os = "openbsd",
418    ))]
419    #[inline]
420    pub fn ino(&self) -> u64 {
421        #[allow(clippy::useless_conversion)]
422        self.dirent.d_fileno.into()
423    }
424}
425
426/// libc's OpenBSD `dirent` has a private field so we can't construct it
427/// directly, so we declare it ourselves to make all fields accessible.
428#[cfg(target_os = "openbsd")]
429#[repr(C)]
430#[derive(Debug)]
431struct libc_dirent {
432    d_fileno: c::ino_t,
433    d_off: c::off_t,
434    d_reclen: u16,
435    d_type: u8,
436    d_namlen: u8,
437    __d_padding: [u8; 4],
438    d_name: [c::c_char; 256],
439}
440
441/// We have our own copy of OpenBSD's dirent; check that the layout
442/// minimally matches libc's.
443#[cfg(target_os = "openbsd")]
444fn check_dirent_layout(dirent: &c::dirent) {
445    use crate::utils::as_ptr;
446    use core::mem::{align_of, size_of};
447
448    // Check that the basic layouts match.
449    assert_eq!(size_of::<libc_dirent>(), size_of::<c::dirent>());
450    assert_eq!(align_of::<libc_dirent>(), align_of::<c::dirent>());
451
452    // Check that the field offsets match.
453    assert_eq!(
454        {
455            let z = libc_dirent {
456                d_fileno: 0_u64,
457                d_off: 0_i64,
458                d_reclen: 0_u16,
459                d_type: 0_u8,
460                d_namlen: 0_u8,
461                __d_padding: [0_u8; 4],
462                d_name: [0 as c::c_char; 256],
463            };
464            let base = as_ptr(&z) as usize;
465            (
466                (as_ptr(&z.d_fileno) as usize) - base,
467                (as_ptr(&z.d_off) as usize) - base,
468                (as_ptr(&z.d_reclen) as usize) - base,
469                (as_ptr(&z.d_type) as usize) - base,
470                (as_ptr(&z.d_namlen) as usize) - base,
471                (as_ptr(&z.d_name) as usize) - base,
472            )
473        },
474        {
475            let z = dirent;
476            let base = as_ptr(z) as usize;
477            (
478                (as_ptr(&z.d_fileno) as usize) - base,
479                (as_ptr(&z.d_off) as usize) - base,
480                (as_ptr(&z.d_reclen) as usize) - base,
481                (as_ptr(&z.d_type) as usize) - base,
482                (as_ptr(&z.d_namlen) as usize) - base,
483                (as_ptr(&z.d_name) as usize) - base,
484            )
485        }
486    );
487}
488