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