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