Skip to main content

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 request shares the caller's handle, and even a duplicate is *not* an
55//! independent enumeration, so an independent 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 std::fmt;
79
80use windows_sys::Win32::Storage::FileSystem::{
81    FILE_INFO_BY_HANDLE_CLASS, FileBasicInfo, FileCaseSensitiveInfo, FileFullDirectoryInfo,
82    FileFullDirectoryRestartInfo, FileIdBothDirectoryInfo, FileIdBothDirectoryRestartInfo,
83    FileIdExtdDirectoryInfo, FileIdExtdDirectoryRestartInfo, FileIdInfo, FileStreamInfo,
84    GetFileInformationByHandleEx,
85};
86
87use io_buffers::AlignedBuf;
88
89use win_shared_os_owned_handle::SharedHandle;
90
91use crate::Win32Error;
92use crate::handle::SharedRaw;
93use crate::outcome::{Outcome, out_of_memory, perform_bool};
94
95/// The alignment a directory-information batch requires.
96///
97/// A `FILE_ID_EXTD_DIR_INFO` contains `i64` fields and the API keeps every
98/// record in a batch on an 8-byte boundary -- but only if the batch itself
99/// starts on one. Changing this value is a breaking change.
100const BATCH_ALIGNMENT: usize = align_of::<u64>();
101
102// `AlignedBuf` refuses an alignment that is not a power of two, and `perform` turns every refusal
103// into an out-of-memory report, so this is checked where it cannot be mistaken for one.
104const _: () = assert!(BATCH_ALIGNMENT.is_power_of_two());
105
106/// The classes whose `lpFileInformation` structure's own page requires an alignment: the class,
107/// the structure, and the alignment in bytes.
108///
109/// The `GetFileInformationByHandleEx` page (`ms.date` 2018-12-05, read 2026-10-20) gives, for each
110/// class, "the corresponding data structure types for use with this function" as the
111/// `lpFileInformation` type. Of those structures' pages, read the same day, three say the
112/// structure "must be aligned on a" "(8-byte) boundary": `FILE_STREAM_INFO`,
113/// `FILE_ID_BOTH_DIR_INFO` and `FILE_FULL_DIR_INFO`, each `ms.date` 2018-12-05. No other structure
114/// the page lists says so. Changing an entry is a breaking change.
115const REQUIRED_ALIGNMENT: [(FILE_INFO_BY_HANDLE_CLASS, &str, usize); 5] = [
116    (FileStreamInfo, "FILE_STREAM_INFO", 8),
117    (FileIdBothDirectoryInfo, "FILE_ID_BOTH_DIR_INFO", 8),
118    (FileIdBothDirectoryRestartInfo, "FILE_ID_BOTH_DIR_INFO", 8),
119    (FileFullDirectoryInfo, "FILE_FULL_DIR_INFO", 8),
120    (FileFullDirectoryRestartInfo, "FILE_FULL_DIR_INFO", 8),
121];
122
123// `perform` allocates at `BATCH_ALIGNMENT`, so it can never be refused by the check below.
124const _: () = {
125    let mut i = 0;
126    while i < REQUIRED_ALIGNMENT.len() {
127        assert!(REQUIRED_ALIGNMENT[i].2 <= BATCH_ALIGNMENT);
128        i += 1;
129    }
130};
131
132/// The structure `class` writes and the alignment its page requires, when its page requires one.
133fn required_alignment(class: FileInformationClass) -> Option<(&'static str, usize)> {
134    REQUIRED_ALIGNMENT
135        .iter()
136        .find(|(raw, _, _)| *raw == class.as_raw())
137        .map(|&(_, structure, required)| (structure, required))
138}
139
140/// Refuses a buffer at `address` for `class` when the page of the structure `class` writes requires
141/// an alignment the address does not have. Decided on the arguments alone, before any call.
142fn check_alignment(class: FileInformationClass, address: usize) -> Result<(), PerformIntoError> {
143    match required_alignment(class) {
144        Some((structure, required)) if !address.is_multiple_of(required) => {
145            Err(PerformIntoError::Misaligned {
146                class,
147                structure,
148                required,
149                address,
150            })
151        }
152        _ => Ok(()),
153    }
154}
155
156/// Why [`QueryFileInformation::perform_into`] did not fill the caller's buffer.
157#[derive(Debug)]
158#[non_exhaustive]
159pub enum PerformIntoError {
160    /// Windows refused the call, with the raw code unaltered.
161    Win32(Win32Error),
162    /// Refused before the call, which was not made: the page of the structure the class writes
163    /// says it "must be aligned on" a boundary the buffer's address is not on. It carries no OS
164    /// code, because Windows reported nothing.
165    Misaligned {
166        /// The class asked for.
167        class: FileInformationClass,
168        /// The structure the `GetFileInformationByHandleEx` page gives as that class's
169        /// `lpFileInformation` type.
170        structure: &'static str,
171        /// The alignment, in bytes, that structure's page requires.
172        required: usize,
173        /// The buffer's address.
174        address: usize,
175    },
176}
177
178impl PerformIntoError {
179    /// The Win32 code, when Windows produced one; `None` for a refusal made before the call.
180    #[must_use]
181    pub fn code(&self) -> Option<u32> {
182        match self {
183            Self::Win32(error) => Some(error.code()),
184            Self::Misaligned { .. } => None,
185        }
186    }
187}
188
189impl fmt::Display for PerformIntoError {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        match self {
192            Self::Win32(error) => write!(f, "GetFileInformationByHandleEx: {error}"),
193            Self::Misaligned {
194                structure,
195                required,
196                address,
197                ..
198            } => write!(
199                f,
200                "GetFileInformationByHandleEx not called: {structure} must be aligned on a \
201                 {required}-byte boundary, and the buffer is at {address:#x}"
202            ),
203        }
204    }
205}
206
207impl std::error::Error for PerformIntoError {
208    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
209        match self {
210            Self::Win32(error) => Some(error),
211            Self::Misaligned { .. } => None,
212        }
213    }
214}
215
216impl From<Win32Error> for PerformIntoError {
217    fn from(error: Win32Error) -> Self {
218        Self::Win32(error)
219    }
220}
221
222/// Which information the query asks for.
223///
224/// A newtype over `FILE_INFO_BY_HANDLE_CLASS` rather than an enum, because
225/// Windows defines classes this crate has never heard of and refusing them
226/// would narrow the entry. The named constants are the five the audit found;
227/// any other class reaches Windows unaltered through [`from_raw`](Self::from_raw).
228///
229/// # Example
230///
231/// ```
232/// use windows_namespace_request_sys::query::FileInformationClass;
233/// use windows_sys::Win32::Storage::FileSystem::{FileBasicInfo, FileStandardInfo};
234///
235/// assert_eq!(FileInformationClass::BASIC.as_raw(), FileBasicInfo);
236///
237/// // A class with no named constant here is still expressible.
238/// let standard = FileInformationClass::from_raw(FileStandardInfo);
239/// assert_eq!(standard.as_raw(), FileStandardInfo);
240/// ```
241#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
242pub struct FileInformationClass(FILE_INFO_BY_HANDLE_CLASS);
243
244impl FileInformationClass {
245    /// `FileBasicInfo`: timestamps and attributes. A pure read.
246    /// [`QueryFileBasicInfo`](crate::file_basic::QueryFileBasicInfo) reads it as values.
247    pub const BASIC: Self = Self(FileBasicInfo);
248    /// `FileIdInfo`: the volume serial and 128-bit file id. A pure read.
249    /// [`QueryFileIdInfo`](crate::file_id::QueryFileIdInfo) reads it as values.
250    pub const ID: Self = Self(FileIdInfo);
251    /// `FileCaseSensitiveInfo`: whether the directory is case-sensitive. A pure
252    /// read.
253    pub const CASE_SENSITIVE: Self = Self(FileCaseSensitiveInfo);
254    /// `FileIdExtdDirectoryInfo`: the next batch of directory entries.
255    ///
256    /// **Advances the enumeration cursor**, which lives in the file object and
257    /// is therefore shared with every duplicate of the handle.
258    pub const ID_EXTD_DIRECTORY: Self = Self(FileIdExtdDirectoryInfo);
259    /// `FileIdExtdDirectoryRestartInfo`: the first batch, restarting the
260    /// enumeration.
261    ///
262    /// **Resets the enumeration cursor**, with the same sharing consequence.
263    pub const ID_EXTD_DIRECTORY_RESTART: Self = Self(FileIdExtdDirectoryRestartInfo);
264
265    /// Wraps a raw `FILE_INFO_BY_HANDLE_CLASS`.
266    #[must_use]
267    pub const fn from_raw(class: FILE_INFO_BY_HANDLE_CLASS) -> Self {
268        Self(class)
269    }
270
271    /// The raw class value.
272    #[must_use]
273    pub const fn as_raw(self) -> FILE_INFO_BY_HANDLE_CLASS {
274        self.0
275    }
276
277    /// Whether this class mutates the handle's shared enumeration cursor.
278    ///
279    /// True only for the two directory-enumeration classes. Every other class
280    /// is a pure read that composes freely with an enumeration in progress,
281    /// measured rather than assumed.
282    ///
283    /// An unrecognised class reports `false`, because this answers "is this one
284    /// of the two known cursor-moving classes", not "is this provably safe".
285    ///
286    /// # Example
287    ///
288    /// ```
289    /// use windows_namespace_request_sys::query::FileInformationClass;
290    ///
291    /// assert!(FileInformationClass::ID_EXTD_DIRECTORY.moves_enumeration_cursor());
292    /// assert!(FileInformationClass::ID_EXTD_DIRECTORY_RESTART.moves_enumeration_cursor());
293    ///
294    /// // Interleaving one of these with an enumeration is safe: measured, not
295    /// // reasoned from the object model.
296    /// assert!(!FileInformationClass::BASIC.moves_enumeration_cursor());
297    /// assert!(!FileInformationClass::ID.moves_enumeration_cursor());
298    /// ```
299    #[must_use]
300    pub const fn moves_enumeration_cursor(self) -> bool {
301        self.0 == FileIdExtdDirectoryInfo || self.0 == FileIdExtdDirectoryRestartInfo
302    }
303}
304
305/// An owned, marshalable parameter set for `GetFileInformationByHandleEx`.
306///
307/// # Example
308///
309/// ```
310/// use std::fs;
311///
312/// use windows_namespace_request_sys::query::{FileInformationClass, QueryFileInformation};
313///
314/// let path = std::env::temp_dir().join(format!("wnrs-q-{}.tmp", std::process::id()));
315/// fs::write(&path, b"example")?;
316/// let file = fs::File::open(&path)?;
317///
318/// let request = QueryFileInformation::new(file, FileInformationClass::BASIC).with_capacity(256);
319///
320/// // The whole buffer comes back: the call reports no written length, so the
321/// // consumer bounds its own reads rather than trusting a count we invented.
322/// let bytes = request.perform()?;
323/// assert_eq!(bytes.len(), 256);
324/// assert_eq!(bytes.as_ptr() as usize % 8, 0);
325/// # fs::remove_file(&path)?;
326/// # Ok::<(), Box<dyn std::error::Error>>(())
327/// ```
328#[derive(Clone, Debug)]
329#[must_use = "an unperformed request queries nothing"]
330pub struct QueryFileInformation {
331    handle: SharedHandle,
332    class: FileInformationClass,
333    capacity: usize,
334}
335
336impl QueryFileInformation {
337    /// The capacity a request starts with.
338    ///
339    /// Large enough for any of the fixed-size classes and for a useful batch of
340    /// directory entries. A caller enumerating a large directory will want more
341    /// and should say so; this is a starting point, not a recommendation.
342    pub const DEFAULT_CAPACITY: usize = 64 * 1024;
343
344    /// Begins a request against `handle` for `class`, holding `handle` as one more holder of it.
345    pub fn new(handle: impl Into<SharedHandle>, class: FileInformationClass) -> Self {
346        Self {
347            handle: handle.into(),
348            class,
349            capacity: Self::DEFAULT_CAPACITY,
350        }
351    }
352
353    /// Sets the buffer size the query is given.
354    ///
355    /// For a fixed-size class this must be at least the size of that class's
356    /// structure, and Windows reports `ERROR_BAD_LENGTH` if it is not. For a
357    /// directory class it bounds how many entries one call can return, and a
358    /// buffer too small for even one entry fails with `ERROR_MORE_DATA` --
359    /// neither is pre-empted here.
360    pub fn with_capacity(mut self, capacity: usize) -> Self {
361        self.capacity = capacity;
362        self
363    }
364
365    /// The handle being queried, shared with its other holders.
366    pub fn handle(&self) -> &SharedHandle {
367        &self.handle
368    }
369
370    /// The information class this request asks for.
371    #[must_use]
372    pub fn class(&self) -> FileInformationClass {
373        self.class
374    }
375
376    /// The buffer size the query will be given.
377    #[must_use]
378    pub fn capacity(&self) -> usize {
379        self.capacity
380    }
381
382    /// Performs the query on the calling thread.
383    ///
384    /// Returns the **whole** buffer, 8-byte aligned. The call reports no
385    /// written length -- a batch is walked by its own next-entry offsets -- so
386    /// nothing here invents a byte count it cannot know.
387    ///
388    /// # Errors
389    ///
390    /// Returns the raw Win32 code, unaltered. `ERROR_NO_MORE_FILES` ends a
391    /// directory enumeration and `ERROR_MORE_DATA` means the buffer held no
392    /// complete entry; both are the caller's to interpret.
393    ///
394    /// The one code the call did not produce is `ERROR_NOT_ENOUGH_MEMORY`, returned when the
395    /// buffer for the capacity this query was built with cannot be allocated -- too large for
396    /// the machine, or refused by the allocator. The call was not made, and a smaller capacity
397    /// can be tried.
398    pub fn perform(&self) -> Outcome<AlignedBuf> {
399        let mut buffer =
400            AlignedBuf::zeroed(self.capacity, BATCH_ALIGNMENT).map_err(|_| out_of_memory())?;
401        // At `BATCH_ALIGNMENT`, which every entry of `REQUIRED_ALIGNMENT` divides (checked at
402        // build), so the refusal `perform_into` makes cannot apply.
403        self.call_into(&mut buffer)?;
404        Ok(buffer)
405    }
406
407    /// Performs the query on the calling thread, into a buffer the caller owns and keeps.
408    ///
409    /// For a consumer that reads batch after batch: one buffer serves every call, and nothing is
410    /// allocated here. The buffer's length is the size the call is given, and this request's
411    /// [`capacity`](Self::capacity) is not used.
412    ///
413    /// # What is asked of the buffer
414    ///
415    /// **Its address is refused, before the call, when the class's structure must be aligned and
416    /// the address is not.** The `GetFileInformationByHandleEx` page (`ms.date` 2018-12-05) gives
417    /// each class's `lpFileInformation` type, and three of those structures' pages say the
418    /// structure "must be aligned on a" "(8-byte) boundary": `FILE_STREAM_INFO` (for
419    /// `FileStreamInfo`), `FILE_ID_BOTH_DIR_INFO` (`FileIdBothDirectoryInfo` and its restart form)
420    /// and `FILE_FULL_DIR_INFO` (`FileFullDirectoryInfo` and its restart form). For those five
421    /// classes a buffer whose address is not on an 8-byte boundary is
422    /// [`PerformIntoError::Misaligned`] and the call is not made. It is the address that is
423    /// checked, since that is what the pages constrain: a buffer stated at 8 or more
424    /// ([`AlignedBuf::align`]) is never refused, and one stated at less is refused only where it
425    /// actually lies off the boundary.
426    ///
427    /// For every other class nothing is refused. The `FILE_ID_EXTD_DIR_INFO` page, for the two
428    /// classes this crate names, states no alignment; those classes were measured failing with
429    /// `ERROR_NOACCESS` on a misaligned buffer, which is not made a check, and is why
430    /// [`perform`](Self::perform) allocates at 8.
431    ///
432    /// A buffer longer than a `DWORD` can count is given as `u32::MAX` bytes, its first that many,
433    /// as `perform` gives an over-large capacity.
434    ///
435    /// # What the buffer holds afterwards
436    ///
437    /// What the call wrote. The buffer is not cleared first, so bytes the call does not write keep
438    /// what they held -- unlike `perform`'s buffer, which starts zeroed. A batch is walked by its
439    /// own next-entry offsets, so a consumer reads only what the call wrote. After a failure the
440    /// contents are whatever the call left; the page does not say.
441    ///
442    /// # Errors
443    ///
444    /// [`PerformIntoError::Misaligned`], with no OS code, for a buffer refused as above. Otherwise
445    /// [`PerformIntoError::Win32`] with the raw code, unaltered, as [`perform`](Self::perform)
446    /// returns it. With no allocation, the out-of-memory report `perform` can make does not arise.
447    ///
448    /// # Example
449    ///
450    /// ```
451    /// use windows_namespace_request_sys::AlignedBuf;
452    /// use windows_namespace_request_sys::query::{
453    ///     FileInformationClass, PerformIntoError, QueryFileInformation,
454    /// };
455    /// use windows_sys::Win32::Foundation::ERROR_NO_MORE_FILES;
456    /// use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
457    ///
458    /// use std::os::windows::fs::OpenOptionsExt;
459    ///
460    /// let path = std::env::temp_dir().join(format!("wnrs-into-{}", std::process::id()));
461    /// std::fs::create_dir_all(&path)?;
462    /// std::fs::write(path.join("a.t"), b"a")?;
463    /// let directory = std::fs::OpenOptions::new()
464    ///     .read(true)
465    ///     .custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
466    ///     .open(&path)?;
467    /// let restart = QueryFileInformation::new(
468    ///     directory.try_clone()?,
469    ///     FileInformationClass::ID_EXTD_DIRECTORY_RESTART,
470    /// );
471    /// let next = QueryFileInformation::new(directory, FileInformationClass::ID_EXTD_DIRECTORY);
472    ///
473    /// // One buffer, allocated once, for every batch.
474    /// let mut buffer = AlignedBuf::zeroed(64 * 1024, 8)?;
475    /// restart.perform_into(&mut buffer)?;
476    /// let mut batches = 1;
477    /// loop {
478    ///     match next.perform_into(&mut buffer) {
479    ///         Ok(()) => batches += 1,
480    ///         Err(PerformIntoError::Win32(error)) if error.code() == ERROR_NO_MORE_FILES => break,
481    ///         Err(error) => return Err(error.into()),
482    ///     }
483    /// }
484    /// assert!(batches >= 1);
485    /// # drop((restart, next));
486    /// # std::fs::remove_dir_all(&path)?;
487    /// # Ok::<(), Box<dyn std::error::Error>>(())
488    /// ```
489    pub fn perform_into(&self, buffer: &mut AlignedBuf) -> Result<(), PerformIntoError> {
490        check_alignment(self.class, buffer.as_ptr().addr())?;
491        Ok(self.call_into(buffer)?)
492    }
493
494    /// The call, into `buffer`, with no check of the buffer.
495    fn call_into(&self, buffer: &mut AlignedBuf) -> Outcome<()> {
496        let size = u32::try_from(buffer.len()).unwrap_or(u32::MAX);
497
498        perform_bool(|| {
499            // SAFETY: the handle is one this request holds and keeps open across the call, and the
500            // buffer is writable for at least `size` bytes, its length or `u32::MAX` if that is
501            // less. Its alignment, where a structure's page requires one, was checked by the
502            // caller (`perform_into`) or is `BATCH_ALIGNMENT` (`perform`).
503            unsafe {
504                GetFileInformationByHandleEx(
505                    self.handle.raw(),
506                    self.class.as_raw(),
507                    buffer.as_mut_ptr().cast(),
508                    size,
509                )
510            }
511        })
512    }
513}
514
515impl crate::request::Request for QueryFileInformation {
516    type Error = crate::Win32Error;
517    type Output = AlignedBuf;
518
519    fn perform(&self) -> Outcome<AlignedBuf> {
520        Self::perform(self)
521    }
522}
523
524#[cfg(test)]
525mod alignment_tests;
526#[cfg(test)]
527mod tests;