Skip to main content

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;