windows_namespace_request_sys/query.rs
1// Copyright (c) Mike Grier.
2
3//! The `GetFileInformationByHandleEx` entry.
4//!
5//! Entry 5 of the audited catalogue, and the most-called namespace operation
6//! across all three audited consumers.
7//!
8//! # Why it is here despite being trivial to marshal
9//!
10//! This entry needs almost no marshaling work: its inputs are a handle, a
11//! scalar class, and a buffer size, with no pointer into caller memory
12//! anywhere. That invites the conclusion that it does not belong in a catalogue
13//! at all. Membership is decided by whether a **blocking** namespace call needs
14//! performing off the caller's thread, not by whether it is awkward to marshal
15//! -- the latter test would select for our implementation convenience rather
16//! than for consumer need. On the former test this is the call whose lack of an
17//! overlapped form is why an unassociated handle had to become a first-class
18//! destination at all.
19//!
20//! # This returns bytes, and does not parse them
21//!
22//! The audited classes have two result shapes -- fixed-size out-parameters and
23//! variable-length batches -- and both collapse to one owned buffer here,
24//! because this crate returns bytes plus the unaltered outcome. Per-class
25//! parsing stays with the consumer that already owns it.
26//!
27//! Two constraints on that buffer are not negotiable:
28//!
29//! - It must be **8-byte aligned**. A `Vec<u8>` guarantees byte alignment and
30//! nothing more, and a misaligned batch is not a subtle problem: the very
31//! first query fails with `ERROR_NOACCESS`. [`AlignedBuf`] states the
32//! alignment rather than arriving at it by luck.
33//! - The call **reports no written length**. A batch is walked by its own
34//! next-entry offsets, so the whole buffer comes back and the consumer bounds
35//! its own reads, rather than this entry inventing a byte count it cannot
36//! know.
37//!
38//! # Only two classes touch the enumeration cursor
39//!
40//! Measured on Windows 11 Enterprise 10.0.28000, `aarch64-pc-windows-msvc`,
41//! against a real directory with a deliberately small buffer:
42//!
43//! | Question | Measured |
44//! |---|---|
45//! | Does a duplicated handle share the enumeration cursor? | **Yes** -- a clean continuation |
46//! | Control: do two separate opens share it? | **No** -- the second restarts |
47//! | Does closing the duplicate disturb the source? | **No** |
48//! | Does an interleaved `FileBasicInfo`, `FileIdInfo`, or non-`Ex` query disturb it? | **No** |
49//!
50//! So the contract is narrower than "handle-taking entries are hazardous":
51//! **only the two directory-enumeration classes mutate the shared cursor**, and
52//! every other query is a pure read that composes freely with an enumeration in
53//! progress, on the same handle or on a duplicate. What follows is specific --
54//! a duplicate is *not* an independent enumeration, and an independent
55//! traversal needs a fresh open.
56//!
57//! # This is single-shot, and is not a streaming engine
58//!
59//! An entry covering the two directory classes otherwise looks like a second
60//! implementation of a shipped streaming enumerator. It is not. This entry is
61//! **single-shot**: one call, one batch, and the *client* sequences the next,
62//! which is the one-entry-per-Win32-call rule applied literally. A consumer
63//! wanting streaming enumeration -- with the cursor, refill loop, quanta, and
64//! backpressure owned for it -- wants
65//! [windows-file-enumeration-sys](https://docs.rs/windows-file-enumeration-sys)
66//! and should not rebuild that loop out of single-shot calls.
67//!
68//! All five audited classes stay reachable here regardless, because restricting
69//! them would narrow the entry for a no-consumer reason.
70//!
71//! # No ambient context is needed
72//!
73//! Access was checked at the open. That is exactly why the enumeration crate
74//! applies impersonation only around `CreateFileW`, and it makes this the
75//! clearest case that a request and a context are **paired at submission**
76//! rather than fused.
77
78use windows_sys::Win32::Storage::FileSystem::{
79 FILE_INFO_BY_HANDLE_CLASS, FileBasicInfo, FileCaseSensitiveInfo, FileIdExtdDirectoryInfo,
80 FileIdExtdDirectoryRestartInfo, FileIdInfo, GetFileInformationByHandleEx,
81};
82
83use io_buffers::AlignedBuf;
84
85use crate::handle::{CapturedHandle, HandleCaptureError};
86use crate::outcome::{Outcome, out_of_memory, perform_bool};
87
88/// The alignment a directory-information batch requires.
89///
90/// A `FILE_ID_EXTD_DIR_INFO` contains `i64` fields and the API keeps every
91/// record in a batch on an 8-byte boundary -- but only if the batch itself
92/// starts on one. Changing this value is a breaking change.
93const BATCH_ALIGNMENT: usize = align_of::<u64>();
94
95// `AlignedBuf` refuses an alignment that is not a power of two, and `perform` turns every refusal
96// into an out-of-memory report, so this is checked where it cannot be mistaken for one.
97const _: () = assert!(BATCH_ALIGNMENT.is_power_of_two());
98
99/// Which information the query asks for.
100///
101/// A newtype over `FILE_INFO_BY_HANDLE_CLASS` rather than an enum, because
102/// Windows defines classes this crate has never heard of and refusing them
103/// would narrow the entry. The named constants are the five the audit found;
104/// any other class reaches Windows unaltered through [`from_raw`](Self::from_raw).
105///
106/// # Example
107///
108/// ```
109/// use windows_namespace_request_sys::query::FileInformationClass;
110/// use windows_sys::Win32::Storage::FileSystem::{FileBasicInfo, FileStandardInfo};
111///
112/// assert_eq!(FileInformationClass::BASIC.as_raw(), FileBasicInfo);
113///
114/// // A class with no named constant here is still expressible.
115/// let standard = FileInformationClass::from_raw(FileStandardInfo);
116/// assert_eq!(standard.as_raw(), FileStandardInfo);
117/// ```
118#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
119pub struct FileInformationClass(FILE_INFO_BY_HANDLE_CLASS);
120
121impl FileInformationClass {
122 /// `FileBasicInfo`: timestamps and attributes. A pure read.
123 pub const BASIC: Self = Self(FileBasicInfo);
124 /// `FileIdInfo`: the volume serial and 128-bit file id. A pure read.
125 pub const ID: Self = Self(FileIdInfo);
126 /// `FileCaseSensitiveInfo`: whether the directory is case-sensitive. A pure
127 /// read.
128 pub const CASE_SENSITIVE: Self = Self(FileCaseSensitiveInfo);
129 /// `FileIdExtdDirectoryInfo`: the next batch of directory entries.
130 ///
131 /// **Advances the enumeration cursor**, which lives in the file object and
132 /// is therefore shared with every duplicate of the handle.
133 pub const ID_EXTD_DIRECTORY: Self = Self(FileIdExtdDirectoryInfo);
134 /// `FileIdExtdDirectoryRestartInfo`: the first batch, restarting the
135 /// enumeration.
136 ///
137 /// **Resets the enumeration cursor**, with the same sharing consequence.
138 pub const ID_EXTD_DIRECTORY_RESTART: Self = Self(FileIdExtdDirectoryRestartInfo);
139
140 /// Wraps a raw `FILE_INFO_BY_HANDLE_CLASS`.
141 #[must_use]
142 pub const fn from_raw(class: FILE_INFO_BY_HANDLE_CLASS) -> Self {
143 Self(class)
144 }
145
146 /// The raw class value.
147 #[must_use]
148 pub const fn as_raw(self) -> FILE_INFO_BY_HANDLE_CLASS {
149 self.0
150 }
151
152 /// Whether this class mutates the handle's shared enumeration cursor.
153 ///
154 /// True only for the two directory-enumeration classes. Every other class
155 /// is a pure read that composes freely with an enumeration in progress,
156 /// measured rather than assumed.
157 ///
158 /// An unrecognised class reports `false`, because this answers "is this one
159 /// of the two known cursor-moving classes", not "is this provably safe".
160 ///
161 /// # Example
162 ///
163 /// ```
164 /// use windows_namespace_request_sys::query::FileInformationClass;
165 ///
166 /// assert!(FileInformationClass::ID_EXTD_DIRECTORY.moves_enumeration_cursor());
167 /// assert!(FileInformationClass::ID_EXTD_DIRECTORY_RESTART.moves_enumeration_cursor());
168 ///
169 /// // Interleaving one of these with an enumeration is safe: measured, not
170 /// // reasoned from the object model.
171 /// assert!(!FileInformationClass::BASIC.moves_enumeration_cursor());
172 /// assert!(!FileInformationClass::ID.moves_enumeration_cursor());
173 /// ```
174 #[must_use]
175 pub const fn moves_enumeration_cursor(self) -> bool {
176 self.0 == FileIdExtdDirectoryInfo || self.0 == FileIdExtdDirectoryRestartInfo
177 }
178}
179
180/// An owned, marshalable parameter set for `GetFileInformationByHandleEx`.
181///
182/// # Example
183///
184/// ```
185/// use std::fs;
186/// use std::os::windows::io::AsHandle;
187///
188/// use windows_namespace_request_sys::query::{FileInformationClass, QueryFileInformation};
189/// use windows_namespace_request_sys::CapturedHandle;
190///
191/// let path = std::env::temp_dir().join(format!("wnrs-q-{}.tmp", std::process::id()));
192/// fs::write(&path, b"example")?;
193/// let file = fs::File::open(&path)?;
194///
195/// let request = QueryFileInformation::new(
196/// CapturedHandle::capture(file.as_handle())?,
197/// FileInformationClass::BASIC,
198/// )
199/// .with_capacity(256);
200///
201/// // The whole buffer comes back: the call reports no written length, so the
202/// // consumer bounds its own reads rather than trusting a count we invented.
203/// let bytes = request.perform()?;
204/// assert_eq!(bytes.len(), 256);
205/// assert_eq!(bytes.as_ptr() as usize % 8, 0);
206/// # drop(file);
207/// # fs::remove_file(&path)?;
208/// # Ok::<(), Box<dyn std::error::Error>>(())
209/// ```
210#[derive(Debug)]
211#[must_use = "an unperformed request queries nothing"]
212pub struct QueryFileInformation {
213 handle: CapturedHandle,
214 class: FileInformationClass,
215 capacity: usize,
216}
217
218impl QueryFileInformation {
219 /// The capacity a request starts with.
220 ///
221 /// Large enough for any of the fixed-size classes and for a useful batch of
222 /// directory entries. A caller enumerating a large directory will want more
223 /// and should say so; this is a starting point, not a recommendation.
224 pub const DEFAULT_CAPACITY: usize = 64 * 1024;
225
226 /// Begins a request against `handle` for `class`.
227 pub fn new(handle: CapturedHandle, class: FileInformationClass) -> Self {
228 Self {
229 handle,
230 class,
231 capacity: Self::DEFAULT_CAPACITY,
232 }
233 }
234
235 /// Sets the buffer size the query is given.
236 ///
237 /// For a fixed-size class this must be at least the size of that class's
238 /// structure, and Windows reports `ERROR_BAD_LENGTH` if it is not. For a
239 /// directory class it bounds how many entries one call can return, and a
240 /// buffer too small for even one entry fails with `ERROR_MORE_DATA` --
241 /// neither is pre-empted here.
242 pub fn with_capacity(mut self, capacity: usize) -> Self {
243 self.capacity = capacity;
244 self
245 }
246
247 /// The owned duplicate of the handle being queried.
248 pub fn handle(&self) -> &CapturedHandle {
249 &self.handle
250 }
251
252 /// The information class this request asks for.
253 #[must_use]
254 pub fn class(&self) -> FileInformationClass {
255 self.class
256 }
257
258 /// The buffer size the query will be given.
259 #[must_use]
260 pub fn capacity(&self) -> usize {
261 self.capacity
262 }
263
264 /// Copies the request, duplicating the handle.
265 ///
266 /// Not `Clone`, because duplicating a handle is fallible. Note that the
267 /// copy shares the original's enumeration cursor, per this module's
268 /// measurements -- it is a second reference, not a second enumeration.
269 ///
270 /// # Errors
271 ///
272 /// Returns the handle-capture failure when the handle cannot be duplicated.
273 pub fn try_clone(&self) -> Result<Self, HandleCaptureError> {
274 Ok(Self {
275 handle: self.handle.try_clone()?,
276 class: self.class,
277 capacity: self.capacity,
278 })
279 }
280
281 /// Performs the query on the calling thread.
282 ///
283 /// Returns the **whole** buffer, 8-byte aligned. The call reports no
284 /// written length -- a batch is walked by its own next-entry offsets -- so
285 /// nothing here invents a byte count it cannot know.
286 ///
287 /// # Errors
288 ///
289 /// Returns the raw Win32 code, unaltered. `ERROR_NO_MORE_FILES` ends a
290 /// directory enumeration and `ERROR_MORE_DATA` means the buffer held no
291 /// complete entry; both are the caller's to interpret.
292 ///
293 /// The one code the call did not produce is `ERROR_NOT_ENOUGH_MEMORY`, returned when the
294 /// buffer for the capacity this query was built with cannot be allocated -- too large for
295 /// the machine, or refused by the allocator. The call was not made, and a smaller capacity
296 /// can be tried.
297 pub fn perform(&self) -> Outcome<AlignedBuf> {
298 let mut buffer =
299 AlignedBuf::zeroed(self.capacity, BATCH_ALIGNMENT).map_err(|_| out_of_memory())?;
300 let size = u32::try_from(self.capacity).unwrap_or(u32::MAX);
301
302 perform_bool(|| {
303 // SAFETY: the handle is a duplicate this request owns and keeps
304 // open across the call; the buffer is writable for `size` bytes and
305 // 8-byte aligned, which is what the directory classes require.
306 unsafe {
307 GetFileInformationByHandleEx(
308 self.handle.raw(),
309 self.class.as_raw(),
310 buffer.as_mut_ptr().cast(),
311 size,
312 )
313 }
314 })?;
315
316 Ok(buffer)
317 }
318}
319
320impl crate::request::Request for QueryFileInformation {
321 type Error = crate::Win32Error;
322 type Output = AlignedBuf;
323
324 fn perform(&self) -> Outcome<AlignedBuf> {
325 Self::perform(self)
326 }
327}
328
329#[cfg(test)]
330mod tests;