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;