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