windows_file_enumeration_sys/error.rs
1// Copyright (c) 2026 Mike Grier
2//! The crate's error taxonomy.
3//!
4//! Failures split by *when* they are observable, which is the distinction the
5//! settled contract draws. Building a request or a query fails synchronously,
6//! on the caller's own thread, before anything has been accepted
7//! ([`RequestError`], [`PredicateError`]). Once an enumeration has been
8//! accepted it owns a reserved completion slot, so every later failure arrives
9//! as one ordered terminal outcome carrying an [`EnumerationError`].
10//!
11//! Every native failure keeps the raw Win32 code it arrived with. The crate
12//! owns the *classification* -- which is why an unsupported directory-
13//! information class is its own variant rather than a code a caller has to
14//! recognise -- but it never discards the code that classification was derived
15//! from.
16
17use std::fmt;
18use std::io;
19
20use windows_impersonation_token_sys::{ApplyError, CaptureError, ImpersonationToken};
21
22use crate::request::EnumerationRequest;
23
24/// A raw Win32 error code, kept in the currency it arrived in.
25///
26/// Every failing API this crate calls is a classic last-error API, so a code is
27/// always a `WIN32_ERROR` rather than an `HRESULT`. Keeping the raw value beside
28/// the crate's own classification means a caller can act on either.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub struct Win32Error(u32);
31
32impl Win32Error {
33 /// Wrap a raw `WIN32_ERROR`.
34 #[must_use]
35 pub const fn from_code(code: u32) -> Self {
36 Self(code)
37 }
38
39 /// Take the code from an OS error, or `0` if it carries none.
40 ///
41 /// A last-error API always sets one, so `0` covers only a fabricated
42 /// [`io::Error`] with no OS error behind it.
43 #[must_use]
44 pub fn from_io(error: &io::Error) -> Self {
45 Self(
46 error
47 .raw_os_error()
48 .and_then(|code| u32::try_from(code).ok())
49 .unwrap_or(0),
50 )
51 }
52
53 /// The last error of the calling thread.
54 ///
55 /// Call immediately after the failing Win32 call, before anything else can
56 /// overwrite the thread's last error.
57 #[must_use]
58 pub(crate) fn last() -> Self {
59 Self::from_io(&io::Error::last_os_error())
60 }
61
62 /// The raw `WIN32_ERROR` value.
63 #[must_use]
64 pub const fn code(self) -> u32 {
65 self.0
66 }
67
68 /// The same failure as a standard [`io::Error`], for callers that funnel
69 /// everything through `std::io`.
70 #[must_use]
71 pub fn to_io_error(self) -> io::Error {
72 io::Error::from_raw_os_error(
73 i32::try_from(self.0).expect("a WIN32_ERROR always fits in an i32"),
74 )
75 }
76}
77
78impl fmt::Display for Win32Error {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "Win32 error {} ({})", self.0, self.to_io_error())
81 }
82}
83
84/// Why a request could not be built.
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
86#[non_exhaustive]
87pub enum RequestFailure {
88 /// The path had no code units. An empty path names no directory.
89 EmptyPath,
90 /// The path contained an interior NUL. Win32 would stop at it and open a
91 /// different, shorter path than the caller named.
92 InteriorNul,
93 /// An ordinary path, or the fully qualified form it resolved to, did not fit
94 /// the ordinary `MAX_PATH` limit including its terminator.
95 ///
96 /// This limit is deliberate rather than incidental: it keeps behaviour
97 /// independent of the host executable's `longPathAware` manifest. Supply a
98 /// fully qualified `\\?\` path to enumerate a longer one.
99 PathTooLong,
100 /// A `\\?\` path was not fully qualified, so Win32 would not interpret it as
101 /// the verbatim absolute path that prefix promises.
102 NotFullyQualified,
103 /// Windows could not resolve an ordinary path to its fully qualified form.
104 PathResolution,
105 /// The requested native buffer capacity, after clamping and alignment,
106 /// cannot be passed to Win32 as a `u32`.
107 BufferCapacityUnrepresentable,
108}
109
110impl RequestFailure {
111 /// A short description of the failure, without any raw code.
112 const fn describe(self) -> &'static str {
113 match self {
114 RequestFailure::EmptyPath => "the path is empty",
115 RequestFailure::InteriorNul => "the path contains an interior NUL",
116 RequestFailure::PathTooLong => {
117 "the path exceeds MAX_PATH; supply a fully qualified \\\\?\\ path"
118 }
119 RequestFailure::NotFullyQualified => "the \\\\?\\ path is not fully qualified",
120 RequestFailure::PathResolution => "the path could not be resolved",
121 RequestFailure::BufferCapacityUnrepresentable => {
122 "the native buffer capacity does not fit a Win32 u32"
123 }
124 }
125 }
126}
127
128/// A synchronous failure while building an [`EnumerationRequest`].
129///
130/// [`EnumerationRequest`]: crate::EnumerationRequest
131#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
132pub struct RequestError {
133 failure: RequestFailure,
134 code: Option<Win32Error>,
135}
136
137impl RequestError {
138 pub(crate) const fn new(failure: RequestFailure) -> Self {
139 Self {
140 failure,
141 code: None,
142 }
143 }
144
145 pub(crate) const fn with_code(failure: RequestFailure, code: Win32Error) -> Self {
146 Self {
147 failure,
148 code: Some(code),
149 }
150 }
151
152 /// What about the request was rejected.
153 #[must_use]
154 pub const fn failure(&self) -> RequestFailure {
155 self.failure
156 }
157
158 /// The raw Win32 code behind the failure, when Windows produced one.
159 ///
160 /// Only [`RequestFailure::PathResolution`] arises from a Win32 call; the
161 /// other failures are decided by this crate before any call is made.
162 #[must_use]
163 pub const fn code(&self) -> Option<Win32Error> {
164 self.code
165 }
166}
167
168impl fmt::Display for RequestError {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self.code {
171 Some(code) => write!(f, "{}: {code}", self.failure.describe()),
172 None => f.write_str(self.failure.describe()),
173 }
174 }
175}
176
177impl std::error::Error for RequestError {}
178
179/// Why an enumeration was not admitted.
180#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
181#[non_exhaustive]
182pub enum BeginFailure {
183 /// The submission ring had no room for ordinary traffic.
184 ///
185 /// Reserved cancellation and abandonment messages are unaffected: this is
186 /// backpressure on *starting* work, applied where a caller can respond to
187 /// it.
188 SubmissionRingFull,
189 /// The completion ring could not reserve the terminal slot this enumeration
190 /// would owe.
191 ///
192 /// Reservations never take the ring's last slot, so this is reached when the
193 /// session is already carrying as many enumerations as its completion ring
194 /// can account for.
195 CompletionRingFull,
196 /// The receiver is gone, so the session no longer starts anything.
197 Abandoned,
198 /// The caller's security context could not be captured.
199 TokenCapture,
200 /// The enumeration's fixed native buffer could not be allocated.
201 ///
202 /// Reported rather than aborting the process, which is what the ordinary
203 /// growable-vector path would do.
204 BufferAllocation,
205}
206
207impl BeginFailure {
208 const fn describe(self) -> &'static str {
209 match self {
210 BeginFailure::SubmissionRingFull => "the submission ring is full",
211 BeginFailure::CompletionRingFull => {
212 "the completion ring cannot reserve a terminal slot"
213 }
214 BeginFailure::Abandoned => "the session has been abandoned by its receiver",
215 BeginFailure::TokenCapture => "the caller's security context could not be captured",
216 BeginFailure::BufferAllocation => "the native buffer could not be allocated",
217 }
218 }
219}
220
221/// A synchronous refusal to start an enumeration.
222///
223/// The request -- and the captured security context, when there was one -- come
224/// back with the error, because nothing was accepted: a caller can retry with
225/// exactly what it submitted rather than rebuilding it.
226#[derive(Debug)]
227pub struct BeginError {
228 failure: BeginFailure,
229 request: EnumerationRequest,
230 token: Option<ImpersonationToken>,
231 capture: Option<CaptureError>,
232}
233
234impl BeginError {
235 pub(crate) fn rejected(
236 failure: BeginFailure,
237 request: EnumerationRequest,
238 token: Option<ImpersonationToken>,
239 ) -> Self {
240 Self {
241 failure,
242 request,
243 token,
244 capture: None,
245 }
246 }
247
248 pub(crate) fn capture(request: EnumerationRequest, capture: CaptureError) -> Self {
249 Self {
250 failure: BeginFailure::TokenCapture,
251 request,
252 token: None,
253 capture: Some(capture),
254 }
255 }
256
257 /// Why the enumeration was refused.
258 #[must_use]
259 pub const fn failure(&self) -> BeginFailure {
260 self.failure
261 }
262
263 /// The request that was refused.
264 #[must_use]
265 pub const fn request(&self) -> &EnumerationRequest {
266 &self.request
267 }
268
269 /// Take back the request and, when one was captured, the security context,
270 /// so a retry costs neither a rebuild nor a second capture.
271 #[must_use]
272 pub fn into_parts(self) -> (EnumerationRequest, Option<ImpersonationToken>) {
273 (self.request, self.token)
274 }
275
276 /// The capture failure behind a [`BeginFailure::TokenCapture`].
277 #[must_use]
278 pub const fn capture_error(&self) -> Option<&CaptureError> {
279 self.capture.as_ref()
280 }
281}
282
283impl fmt::Display for BeginError {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 match &self.capture {
286 Some(capture) => write!(f, "{}: {capture}", self.failure.describe()),
287 None => f.write_str(self.failure.describe()),
288 }
289 }
290}
291
292impl std::error::Error for BeginError {
293 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
294 self.capture
295 .as_ref()
296 .map(|capture| capture as &(dyn std::error::Error + 'static))
297 }
298}
299
300/// Why a session could not be built.
301#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
302#[non_exhaustive]
303pub enum SessionFailure {
304 /// The submission ring could not carry one enumeration.
305 ///
306 /// It needs room for the session's standing abandon message, one
307 /// enumeration's reserved cancellation, and one ordinary begin.
308 SubmissionCapacityTooSmall,
309 /// The completion ring could not carry one enumeration.
310 ///
311 /// It needs room for one reserved terminal outcome and one entry, and
312 /// reservations never take the last slot.
313 CompletionCapacityTooSmall,
314 /// Windows refused to create the servicer's thread-pool work object.
315 WorkObject,
316}
317
318impl SessionFailure {
319 const fn describe(self) -> &'static str {
320 match self {
321 SessionFailure::SubmissionCapacityTooSmall => {
322 "the submission ring is too small to carry one enumeration"
323 }
324 SessionFailure::CompletionCapacityTooSmall => {
325 "the completion ring is too small to carry one enumeration"
326 }
327 SessionFailure::WorkObject => "the servicer's work object could not be created",
328 }
329 }
330}
331
332/// A synchronous failure while building a session.
333#[derive(Debug)]
334pub struct SessionError {
335 failure: SessionFailure,
336 source: Option<io::Error>,
337}
338
339impl SessionError {
340 pub(crate) const fn new(failure: SessionFailure) -> Self {
341 Self {
342 failure,
343 source: None,
344 }
345 }
346
347 pub(crate) const fn with_source(failure: SessionFailure, source: io::Error) -> Self {
348 Self {
349 failure,
350 source: Some(source),
351 }
352 }
353
354 /// What about the session was rejected.
355 #[must_use]
356 pub const fn failure(&self) -> SessionFailure {
357 self.failure
358 }
359
360 /// The OS error behind the failure, when Windows produced one.
361 #[must_use]
362 pub const fn os_error(&self) -> Option<&io::Error> {
363 self.source.as_ref()
364 }
365}
366
367impl fmt::Display for SessionError {
368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369 match &self.source {
370 Some(source) => write!(f, "{}: {source}", self.failure.describe()),
371 None => f.write_str(self.failure.describe()),
372 }
373 }
374}
375
376impl std::error::Error for SessionError {
377 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
378 self.source
379 .as_ref()
380 .map(|source| source as &(dyn std::error::Error + 'static))
381 }
382}
383
384/// Why a query-by-example clause was rejected.
385///
386/// Both cases describe a clause that would silently match everything. Rejecting
387/// them turns a likely caller mistake into a reported error rather than an
388/// invisible match-all.
389#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
390#[non_exhaustive]
391pub enum PredicateFailure {
392 /// An attribute mask was zero. Every bit of an empty mask is both set and
393 /// clear, so the clause is vacuous either way round.
394 EmptyAttributeMask,
395 /// A name-pattern set was empty. It matches nothing, and its negation
396 /// matches everything.
397 EmptyNameSet,
398}
399
400impl PredicateFailure {
401 const fn describe(self) -> &'static str {
402 match self {
403 PredicateFailure::EmptyAttributeMask => {
404 "an attribute mask clause requires a non-zero mask"
405 }
406 PredicateFailure::EmptyNameSet => "a name-set clause requires at least one pattern",
407 }
408 }
409}
410
411/// A synchronous failure while building a query.
412#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
413pub struct PredicateError {
414 failure: PredicateFailure,
415}
416
417impl PredicateError {
418 pub(crate) const fn new(failure: PredicateFailure) -> Self {
419 Self { failure }
420 }
421
422 /// What about the clause was rejected.
423 #[must_use]
424 pub const fn failure(&self) -> PredicateFailure {
425 self.failure
426 }
427}
428
429impl fmt::Display for PredicateError {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 f.write_str(self.failure.describe())
432 }
433}
434
435impl std::error::Error for PredicateError {}
436
437/// Which part of a native directory record failed validation.
438///
439/// Every variant describes a record the crate refused to read rather than one it
440/// read incorrectly: the check happens before the field is touched.
441#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
442#[non_exhaustive]
443pub enum MalformedRecord {
444 /// The record did not start on the alignment its fixed fields require.
445 Alignment,
446 /// The remaining buffer was too short to hold the record's fixed fields.
447 TruncatedFixedFields,
448 /// The next-entry offset did not advance within the returned batch.
449 NextEntryOffset,
450 /// The name length was not a whole number of UTF-16 code units.
451 OddNameLength,
452 /// The name extended past the end of the returned batch.
453 NameOutOfBounds,
454 /// A native size field was negative, so it cannot be a byte count.
455 NegativeSize,
456}
457
458impl MalformedRecord {
459 const fn describe(self) -> &'static str {
460 match self {
461 MalformedRecord::Alignment => "the record is misaligned",
462 MalformedRecord::TruncatedFixedFields => "the record's fixed fields are truncated",
463 MalformedRecord::NextEntryOffset => "the record's next-entry offset does not advance",
464 MalformedRecord::OddNameLength => {
465 "the record's name length is not a whole code-unit count"
466 }
467 MalformedRecord::NameOutOfBounds => "the record's name extends past the batch",
468 MalformedRecord::NegativeSize => "the record reports a negative size",
469 }
470 }
471}
472
473/// Why an accepted enumeration failed.
474///
475/// An enumeration reaches this only after it has been accepted, so every value
476/// here arrives as the [`Failed`](crate::TerminalOutcome::Failed) terminal
477/// outcome for one [`EnumerationId`](crate::EnumerationId) -- never as the
478/// result of a submission call.
479///
480/// Clean exhaustion is deliberately absent. `ERROR_NO_MORE_FILES` from any
481/// refill, and `ERROR_FILE_NOT_FOUND` from the very first one, are the two forms
482/// of "this directory has no more entries" and produce
483/// [`Completed`](crate::TerminalOutcome::Completed).
484#[derive(Debug)]
485#[non_exhaustive]
486pub enum EnumerationError {
487 /// The worker could not apply the submitted impersonation context, so the
488 /// directory was never opened under the submitter's identity.
489 Impersonation(ApplyError),
490 /// The directory could not be opened. Existence, access, and
491 /// not-a-directory failures all arrive here, distinguished by the raw code.
492 DirectoryOpen(Win32Error),
493 /// A volume serial was [`Required`](crate::FileIdentityMode::Required) and
494 /// could not be obtained, so no entry could carry the globally meaningful
495 /// identity the request demanded.
496 VolumeIdentity(Win32Error),
497 /// The filesystem does not support extended directory information.
498 ///
499 /// The crate does not fall back to a metadata-poorer enumeration API,
500 /// because that would silently drop change time, allocation size,
501 /// extended-attribute size, and the 128-bit file ID from the contract.
502 UnsupportedExtendedDirectoryInfo(Win32Error),
503 /// A directory-information query failed for a reason that is neither clean
504 /// exhaustion, an unsupported class, nor an oversize record.
505 DirectoryQuery(Win32Error),
506 /// One record did not fit the request's fixed native buffer.
507 ///
508 /// The buffer never grows, so this is reported rather than hidden. Retry
509 /// with an explicitly larger capacity.
510 RecordTooLarge {
511 /// The effective capacity, in bytes, that the record did not fit.
512 buffer_capacity: usize,
513 /// The raw code the failing refill reported.
514 code: Win32Error,
515 },
516 /// A returned record failed validation before any of its fields were read.
517 MalformedRecord(MalformedRecord),
518}
519
520impl EnumerationError {
521 /// The raw Win32 code behind this failure, when one is available.
522 ///
523 /// [`MalformedRecord`](Self::MalformedRecord) has none -- the record was
524 /// rejected by this crate, not by Windows -- and
525 /// [`Impersonation`](Self::Impersonation) carries the sibling crate's typed
526 /// error, whose own code is reachable through it.
527 #[must_use]
528 pub fn code(&self) -> Option<Win32Error> {
529 match self {
530 EnumerationError::Impersonation(error) => error
531 .raw_os_error()
532 .and_then(|code| u32::try_from(code).ok())
533 .map(Win32Error::from_code),
534 EnumerationError::DirectoryOpen(code)
535 | EnumerationError::VolumeIdentity(code)
536 | EnumerationError::UnsupportedExtendedDirectoryInfo(code)
537 | EnumerationError::DirectoryQuery(code)
538 | EnumerationError::RecordTooLarge { code, .. } => Some(*code),
539 EnumerationError::MalformedRecord(_) => None,
540 }
541 }
542}
543
544impl fmt::Display for EnumerationError {
545 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546 match self {
547 EnumerationError::Impersonation(error) => {
548 write!(f, "the submitted impersonation context failed: {error}")
549 }
550 EnumerationError::DirectoryOpen(code) => {
551 write!(f, "the directory could not be opened: {code}")
552 }
553 EnumerationError::VolumeIdentity(code) => {
554 write!(f, "the required volume identity is unavailable: {code}")
555 }
556 EnumerationError::UnsupportedExtendedDirectoryInfo(code) => write!(
557 f,
558 "extended directory information is unsupported here: {code}"
559 ),
560 EnumerationError::DirectoryQuery(code) => {
561 write!(f, "the directory query failed: {code}")
562 }
563 EnumerationError::RecordTooLarge {
564 buffer_capacity,
565 code,
566 } => write!(
567 f,
568 "one record exceeds the {buffer_capacity}-byte native buffer: {code}"
569 ),
570 EnumerationError::MalformedRecord(detail) => {
571 write!(
572 f,
573 "a native record failed validation: {}",
574 detail.describe()
575 )
576 }
577 }
578 }
579}
580
581impl std::error::Error for EnumerationError {
582 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
583 match self {
584 EnumerationError::Impersonation(error) => Some(error),
585 _ => None,
586 }
587 }
588}
589
590#[cfg(test)]
591mod tests;