| use crate::fd::{AsFd, BorrowedFd, OwnedFd}; |
| use crate::ffi::{CStr, CString}; |
| use crate::fs::{ |
| fcntl_getfl, fstat, fstatfs, fstatvfs, openat, FileType, Mode, OFlags, Stat, StatFs, StatVfs, |
| }; |
| use crate::io; |
| #[cfg(feature = "process")] |
| use crate::process::fchdir; |
| use crate::utils::as_ptr; |
| use alloc::borrow::ToOwned; |
| use alloc::vec::Vec; |
| use core::fmt; |
| use core::mem::size_of; |
| use linux_raw_sys::general::{linux_dirent64, SEEK_SET}; |
| |
| /// `DIR*` |
| pub struct Dir { |
| /// The `OwnedFd` that we read directory entries from. |
| fd: OwnedFd, |
| |
| /// Have we seen any errors in this iteration? |
| any_errors: bool, |
| |
| /// Should we rewind the stream on the next iteration? |
| rewind: bool, |
| |
| /// The buffer for `linux_dirent64` entries. |
| buf: Vec<u8>, |
| |
| /// Where we are in the buffer. |
| pos: usize, |
| } |
| |
| impl Dir { |
| /// Take ownership of `fd` and construct a `Dir` that reads entries from |
| /// the given directory file descriptor. |
| #[inline] |
| pub fn new<Fd: Into<OwnedFd>>(fd: Fd) -> io::Result<Self> { |
| Self::_new(fd.into()) |
| } |
| |
| #[inline] |
| fn _new(fd: OwnedFd) -> io::Result<Self> { |
| Ok(Self { |
| fd, |
| any_errors: false, |
| rewind: false, |
| buf: Vec::new(), |
| pos: 0, |
| }) |
| } |
| |
| /// Borrow `fd` and construct a `Dir` that reads entries from the given |
| /// directory file descriptor. |
| #[inline] |
| pub fn read_from<Fd: AsFd>(fd: Fd) -> io::Result<Self> { |
| Self::_read_from(fd.as_fd()) |
| } |
| |
| #[inline] |
| fn _read_from(fd: BorrowedFd<'_>) -> io::Result<Self> { |
| let flags = fcntl_getfl(fd)?; |
| let fd_for_dir = openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?; |
| |
| Ok(Self { |
| fd: fd_for_dir, |
| any_errors: false, |
| rewind: false, |
| buf: Vec::new(), |
| pos: 0, |
| }) |
| } |
| |
| /// `rewinddir(self)` |
| #[inline] |
| pub fn rewind(&mut self) { |
| self.any_errors = false; |
| self.rewind = true; |
| self.pos = self.buf.len(); |
| } |
| |
| /// `readdir(self)`, where `None` means the end of the directory. |
| pub fn read(&mut self) -> Option<io::Result<DirEntry>> { |
| // If we've seen errors, don't continue to try to read anything further. |
| if self.any_errors { |
| return None; |
| } |
| |
| // If a rewind was requested, seek to the beginning. |
| if self.rewind { |
| self.rewind = false; |
| match io::retry_on_intr(|| { |
| crate::backend::fs::syscalls::_seek(self.fd.as_fd(), 0, SEEK_SET) |
| }) { |
| Ok(_) => (), |
| Err(err) => { |
| self.any_errors = true; |
| return Some(Err(err)); |
| } |
| } |
| } |
| |
| // Compute linux_dirent64 field offsets. |
| let z = linux_dirent64 { |
| d_ino: 0_u64, |
| d_off: 0_i64, |
| d_type: 0_u8, |
| d_reclen: 0_u16, |
| d_name: Default::default(), |
| }; |
| let base = as_ptr(&z) as usize; |
| let offsetof_d_reclen = (as_ptr(&z.d_reclen) as usize) - base; |
| let offsetof_d_name = (as_ptr(&z.d_name) as usize) - base; |
| let offsetof_d_ino = (as_ptr(&z.d_ino) as usize) - base; |
| let offsetof_d_type = (as_ptr(&z.d_type) as usize) - base; |
| |
| // Test if we need more entries, and if so, read more. |
| if self.buf.len() - self.pos < size_of::<linux_dirent64>() { |
| match self.read_more()? { |
| Ok(()) => (), |
| Err(err) => return Some(Err(err)), |
| } |
| } |
| |
| // We successfully read an entry. Extract the fields. |
| let pos = self.pos; |
| |
| // Do an unaligned u16 load. |
| let d_reclen = u16::from_ne_bytes([ |
| self.buf[pos + offsetof_d_reclen], |
| self.buf[pos + offsetof_d_reclen + 1], |
| ]); |
| assert!(self.buf.len() - pos >= d_reclen as usize); |
| self.pos += d_reclen as usize; |
| |
| // Read the NUL-terminated name from the `d_name` field. Without |
| // `unsafe`, we need to scan for the NUL twice: once to obtain a size |
| // for the slice, and then once within `CStr::from_bytes_with_nul`. |
| let name_start = pos + offsetof_d_name; |
| let name_len = self.buf[name_start..] |
| .iter() |
| .position(|x| *x == b'\0') |
| .unwrap(); |
| let name = CStr::from_bytes_with_nul(&self.buf[name_start..][..=name_len]).unwrap(); |
| let name = name.to_owned(); |
| assert!(name.as_bytes().len() <= self.buf.len() - name_start); |
| |
| // Do an unaligned u64 load. |
| let d_ino = u64::from_ne_bytes([ |
| self.buf[pos + offsetof_d_ino], |
| self.buf[pos + offsetof_d_ino + 1], |
| self.buf[pos + offsetof_d_ino + 2], |
| self.buf[pos + offsetof_d_ino + 3], |
| self.buf[pos + offsetof_d_ino + 4], |
| self.buf[pos + offsetof_d_ino + 5], |
| self.buf[pos + offsetof_d_ino + 6], |
| self.buf[pos + offsetof_d_ino + 7], |
| ]); |
| |
| let d_type = self.buf[pos + offsetof_d_type]; |
| |
| // Check that our types correspond to the `linux_dirent64` types. |
| let _ = linux_dirent64 { |
| d_ino, |
| d_off: 0, |
| d_type, |
| d_reclen, |
| d_name: Default::default(), |
| }; |
| |
| Some(Ok(DirEntry { |
| d_ino, |
| d_type, |
| name, |
| })) |
| } |
| |
| #[must_use] |
| fn read_more(&mut self) -> Option<io::Result<()>> { |
| // The first few times we're called, we allocate a relatively small |
| // buffer, because many directories are small. If we're called more, |
| // use progressively larger allocations, up to a fixed maximum. |
| // |
| // The specific sizes and policy here have not been tuned in detail yet |
| // and may need to be adjusted. In doing so, we should be careful to |
| // avoid unbounded buffer growth. This buffer only exists to share the |
| // cost of a `getdents` call over many entries, so if it gets too big, |
| // cache and heap usage will outweigh the benefit. And ultimately, |
| // directories can contain more entries than we can allocate contiguous |
| // memory for, so we'll always need to cap the size at some point. |
| if self.buf.len() < 1024 * size_of::<linux_dirent64>() { |
| self.buf.reserve(32 * size_of::<linux_dirent64>()); |
| } |
| self.buf.resize(self.buf.capacity(), 0); |
| let nread = match io::retry_on_intr(|| { |
| crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) |
| }) { |
| Ok(nread) => nread, |
| Err(io::Errno::NOENT) => { |
| self.any_errors = true; |
| return None; |
| } |
| Err(err) => { |
| self.any_errors = true; |
| return Some(Err(err)); |
| } |
| }; |
| self.buf.resize(nread, 0); |
| self.pos = 0; |
| if nread == 0 { |
| None |
| } else { |
| Some(Ok(())) |
| } |
| } |
| |
| /// `fstat(self)` |
| #[inline] |
| pub fn stat(&self) -> io::Result<Stat> { |
| fstat(&self.fd) |
| } |
| |
| /// `fstatfs(self)` |
| #[inline] |
| pub fn statfs(&self) -> io::Result<StatFs> { |
| fstatfs(&self.fd) |
| } |
| |
| /// `fstatvfs(self)` |
| #[inline] |
| pub fn statvfs(&self) -> io::Result<StatVfs> { |
| fstatvfs(&self.fd) |
| } |
| |
| /// `fchdir(self)` |
| #[cfg(feature = "process")] |
| #[cfg_attr(doc_cfg, doc(cfg(feature = "process")))] |
| #[inline] |
| pub fn chdir(&self) -> io::Result<()> { |
| fchdir(&self.fd) |
| } |
| } |
| |
| impl Iterator for Dir { |
| type Item = io::Result<DirEntry>; |
| |
| #[inline] |
| fn next(&mut self) -> Option<Self::Item> { |
| Self::read(self) |
| } |
| } |
| |
| impl fmt::Debug for Dir { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.debug_struct("Dir").field("fd", &self.fd).finish() |
| } |
| } |
| |
| /// `struct dirent` |
| #[derive(Debug)] |
| pub struct DirEntry { |
| d_ino: u64, |
| d_type: u8, |
| name: CString, |
| } |
| |
| impl DirEntry { |
| /// Returns the file name of this directory entry. |
| #[inline] |
| pub fn file_name(&self) -> &CStr { |
| &self.name |
| } |
| |
| /// Returns the type of this directory entry. |
| #[inline] |
| pub fn file_type(&self) -> FileType { |
| FileType::from_dirent_d_type(self.d_type) |
| } |
| |
| /// Return the inode number of this directory entry. |
| #[inline] |
| pub fn ino(&self) -> u64 { |
| self.d_ino |
| } |
| } |
| |
| #[test] |
| fn dir_iterator_handles_io_errors() { |
| // create a dir, keep the FD, then delete the dir |
| let tmp = tempfile::tempdir().unwrap(); |
| let fd = crate::fs::openat( |
| crate::fs::CWD, |
| tmp.path(), |
| crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, |
| crate::fs::Mode::empty(), |
| ) |
| .unwrap(); |
| |
| let file_fd = crate::fs::openat( |
| &fd, |
| tmp.path().join("test.txt"), |
| crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, |
| crate::fs::Mode::RWXU, |
| ) |
| .unwrap(); |
| |
| let mut dir = Dir::read_from(&fd).unwrap(); |
| |
| // Reach inside the `Dir` and replace its directory with a file, which |
| // will cause the subsequent `getdents64` to fail. |
| crate::io::dup2(&file_fd, &mut dir.fd).unwrap(); |
| |
| assert!(matches!(dir.next(), Some(Err(_)))); |
| assert!(dir.next().is_none()); |
| } |