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 /// ```
44 /// # use std::mem::MaybeUninit;
45 /// # use rustix::fs::{CWD, Mode, OFlags, openat, RawDir};
46 /// # use rustix::cstr;
47 ///
48 /// let fd = openat(
49 /// CWD,
50 /// cstr!("."),
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 /// # use rustix::cstr;
70 ///
71 /// let fd = openat(
72 /// CWD,
73 /// cstr!("."),
74 /// OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
75 /// Mode::empty(),
76 /// )
77 /// .unwrap();
78 ///
79 /// let mut buf = [MaybeUninit::uninit(); 2048];
80 /// let mut iter = RawDir::new(fd, &mut buf);
81 /// while let Some(entry) = iter.next() {
82 /// let entry = entry.unwrap();
83 /// dbg!(&entry);
84 /// }
85 /// ```
86 ///
87 /// ## Portable
88 ///
89 /// Heap allocated growing buffer for supporting directory entries with
90 /// arbitrarily large file names:
91 ///
92 /// ```notrust
93 /// # // The `notrust` above can be removed when we can depend on Rust 1.65.
94 /// # use std::mem::MaybeUninit;
95 /// # use rustix::fs::{CWD, Mode, OFlags, openat, RawDir};
96 /// # use rustix::io::Errno;
97 /// # use rustix::cstr;
98 ///
99 /// let fd = openat(
100 /// CWD,
101 /// cstr!("."),
102 /// OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
103 /// Mode::empty(),
104 /// )
105 /// .unwrap();
106 ///
107 /// let mut buf = Vec::with_capacity(8192);
108 /// 'read: loop {
109 /// 'resize: {
110 /// let mut iter = RawDir::new(&fd, buf.spare_capacity_mut());
111 /// while let Some(entry) = iter.next() {
112 /// let entry = match entry {
113 /// Err(Errno::INVAL) => break 'resize,
114 /// r => r.unwrap(),
115 /// };
116 /// dbg!(&entry);
117 /// }
118 /// break 'read;
119 /// }
120 ///
121 /// let new_capacity = buf.capacity() * 2;
122 /// buf.reserve(new_capacity);
123 /// }
124 /// ```
125 pub fn new(fd: Fd, buf: &'buf mut [MaybeUninit<u8>]) -> Self {
126 Self {
127 fd,
128 buf: {
129 let offset = buf.as_ptr().align_offset(align_of::<linux_dirent64>());
130 if offset < buf.len() {
131 &mut buf[offset..]
132 } else {
133 &mut []
134 }
135 },
136 initialized: 0,
137 offset: 0,
138 }
139 }
140}
141
142/// A raw directory entry, similar to [`std::fs::DirEntry`].
143///
144/// Unlike the std version, this may represent the `.` or `..` entries.
145pub struct RawDirEntry<'a> {
146 file_name: &'a CStr,
147 file_type: u8,
148 inode_number: u64,
149 next_entry_cookie: i64,
150}
151
152impl<'a> fmt::Debug for RawDirEntry<'a> {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 let mut f: DebugStruct<'_, '_> = f.debug_struct(name:"RawDirEntry");
155 f.field(name:"file_name", &self.file_name());
156 f.field(name:"file_type", &self.file_type());
157 f.field(name:"ino", &self.ino());
158 f.field(name:"next_entry_cookie", &self.next_entry_cookie());
159 f.finish()
160 }
161}
162
163impl<'a> RawDirEntry<'a> {
164 /// Returns the file name of this directory entry.
165 #[inline]
166 pub fn file_name(&self) -> &CStr {
167 self.file_name
168 }
169
170 /// Returns the type of this directory entry.
171 #[inline]
172 pub fn file_type(&self) -> FileType {
173 FileType::from_dirent_d_type(self.file_type)
174 }
175
176 /// Returns the inode number of this directory entry.
177 #[inline]
178 #[doc(alias = "inode_number")]
179 pub fn ino(&self) -> u64 {
180 self.inode_number
181 }
182
183 /// Returns the seek cookie to the next directory entry.
184 #[inline]
185 #[doc(alias = "off")]
186 pub fn next_entry_cookie(&self) -> u64 {
187 self.next_entry_cookie as u64
188 }
189}
190
191impl<'buf, Fd: AsFd> RawDir<'buf, Fd> {
192 /// Identical to [`Iterator::next`] except that [`Iterator::Item`] borrows
193 /// from self.
194 ///
195 /// Note: this interface will be broken to implement a stdlib iterator API
196 /// with GAT support once one becomes available.
197 #[allow(unsafe_code)]
198 #[allow(clippy::should_implement_trait)]
199 pub fn next(&mut self) -> Option<io::Result<RawDirEntry<'_>>> {
200 if self.is_buffer_empty() {
201 match getdents_uninit(self.fd.as_fd(), self.buf) {
202 Ok(0) => return None,
203 Ok(bytes_read) => {
204 self.initialized = bytes_read;
205 self.offset = 0;
206 }
207 Err(e) => return Some(Err(e)),
208 }
209 }
210
211 let dirent_ptr = self.buf[self.offset..].as_ptr();
212 // SAFETY:
213 // - This data is initialized by the check above.
214 // - Assumption: the kernel will not give us partial structs.
215 // - Assumption: the kernel uses proper alignment between structs.
216 // - The starting pointer is aligned (performed in RawDir::new)
217 let dirent = unsafe { &*dirent_ptr.cast::<linux_dirent64>() };
218
219 self.offset += usize::from(dirent.d_reclen);
220
221 Some(Ok(RawDirEntry {
222 file_type: dirent.d_type,
223 inode_number: dirent.d_ino.into(),
224 next_entry_cookie: dirent.d_off.into(),
225 // SAFETY: The kernel guarantees a NUL-terminated string.
226 file_name: unsafe { CStr::from_ptr(dirent.d_name.as_ptr().cast()) },
227 }))
228 }
229
230 /// Returns true if the internal buffer is empty and will be refilled when
231 /// calling [`next`].
232 ///
233 /// [`next`]: Self::next
234 pub fn is_buffer_empty(&self) -> bool {
235 self.offset >= self.initialized
236 }
237}
238