rustix/fs/
raw_dir.rs

1//! `RawDir` and `RawDirEntry`.
2
3use crate::backend::fs::syscalls::getdents_uninit;
4use crate::fd::AsFd;
5use crate::ffi::CStr;
6use crate::fs::FileType;
7use crate::io;
8use core::fmt;
9use core::mem::{align_of, MaybeUninit};
10
11#[cfg(not(linux_raw_dep))]
12use libc::dirent64 as linux_dirent64;
13#[cfg(linux_raw_dep)]
14use linux_raw_sys::general::linux_dirent64;
15
16/// A directory iterator implemented with getdents.
17///
18/// Note: This implementation does not handle growing the buffer. If this
19/// functionality is necessary, you'll need to drop the current iterator,
20/// resize the buffer, and then re-create the iterator. The iterator is
21/// guaranteed to continue where it left off provided the file descriptor isn't
22/// changed. See the example in [`RawDir::new`].
23pub struct RawDir<'buf, Fd: AsFd> {
24    fd: Fd,
25    buf: &'buf mut [MaybeUninit<u8>],
26    initialized: usize,
27    offset: usize,
28}
29
30impl<'buf, Fd: AsFd> RawDir<'buf, Fd> {
31    /// Create a new iterator from the given file descriptor and buffer.
32    ///
33    /// Note: the buffer size may be trimmed to accommodate alignment
34    /// requirements.
35    ///
36    /// # Examples
37    ///
38    /// ## Simple but non-portable
39    ///
40    /// These examples are non-portable, because file systems may not have a
41    /// maximum file name length. If you can make assumptions that bound
42    /// this length, then these examples may suffice.
43    ///
44    /// Using the heap:
45    ///
46    /// ```
47    /// # use std::mem::MaybeUninit;
48    /// # use rustix::fs::{CWD, Mode, OFlags, openat, RawDir};
49    /// # use rustix::cstr;
50    ///
51    /// let fd = openat(
52    ///     CWD,
53    ///     cstr!("."),
54    ///     OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
55    ///     Mode::empty(),
56    /// )
57    /// .unwrap();
58    ///
59    /// let mut buf = Vec::with_capacity(8192);
60    /// let mut iter = RawDir::new(fd, buf.spare_capacity_mut());
61    /// while let Some(entry) = iter.next() {
62    ///     let entry = entry.unwrap();
63    ///     dbg!(&entry);
64    /// }
65    /// ```
66    ///
67    /// Using the stack:
68    ///
69    /// ```
70    /// # use std::mem::MaybeUninit;
71    /// # use rustix::fs::{CWD, Mode, OFlags, openat, RawDir};
72    /// # use rustix::cstr;
73    ///
74    /// let fd = openat(
75    ///     CWD,
76    ///     cstr!("."),
77    ///     OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
78    ///     Mode::empty(),
79    /// )
80    /// .unwrap();
81    ///
82    /// let mut buf = [MaybeUninit::uninit(); 2048];
83    /// let mut iter = RawDir::new(fd, &mut buf);
84    /// while let Some(entry) = iter.next() {
85    ///     let entry = entry.unwrap();
86    ///     dbg!(&entry);
87    /// }
88    /// ```
89    ///
90    /// ## Portable
91    ///
92    /// Heap allocated growing buffer for supporting directory entries with
93    /// arbitrarily large file names:
94    ///
95    /// ```ignore
96    /// # // The `ignore` above can be removed when we can depend on Rust 1.65.
97    /// # use std::mem::MaybeUninit;
98    /// # use rustix::fs::{CWD, Mode, OFlags, openat, RawDir};
99    /// # use rustix::io::Errno;
100    /// # use rustix::cstr;
101    ///
102    /// let fd = openat(
103    ///     CWD,
104    ///     cstr!("."),
105    ///     OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
106    ///     Mode::empty(),
107    /// )
108    /// .unwrap();
109    ///
110    /// let mut buf = Vec::with_capacity(8192);
111    /// 'read: loop {
112    ///     'resize: {
113    ///         let mut iter = RawDir::new(&fd, buf.spare_capacity_mut());
114    ///         while let Some(entry) = iter.next() {
115    ///             let entry = match entry {
116    ///                 Err(Errno::INVAL) => break 'resize,
117    ///                 r => r.unwrap(),
118    ///             };
119    ///             dbg!(&entry);
120    ///         }
121    ///         break 'read;
122    ///     }
123    ///
124    ///     let new_capacity = buf.capacity() * 2;
125    ///     buf.reserve(new_capacity);
126    /// }
127    /// ```
128    pub fn new(fd: Fd, buf: &'buf mut [MaybeUninit<u8>]) -> Self {
129        Self {
130            fd,
131            buf: {
132                let offset = buf.as_ptr().align_offset(align_of::<linux_dirent64>());
133                if offset < buf.len() {
134                    &mut buf[offset..]
135                } else {
136                    &mut []
137                }
138            },
139            initialized: 0,
140            offset: 0,
141        }
142    }
143}
144
145/// A raw directory entry, similar to [`std::fs::DirEntry`].
146///
147/// Unlike the std version, this may represent the `.` or `..` entries.
148pub struct RawDirEntry<'a> {
149    file_name: &'a CStr,
150    file_type: u8,
151    inode_number: u64,
152    next_entry_cookie: i64,
153}
154
155impl<'a> fmt::Debug for RawDirEntry<'a> {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        let mut f = f.debug_struct("RawDirEntry");
158        f.field("file_name", &self.file_name());
159        f.field("file_type", &self.file_type());
160        f.field("ino", &self.ino());
161        f.field("next_entry_cookie", &self.next_entry_cookie());
162        f.finish()
163    }
164}
165
166impl<'a> RawDirEntry<'a> {
167    /// Returns the file name of this directory entry.
168    #[inline]
169    pub fn file_name(&self) -> &CStr {
170        self.file_name
171    }
172
173    /// Returns the type of this directory entry.
174    #[inline]
175    pub fn file_type(&self) -> FileType {
176        FileType::from_dirent_d_type(self.file_type)
177    }
178
179    /// Returns the inode number of this directory entry.
180    #[inline]
181    #[doc(alias = "inode_number")]
182    pub fn ino(&self) -> u64 {
183        self.inode_number
184    }
185
186    /// Returns the seek cookie to the next directory entry.
187    #[inline]
188    #[doc(alias = "off")]
189    pub fn next_entry_cookie(&self) -> u64 {
190        self.next_entry_cookie as u64
191    }
192}
193
194impl<'buf, Fd: AsFd> RawDir<'buf, Fd> {
195    /// Identical to [`Iterator::next`] except that [`Iterator::Item`] borrows
196    /// from self.
197    ///
198    /// Note: this interface will be broken to implement a stdlib iterator API
199    /// with GAT support once one becomes available.
200    #[allow(unsafe_code)]
201    #[allow(clippy::should_implement_trait)]
202    pub fn next(&mut self) -> Option<io::Result<RawDirEntry<'_>>> {
203        if self.is_buffer_empty() {
204            match getdents_uninit(self.fd.as_fd(), self.buf) {
205                Ok(0) => return None,
206                Ok(bytes_read) => {
207                    self.initialized = bytes_read;
208                    self.offset = 0;
209                }
210                Err(e) => return Some(Err(e)),
211            }
212        }
213
214        let dirent_ptr = self.buf[self.offset..].as_ptr();
215        // SAFETY:
216        // - This data is initialized by the check above.
217        //   - Assumption: the kernel will not give us partial structs.
218        // - Assumption: the kernel uses proper alignment between structs.
219        // - The starting pointer is aligned (performed in `RawDir::new`).
220        let dirent = unsafe { &*dirent_ptr.cast::<linux_dirent64>() };
221
222        self.offset += usize::from(dirent.d_reclen);
223
224        Some(Ok(RawDirEntry {
225            file_type: dirent.d_type,
226            inode_number: dirent.d_ino.into(),
227            next_entry_cookie: dirent.d_off.into(),
228            // SAFETY: The kernel guarantees a NUL-terminated string.
229            file_name: unsafe { CStr::from_ptr(dirent.d_name.as_ptr().cast()) },
230        }))
231    }
232
233    /// Returns true if the internal buffer is empty and will be refilled when
234    /// calling [`next`].
235    ///
236    /// [`next`]: Self::next
237    pub fn is_buffer_empty(&self) -> bool {
238        self.offset >= self.initialized
239    }
240}