Skip to main content

windows_file_enumeration_sys/
request.rs

1// Copyright (c) 2026 Mike Grier
2//! What a caller submits: one directory, one predicate, one set of bounds.
3//!
4//! A request is a plain owned value. It borrows nothing from the caller, so the
5//! string it was built from may go away immediately, and it can be built on one
6//! thread and submitted from another. Everything that can be rejected about a
7//! request is rejected here, on the caller's own thread, before anything has
8//! been accepted.
9
10use std::path::Path;
11
12use wtf_string::{Wtf16Str, Wtf16String};
13
14use crate::entry::FileIdentityMode;
15use crate::error::{RequestError, RequestFailure};
16use crate::path;
17use crate::predicate::EntryPredicate;
18
19/// The default native buffer capacity, in bytes.
20///
21/// Large enough that an ordinary directory is read in one or two queries, which
22/// is what keeps the per-refill cost off the per-entry path.
23pub const DEFAULT_BUFFER_CAPACITY: usize = 64 * 1024;
24
25/// The smallest native buffer capacity, in bytes.
26///
27/// A smaller buffer would not reliably hold one maximum-length record, turning
28/// an ordinary directory into an oversize-record failure.
29pub const MINIMUM_BUFFER_CAPACITY: usize = 1024;
30
31/// The alignment a `FILE_ID_EXTD_DIR_INFO` record's fields require.
32///
33/// The record contains `i64` fields, and the API keeps every record in a batch
34/// on this boundary, so both the buffer's base address and its length are held
35/// to it.
36pub(crate) const RECORD_ALIGNMENT: usize = 8;
37
38/// One directory to enumerate, with the predicate and bounds that apply to it.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct EnumerationRequest {
41    path: Wtf16String,
42    predicate: EntryPredicate,
43    file_identity_mode: FileIdentityMode,
44    buffer_capacity: usize,
45}
46
47impl EnumerationRequest {
48    /// Build a request for a native WTF-16 path.
49    ///
50    /// The path is validated and, unless it is already a verbatim `\\?\` path,
51    /// resolved to its fully qualified form now. The stored value is exactly
52    /// what a worker will later open.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`RequestError`] for an empty path, an interior NUL, a `\\?\`
57    /// path that is not fully qualified, an ordinary path longer than
58    /// `MAX_PATH` before or after resolution, or a resolution failure reported
59    /// by Windows.
60    pub fn new(path: &Wtf16Str) -> Result<Self, RequestError> {
61        Ok(Self {
62            path: path::prepare(path)?,
63            predicate: EntryPredicate::default(),
64            file_identity_mode: FileIdentityMode::default(),
65            buffer_capacity: DEFAULT_BUFFER_CAPACITY,
66        })
67    }
68
69    /// Build a request for a `std` path.
70    ///
71    /// The conversion is lossless in both directions: a Windows [`Path`] is
72    /// already WTF-16 underneath.
73    ///
74    /// # Errors
75    ///
76    /// As [`new`](Self::new).
77    pub fn for_path(path: &Path) -> Result<Self, RequestError> {
78        Self::new(&Wtf16String::from_os_str(path.as_os_str()))
79    }
80
81    /// Set the predicate entries must satisfy to be delivered.
82    ///
83    /// Infallible because a [`QueryByExample`](crate::QueryByExample) validates
84    /// each clause as it is added, so an invalid predicate cannot be built in
85    /// the first place.
86    #[must_use]
87    pub fn with_predicate(mut self, predicate: impl Into<EntryPredicate>) -> Self {
88        self.predicate = predicate.into();
89        self
90    }
91
92    /// Set how much work the request will do for file identity.
93    #[must_use]
94    pub fn with_file_identity(mut self, mode: FileIdentityMode) -> Self {
95        self.file_identity_mode = mode;
96        self
97    }
98
99    /// Set the native buffer capacity, in bytes.
100    ///
101    /// The value is clamped up to [`MINIMUM_BUFFER_CAPACITY`] and then rounded
102    /// up to the record alignment. The result is fixed for the request's whole
103    /// life: the buffer never grows in response to what a directory turns out to
104    /// contain, because a bound that silently moves is not a bound. Read the
105    /// value back with [`buffer_capacity`](Self::buffer_capacity).
106    ///
107    /// # Errors
108    ///
109    /// Returns [`RequestFailure::BufferCapacityUnrepresentable`] if the aligned
110    /// capacity cannot be passed to Win32 as a `u32`.
111    pub fn with_buffer_capacity(mut self, bytes: usize) -> Result<Self, RequestError> {
112        self.buffer_capacity = effective_buffer_capacity(bytes)?;
113        Ok(self)
114    }
115
116    /// The exact path a worker will open.
117    #[must_use]
118    pub fn path(&self) -> &Wtf16Str {
119        &self.path
120    }
121
122    /// The predicate entries must satisfy.
123    #[must_use]
124    pub fn predicate(&self) -> &EntryPredicate {
125        &self.predicate
126    }
127
128    /// How much work the request will do for file identity.
129    #[must_use]
130    pub fn file_identity_mode(&self) -> FileIdentityMode {
131        self.file_identity_mode
132    }
133
134    /// The effective native buffer capacity, in bytes, after clamping and
135    /// alignment.
136    #[must_use]
137    pub fn buffer_capacity(&self) -> usize {
138        self.buffer_capacity
139    }
140}
141
142/// Clamp and align a requested capacity.
143fn effective_buffer_capacity(bytes: usize) -> Result<usize, RequestError> {
144    let clamped = bytes.max(MINIMUM_BUFFER_CAPACITY);
145    // Rounding up cannot overflow in practice, but a `usize::MAX` request would
146    // wrap to zero, which is exactly the value the alignment is meant to rule
147    // out -- so the overflow is reported rather than wrapped.
148    let aligned = clamped
149        .checked_next_multiple_of(RECORD_ALIGNMENT)
150        .ok_or_else(|| RequestError::new(RequestFailure::BufferCapacityUnrepresentable))?;
151    u32::try_from(aligned)
152        .map_err(|_| RequestError::new(RequestFailure::BufferCapacityUnrepresentable))?;
153    Ok(aligned)
154}
155
156#[cfg(test)]
157mod tests;