windows-file-enumeration-sys 0.1.1

Memory-safe asynchronous enumeration of one Windows directory with bounded submission and completion rings.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
// Copyright (c) 2026 Mike Grier
//! The crate's error taxonomy.
//!
//! Failures split by *when* they are observable, which is the distinction the
//! settled contract draws. Building a request or a query fails synchronously,
//! on the caller's own thread, before anything has been accepted
//! ([`RequestError`], [`PredicateError`]). Once an enumeration has been
//! accepted it owns a reserved completion slot, so every later failure arrives
//! as one ordered terminal outcome carrying an [`EnumerationError`].
//!
//! Every native failure keeps the raw Win32 code it arrived with. The crate
//! owns the *classification* -- which is why an unsupported directory-
//! information class is its own variant rather than a code a caller has to
//! recognise -- but it never discards the code that classification was derived
//! from.

use std::fmt;
use std::io;

use windows_impersonation_token_sys::{ApplyError, CaptureError, ImpersonationToken};

use crate::request::EnumerationRequest;

/// A raw Win32 error code, kept in the currency it arrived in.
///
/// Every failing API this crate calls is a classic last-error API, so a code is
/// always a `WIN32_ERROR` rather than an `HRESULT`. Keeping the raw value beside
/// the crate's own classification means a caller can act on either.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Win32Error(u32);

impl Win32Error {
    /// Wrap a raw `WIN32_ERROR`.
    #[must_use]
    pub const fn from_code(code: u32) -> Self {
        Self(code)
    }

    /// Take the code from an OS error, or `0` if it carries none.
    ///
    /// A last-error API always sets one, so `0` covers only a fabricated
    /// [`io::Error`] with no OS error behind it.
    #[must_use]
    pub fn from_io(error: &io::Error) -> Self {
        Self(
            error
                .raw_os_error()
                .and_then(|code| u32::try_from(code).ok())
                .unwrap_or(0),
        )
    }

    /// The last error of the calling thread.
    ///
    /// Call immediately after the failing Win32 call, before anything else can
    /// overwrite the thread's last error.
    #[must_use]
    pub(crate) fn last() -> Self {
        Self::from_io(&io::Error::last_os_error())
    }

    /// The raw `WIN32_ERROR` value.
    #[must_use]
    pub const fn code(self) -> u32 {
        self.0
    }

    /// The same failure as a standard [`io::Error`], for callers that funnel
    /// everything through `std::io`.
    #[must_use]
    pub fn to_io_error(self) -> io::Error {
        io::Error::from_raw_os_error(
            i32::try_from(self.0).expect("a WIN32_ERROR always fits in an i32"),
        )
    }
}

impl fmt::Display for Win32Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Win32 error {} ({})", self.0, self.to_io_error())
    }
}

/// Why a request could not be built.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RequestFailure {
    /// The path had no code units. An empty path names no directory.
    EmptyPath,
    /// The path contained an interior NUL. Win32 would stop at it and open a
    /// different, shorter path than the caller named.
    InteriorNul,
    /// An ordinary path, or the fully qualified form it resolved to, did not fit
    /// the ordinary `MAX_PATH` limit including its terminator.
    ///
    /// This limit is deliberate rather than incidental: it keeps behaviour
    /// independent of the host executable's `longPathAware` manifest. Supply a
    /// fully qualified `\\?\` path to enumerate a longer one.
    PathTooLong,
    /// A `\\?\` path was not fully qualified, so Win32 would not interpret it as
    /// the verbatim absolute path that prefix promises.
    NotFullyQualified,
    /// Windows could not resolve an ordinary path to its fully qualified form.
    PathResolution,
    /// The requested native buffer capacity, after clamping and alignment,
    /// cannot be passed to Win32 as a `u32`.
    BufferCapacityUnrepresentable,
}

impl RequestFailure {
    /// A short description of the failure, without any raw code.
    const fn describe(self) -> &'static str {
        match self {
            RequestFailure::EmptyPath => "the path is empty",
            RequestFailure::InteriorNul => "the path contains an interior NUL",
            RequestFailure::PathTooLong => {
                "the path exceeds MAX_PATH; supply a fully qualified \\\\?\\ path"
            }
            RequestFailure::NotFullyQualified => "the \\\\?\\ path is not fully qualified",
            RequestFailure::PathResolution => "the path could not be resolved",
            RequestFailure::BufferCapacityUnrepresentable => {
                "the native buffer capacity does not fit a Win32 u32"
            }
        }
    }
}

/// A synchronous failure while building an [`EnumerationRequest`].
///
/// [`EnumerationRequest`]: crate::EnumerationRequest
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RequestError {
    failure: RequestFailure,
    code: Option<Win32Error>,
}

impl RequestError {
    pub(crate) const fn new(failure: RequestFailure) -> Self {
        Self {
            failure,
            code: None,
        }
    }

    pub(crate) const fn with_code(failure: RequestFailure, code: Win32Error) -> Self {
        Self {
            failure,
            code: Some(code),
        }
    }

    /// What about the request was rejected.
    #[must_use]
    pub const fn failure(&self) -> RequestFailure {
        self.failure
    }

    /// The raw Win32 code behind the failure, when Windows produced one.
    ///
    /// Only [`RequestFailure::PathResolution`] arises from a Win32 call; the
    /// other failures are decided by this crate before any call is made.
    #[must_use]
    pub const fn code(&self) -> Option<Win32Error> {
        self.code
    }
}

impl fmt::Display for RequestError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.code {
            Some(code) => write!(f, "{}: {code}", self.failure.describe()),
            None => f.write_str(self.failure.describe()),
        }
    }
}

impl std::error::Error for RequestError {}

/// Why an enumeration was not admitted.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BeginFailure {
    /// The submission ring had no room for ordinary traffic.
    ///
    /// Reserved cancellation and abandonment messages are unaffected: this is
    /// backpressure on *starting* work, applied where a caller can respond to
    /// it.
    SubmissionRingFull,
    /// The completion ring could not reserve the terminal slot this enumeration
    /// would owe.
    ///
    /// Reservations never take the ring's last slot, so this is reached when the
    /// session is already carrying as many enumerations as its completion ring
    /// can account for.
    CompletionRingFull,
    /// The receiver is gone, so the session no longer starts anything.
    Abandoned,
    /// The caller's security context could not be captured.
    TokenCapture,
    /// The enumeration's fixed native buffer could not be allocated.
    ///
    /// Reported rather than aborting the process, which is what the ordinary
    /// growable-vector path would do.
    BufferAllocation,
}

impl BeginFailure {
    const fn describe(self) -> &'static str {
        match self {
            BeginFailure::SubmissionRingFull => "the submission ring is full",
            BeginFailure::CompletionRingFull => {
                "the completion ring cannot reserve a terminal slot"
            }
            BeginFailure::Abandoned => "the session has been abandoned by its receiver",
            BeginFailure::TokenCapture => "the caller's security context could not be captured",
            BeginFailure::BufferAllocation => "the native buffer could not be allocated",
        }
    }
}

/// A synchronous refusal to start an enumeration.
///
/// The request -- and the captured security context, when there was one -- come
/// back with the error, because nothing was accepted: a caller can retry with
/// exactly what it submitted rather than rebuilding it.
#[derive(Debug)]
pub struct BeginError {
    failure: BeginFailure,
    request: EnumerationRequest,
    token: Option<ImpersonationToken>,
    capture: Option<CaptureError>,
}

impl BeginError {
    pub(crate) fn rejected(
        failure: BeginFailure,
        request: EnumerationRequest,
        token: Option<ImpersonationToken>,
    ) -> Self {
        Self {
            failure,
            request,
            token,
            capture: None,
        }
    }

    pub(crate) fn capture(request: EnumerationRequest, capture: CaptureError) -> Self {
        Self {
            failure: BeginFailure::TokenCapture,
            request,
            token: None,
            capture: Some(capture),
        }
    }

    /// Why the enumeration was refused.
    #[must_use]
    pub const fn failure(&self) -> BeginFailure {
        self.failure
    }

    /// The request that was refused.
    #[must_use]
    pub const fn request(&self) -> &EnumerationRequest {
        &self.request
    }

    /// Take back the request and, when one was captured, the security context,
    /// so a retry costs neither a rebuild nor a second capture.
    #[must_use]
    pub fn into_parts(self) -> (EnumerationRequest, Option<ImpersonationToken>) {
        (self.request, self.token)
    }

    /// The capture failure behind a [`BeginFailure::TokenCapture`].
    #[must_use]
    pub const fn capture_error(&self) -> Option<&CaptureError> {
        self.capture.as_ref()
    }
}

impl fmt::Display for BeginError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.capture {
            Some(capture) => write!(f, "{}: {capture}", self.failure.describe()),
            None => f.write_str(self.failure.describe()),
        }
    }
}

impl std::error::Error for BeginError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.capture
            .as_ref()
            .map(|capture| capture as &(dyn std::error::Error + 'static))
    }
}

/// Why a session could not be built.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SessionFailure {
    /// The submission ring could not carry one enumeration.
    ///
    /// It needs room for the session's standing abandon message, one
    /// enumeration's reserved cancellation, and one ordinary begin.
    SubmissionCapacityTooSmall,
    /// The completion ring could not carry one enumeration.
    ///
    /// It needs room for one reserved terminal outcome and one entry, and
    /// reservations never take the last slot.
    CompletionCapacityTooSmall,
    /// Windows refused to create the servicer's thread-pool work object.
    WorkObject,
}

impl SessionFailure {
    const fn describe(self) -> &'static str {
        match self {
            SessionFailure::SubmissionCapacityTooSmall => {
                "the submission ring is too small to carry one enumeration"
            }
            SessionFailure::CompletionCapacityTooSmall => {
                "the completion ring is too small to carry one enumeration"
            }
            SessionFailure::WorkObject => "the servicer's work object could not be created",
        }
    }
}

/// A synchronous failure while building a session.
#[derive(Debug)]
pub struct SessionError {
    failure: SessionFailure,
    source: Option<io::Error>,
}

impl SessionError {
    pub(crate) const fn new(failure: SessionFailure) -> Self {
        Self {
            failure,
            source: None,
        }
    }

    pub(crate) const fn with_source(failure: SessionFailure, source: io::Error) -> Self {
        Self {
            failure,
            source: Some(source),
        }
    }

    /// What about the session was rejected.
    #[must_use]
    pub const fn failure(&self) -> SessionFailure {
        self.failure
    }

    /// The OS error behind the failure, when Windows produced one.
    #[must_use]
    pub const fn os_error(&self) -> Option<&io::Error> {
        self.source.as_ref()
    }
}

impl fmt::Display for SessionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.source {
            Some(source) => write!(f, "{}: {source}", self.failure.describe()),
            None => f.write_str(self.failure.describe()),
        }
    }
}

impl std::error::Error for SessionError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source
            .as_ref()
            .map(|source| source as &(dyn std::error::Error + 'static))
    }
}

/// Why a query-by-example clause was rejected.
///
/// Both cases describe a clause that would silently match everything. Rejecting
/// them turns a likely caller mistake into a reported error rather than an
/// invisible match-all.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PredicateFailure {
    /// An attribute mask was zero. Every bit of an empty mask is both set and
    /// clear, so the clause is vacuous either way round.
    EmptyAttributeMask,
    /// A name-pattern set was empty. It matches nothing, and its negation
    /// matches everything.
    EmptyNameSet,
}

impl PredicateFailure {
    const fn describe(self) -> &'static str {
        match self {
            PredicateFailure::EmptyAttributeMask => {
                "an attribute mask clause requires a non-zero mask"
            }
            PredicateFailure::EmptyNameSet => "a name-set clause requires at least one pattern",
        }
    }
}

/// A synchronous failure while building a query.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PredicateError {
    failure: PredicateFailure,
}

impl PredicateError {
    pub(crate) const fn new(failure: PredicateFailure) -> Self {
        Self { failure }
    }

    /// What about the clause was rejected.
    #[must_use]
    pub const fn failure(&self) -> PredicateFailure {
        self.failure
    }
}

impl fmt::Display for PredicateError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.failure.describe())
    }
}

impl std::error::Error for PredicateError {}

/// Which part of a native directory record failed validation.
///
/// Every variant describes a record the crate refused to read rather than one it
/// read incorrectly: the check happens before the field is touched.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MalformedRecord {
    /// The record did not start on the alignment its fixed fields require.
    Alignment,
    /// The remaining buffer was too short to hold the record's fixed fields.
    TruncatedFixedFields,
    /// The next-entry offset did not advance within the returned batch.
    NextEntryOffset,
    /// The name length was not a whole number of UTF-16 code units.
    OddNameLength,
    /// The name extended past the end of the returned batch.
    NameOutOfBounds,
    /// A native size field was negative, so it cannot be a byte count.
    NegativeSize,
}

impl MalformedRecord {
    const fn describe(self) -> &'static str {
        match self {
            MalformedRecord::Alignment => "the record is misaligned",
            MalformedRecord::TruncatedFixedFields => "the record's fixed fields are truncated",
            MalformedRecord::NextEntryOffset => "the record's next-entry offset does not advance",
            MalformedRecord::OddNameLength => {
                "the record's name length is not a whole code-unit count"
            }
            MalformedRecord::NameOutOfBounds => "the record's name extends past the batch",
            MalformedRecord::NegativeSize => "the record reports a negative size",
        }
    }
}

/// Why an accepted enumeration failed.
///
/// An enumeration reaches this only after it has been accepted, so every value
/// here arrives as the [`Failed`](crate::TerminalOutcome::Failed) terminal
/// outcome for one [`EnumerationId`](crate::EnumerationId) -- never as the
/// result of a submission call.
///
/// Clean exhaustion is deliberately absent. `ERROR_NO_MORE_FILES` from any
/// refill, and `ERROR_FILE_NOT_FOUND` from the very first one, are the two forms
/// of "this directory has no more entries" and produce
/// [`Completed`](crate::TerminalOutcome::Completed).
#[derive(Debug)]
#[non_exhaustive]
pub enum EnumerationError {
    /// The worker could not apply the submitted impersonation context, so the
    /// directory was never opened under the submitter's identity.
    Impersonation(ApplyError),
    /// The directory could not be opened. Existence, access, and
    /// not-a-directory failures all arrive here, distinguished by the raw code.
    DirectoryOpen(Win32Error),
    /// A volume serial was [`Required`](crate::FileIdentityMode::Required) and
    /// could not be obtained, so no entry could carry the globally meaningful
    /// identity the request demanded.
    VolumeIdentity(Win32Error),
    /// The filesystem does not support extended directory information.
    ///
    /// The crate does not fall back to a metadata-poorer enumeration API,
    /// because that would silently drop change time, allocation size,
    /// extended-attribute size, and the 128-bit file ID from the contract.
    UnsupportedExtendedDirectoryInfo(Win32Error),
    /// A directory-information query failed for a reason that is neither clean
    /// exhaustion, an unsupported class, nor an oversize record.
    DirectoryQuery(Win32Error),
    /// One record did not fit the request's fixed native buffer.
    ///
    /// The buffer never grows, so this is reported rather than hidden. Retry
    /// with an explicitly larger capacity.
    RecordTooLarge {
        /// The effective capacity, in bytes, that the record did not fit.
        buffer_capacity: usize,
        /// The raw code the failing refill reported.
        code: Win32Error,
    },
    /// A returned record failed validation before any of its fields were read.
    MalformedRecord(MalformedRecord),
}

impl EnumerationError {
    /// The raw Win32 code behind this failure, when one is available.
    ///
    /// [`MalformedRecord`](Self::MalformedRecord) has none -- the record was
    /// rejected by this crate, not by Windows -- and
    /// [`Impersonation`](Self::Impersonation) carries the sibling crate's typed
    /// error, whose own code is reachable through it.
    #[must_use]
    pub fn code(&self) -> Option<Win32Error> {
        match self {
            EnumerationError::Impersonation(error) => error
                .raw_os_error()
                .and_then(|code| u32::try_from(code).ok())
                .map(Win32Error::from_code),
            EnumerationError::DirectoryOpen(code)
            | EnumerationError::VolumeIdentity(code)
            | EnumerationError::UnsupportedExtendedDirectoryInfo(code)
            | EnumerationError::DirectoryQuery(code)
            | EnumerationError::RecordTooLarge { code, .. } => Some(*code),
            EnumerationError::MalformedRecord(_) => None,
        }
    }
}

impl fmt::Display for EnumerationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            EnumerationError::Impersonation(error) => {
                write!(f, "the submitted impersonation context failed: {error}")
            }
            EnumerationError::DirectoryOpen(code) => {
                write!(f, "the directory could not be opened: {code}")
            }
            EnumerationError::VolumeIdentity(code) => {
                write!(f, "the required volume identity is unavailable: {code}")
            }
            EnumerationError::UnsupportedExtendedDirectoryInfo(code) => write!(
                f,
                "extended directory information is unsupported here: {code}"
            ),
            EnumerationError::DirectoryQuery(code) => {
                write!(f, "the directory query failed: {code}")
            }
            EnumerationError::RecordTooLarge {
                buffer_capacity,
                code,
            } => write!(
                f,
                "one record exceeds the {buffer_capacity}-byte native buffer: {code}"
            ),
            EnumerationError::MalformedRecord(detail) => {
                write!(
                    f,
                    "a native record failed validation: {}",
                    detail.describe()
                )
            }
        }
    }
}

impl std::error::Error for EnumerationError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            EnumerationError::Impersonation(error) => Some(error),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests;