Skip to main content

windows_namespace_request_sys/
path.rs

1// Copyright (c) Mike Grier.
2// Copied from windows-file-enumeration-sys/src/path.rs at 126eb5f.
3
4//! The request path contract: what a caller may name, and what gets stored.
5//!
6//! # Duplicated on purpose, for now
7//!
8//! This is a second copy of the preparation that ships in
9//! `windows-file-enumeration-sys`, not a replacement for it. That crate is
10//! released and this one is not, so making it depend here would make it
11//! unpublishable; the copy keeps the working crate untouched while this one is
12//! proven. The de-duplication happens after this branch merges with `main`, and
13//! is scheduled as a checklist item -- it is not a duplicate that nobody
14//! circled back to.
15//!
16//! # A resolved path is not a session-independent path
17//!
18//! `GetFullPathNameW` is **lexical**. It resolves relative components and
19//! `.`/`..` and never expands a drive letter, and a drive letter resolves
20//! against the *logon session* of whatever token is in effect. So a path
21//! prepared on a submitting thread and opened on a worker under a captured
22//! token from another session can name a different device. Preparation closes
23//! the current-directory race; it does not close that one, and nothing here
24//! should be read as implying otherwise.
25//!
26//! A request resolves its path **when it is built**, on the submitting thread.
27//! Deferring that to a worker would let the meaning of a relative path change
28//! between submission and execution, because the process current directory is
29//! shared mutable state that nothing in this crate controls. Resolving early
30//! also separates the two concerns cleanly: string resolution happens here, and
31//! the privileged open happens later under the captured token.
32//!
33//! # Two path families
34//!
35//! A `\\?\` path is *verbatim*: Win32 disables path parsing for it, so the crate
36//! stores it code unit for code unit. It is checked for full qualification --
37//! the one property the prefix promises and a caller can get wrong -- and
38//! otherwise left alone. Trailing separators and `.`/`..` components are
39//! preserved, because in verbatim form they are literal name components rather
40//! than syntax.
41//!
42//! Everything else, including `\\.\` device paths, goes through
43//! `GetFullPathNameW`. Those forms *are* normalised by Win32, so resolving them
44//! here produces exactly the path a later `CreateFileW` would have used.
45//!
46//! # Why ordinary paths stop at `MAX_PATH`
47//!
48//! Whether `CreateFileW` accepts a longer ordinary path depends on the host
49//! executable's `longPathAware` manifest and on system policy -- neither of
50//! which this crate controls, and both of which belong to whoever *embeds* it.
51//! Letting them decide would make the same call succeed in one host and fail in
52//! another. The crate instead draws the line itself: ordinary paths stop at
53//! `MAX_PATH`, and a caller who wants a longer one says so explicitly with a
54//! fully qualified `\\?\` path, which has never depended on the manifest.
55
56use std::fmt;
57use std::io;
58
59use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
60use wtf_string::{Wtf16Str, Wtf16String};
61
62/// `MAX_PATH`: the ordinary Win32 path limit, counting the terminator.
63const MAX_PATH: usize = 260;
64
65/// The longest ordinary path content, excluding the terminator.
66const MAX_PATH_CONTENT: usize = MAX_PATH - 1;
67
68/// The Win32 verbatim prefix, `\\?\`.
69const VERBATIM_PREFIX: [u16; 4] = [b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16];
70
71/// The `UNC\` component that follows the verbatim prefix for a network path.
72const VERBATIM_UNC: [u16; 4] = [b'U' as u16, b'N' as u16, b'C' as u16, b'\\' as u16];
73
74const BACKSLASH: u16 = b'\\' as u16;
75const COLON: u16 = b':' as u16;
76
77/// Why a caller's path could not be prepared.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79#[non_exhaustive]
80pub enum PathFailure {
81    /// The path had no code units. An empty path names nothing.
82    EmptyPath,
83    /// The path contained an interior NUL. Win32 would stop at it and open a
84    /// different, shorter path than the caller named.
85    InteriorNul,
86    /// An ordinary path, or the fully qualified form it resolved to, did not
87    /// fit the ordinary `MAX_PATH` limit including its terminator.
88    ///
89    /// This limit is deliberate rather than incidental: it keeps behaviour
90    /// independent of the host executable's `longPathAware` manifest. Supply a
91    /// fully qualified `\?\` path to name a longer one.
92    PathTooLong,
93    /// A `\?\` path was not fully qualified, so Win32 would not interpret it
94    /// as the verbatim absolute path that prefix promises.
95    NotFullyQualified,
96    /// Windows could not resolve an ordinary path to its fully qualified form.
97    PathResolution,
98}
99
100impl PathFailure {
101    /// A short description of the failure, without any raw code.
102    #[must_use]
103    pub fn description(self) -> &'static str {
104        match self {
105            Self::EmptyPath => "the path was empty",
106            Self::InteriorNul => "the path contained an interior NUL",
107            Self::PathTooLong => "the path exceeded MAX_PATH",
108            Self::NotFullyQualified => "the verbatim path was not fully qualified",
109            Self::PathResolution => "the path could not be resolved",
110        }
111    }
112}
113
114/// A synchronous failure while preparing a caller's path.
115///
116/// Preparation happens where the caller is, so this is reported at
117/// construction rather than from the thread that would later have opened the
118/// path.
119#[derive(Debug)]
120pub struct PathError {
121    failure: PathFailure,
122    source: Option<io::Error>,
123}
124
125impl PathError {
126    fn new(failure: PathFailure) -> Self {
127        Self {
128            failure,
129            source: None,
130        }
131    }
132
133    fn with_last_os(failure: PathFailure) -> Self {
134        Self {
135            failure,
136            source: Some(io::Error::last_os_error()),
137        }
138    }
139
140    /// What about the path was rejected.
141    #[must_use]
142    pub fn failure(&self) -> PathFailure {
143        self.failure
144    }
145
146    /// The raw Win32 code behind the failure, when Windows produced one.
147    ///
148    /// Only [`PathFailure::PathResolution`] arises from a Win32 call; the other
149    /// failures are decided here before any call is made.
150    #[must_use]
151    pub fn raw_os_error(&self) -> Option<i32> {
152        self.source.as_ref().and_then(io::Error::raw_os_error)
153    }
154}
155
156impl fmt::Display for PathError {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match &self.source {
159            Some(source) => write!(f, "{}: {source}", self.failure.description()),
160            None => f.write_str(self.failure.description()),
161        }
162    }
163}
164
165impl std::error::Error for PathError {
166    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
167        self.source
168            .as_ref()
169            .map(|source| source as &(dyn std::error::Error + 'static))
170    }
171}
172
173/// A path that has been through [`prepare`]: the exact path a worker will open.
174///
175/// # This is not a session-independent path
176///
177/// Preparation resolves against the *process* current directory, on the
178/// calling thread, which is what stops the meaning of a relative path changing
179/// between submission and execution. It does **not** expand a drive letter,
180/// because `GetFullPathNameW` is lexical and never does -- and a drive letter
181/// is resolved against the logon session of the token in effect at open time.
182/// A prepared path carried to a worker running under a captured token from
183/// another logon session can therefore still name a different device. That
184/// hazard is open at the workspace level; this type inherits it rather than
185/// resolving it.
186///
187/// # Example
188///
189/// An ordinary path is resolved to its fully qualified form here, on the
190/// calling thread, so the meaning cannot change before a worker opens it:
191///
192/// ```
193/// use windows_namespace_request_sys::prepare;
194/// use wtf_string::Wtf16String;
195///
196/// let prepared = prepare(&Wtf16String::from(r"C:\Windows\.\System32"))?;
197///
198/// // `.` is resolved away, exactly as a later CreateFileW would have done.
199/// assert_eq!(prepared.as_wtf16().to_string_lossy(), r"C:\Windows\System32");
200/// # Ok::<(), Box<dyn std::error::Error>>(())
201/// ```
202///
203/// # Example: a verbatim path is kept exactly
204///
205/// Win32 disables path parsing for a `\?\` path, so trailing separators and
206/// `.` components are literal name components rather than syntax. Preparation
207/// checks it is fully qualified and otherwise leaves it alone:
208///
209/// ```
210/// use windows_namespace_request_sys::prepare;
211/// use wtf_string::Wtf16String;
212///
213/// let verbatim = prepare(&Wtf16String::from(r"\\?\C:\Windows\"))?;
214/// assert_eq!(verbatim.as_wtf16().to_string_lossy(), r"\\?\C:\Windows\");
215/// # Ok::<(), Box<dyn std::error::Error>>(())
216/// ```
217#[derive(Clone, Debug, PartialEq, Eq, Hash)]
218pub struct PreparedPath {
219    units: Wtf16String,
220}
221
222impl PreparedPath {
223    /// The prepared path's code units.
224    #[must_use]
225    pub fn as_wtf16(&self) -> &Wtf16Str {
226        &self.units
227    }
228
229    /// Releases the prepared path's owned string.
230    #[must_use]
231    pub fn into_wtf16(self) -> Wtf16String {
232        self.units
233    }
234
235    /// A NUL-terminated pointer to the path, for passing to a Win32 call.
236    ///
237    /// Borrows from this value and must not outlive it. Preparation rejects an
238    /// interior NUL, so the terminator is unambiguous.
239    pub(crate) fn as_wtf16_terminated(&self) -> *const u16 {
240        self.units.as_terminated_ptr()
241    }
242}
243
244/// Validate and, where the contract calls for it, resolve a caller's path.
245///
246/// The returned value is the exact path a worker will later open, subject to
247/// the session hazard [`PreparedPath`] documents.
248///
249/// # Errors
250///
251/// Returns [`PathError`] for an empty path, an interior NUL, a `\?\` path
252/// that is not fully qualified, an ordinary path that exceeds `MAX_PATH` before
253/// or after resolution, or a resolution failure reported by Windows.
254///
255/// # Example
256///
257/// Each rejection names what was wrong, on the calling thread, rather than
258/// producing a path that fails later with a code that explains nothing:
259///
260/// ```
261/// use windows_namespace_request_sys::path::PathFailure;
262/// use windows_namespace_request_sys::prepare;
263/// use wtf_string::Wtf16String;
264///
265/// let empty = prepare(&Wtf16String::new()).expect_err("an empty path names nothing");
266/// assert_eq!(empty.failure(), PathFailure::EmptyPath);
267///
268/// // A verbatim path that is not fully qualified cannot be repaired later,
269/// // because Win32 will not parse it.
270/// // A drive-RELATIVE verbatim path is refused: verbatim parsing would
271/// // treat the whole thing as a literal name rather than the current
272/// // directory on C:, and that cannot be repaired later.
273/// let drive_relative = prepare(&Wtf16String::from(r"\\?\C:relative\path"))
274///     .expect_err("a drive-relative verbatim path is not fully qualified");
275/// assert_eq!(drive_relative.failure(), PathFailure::NotFullyQualified);
276///
277/// // These are decided here, before any Win32 call, so they carry no OS code.
278/// assert_eq!(empty.raw_os_error(), None);
279/// ```
280pub fn prepare(path: &Wtf16Str) -> Result<PreparedPath, PathError> {
281    prepare_units(path).map(|units| PreparedPath { units })
282}
283
284fn prepare_units(path: &Wtf16Str) -> Result<Wtf16String, PathError> {
285    if path.is_empty() {
286        return Err(PathError::new(PathFailure::EmptyPath));
287    }
288    if path.has_interior_nul() {
289        return Err(PathError::new(PathFailure::InteriorNul));
290    }
291
292    let units = path.as_units();
293    if units.starts_with(&VERBATIM_PREFIX) {
294        validate_verbatim(&units[VERBATIM_PREFIX.len()..])?;
295        return Ok(Wtf16String::from_units(units));
296    }
297
298    if units.len() > MAX_PATH_CONTENT {
299        return Err(PathError::new(PathFailure::PathTooLong));
300    }
301    resolve(path)
302}
303
304/// Check that a `\\?\` path names an absolute root.
305///
306/// `rest` is everything after the prefix. Win32 will not parse this path, so it
307/// must already be the absolute form: a relative or rootless verbatim path
308/// cannot be repaired later and would fail at open with a code that says nothing
309/// about why.
310fn validate_verbatim(rest: &[u16]) -> Result<(), PathError> {
311    let not_qualified = || PathError::new(PathFailure::NotFullyQualified);
312
313    if rest.starts_with(&VERBATIM_UNC) {
314        // `\\?\UNC\server\share`: both components must be present and non-empty,
315        // because a server with no share names no filesystem to enumerate.
316        let after_unc = &rest[VERBATIM_UNC.len()..];
317        let Some(separator) = after_unc.iter().position(|unit| *unit == BACKSLASH) else {
318            return Err(not_qualified());
319        };
320        let server = &after_unc[..separator];
321        let share = &after_unc[separator + 1..];
322        let share_len = share
323            .iter()
324            .position(|unit| *unit == BACKSLASH)
325            .unwrap_or(share.len());
326        if server.is_empty() || share_len == 0 {
327            return Err(not_qualified());
328        }
329        return Ok(());
330    }
331
332    // Any other verbatim form needs a non-empty root component followed by a
333    // separator: `\\?\C:\`, `\\?\Volume{...}\`, and so on.
334    let Some(separator) = rest.iter().position(|unit| *unit == BACKSLASH) else {
335        return Err(not_qualified());
336    };
337    let root = &rest[..separator];
338    if root.is_empty() {
339        return Err(not_qualified());
340    }
341    // A DOS drive root is spelled exactly `X:`. Rejecting `\\?\C:foo` matters
342    // because that is drive-*relative*: verbatim parsing would treat the whole
343    // thing as a literal name rather than the current directory on C:.
344    if root.contains(&COLON) && !is_drive_designator(root) {
345        return Err(not_qualified());
346    }
347    Ok(())
348}
349
350/// Whether `root` is exactly an ASCII drive designator such as `C:`.
351fn is_drive_designator(root: &[u16]) -> bool {
352    let [letter, colon] = root else {
353        return false;
354    };
355    *colon == COLON && u8::try_from(*letter).is_ok_and(|byte| byte.is_ascii_alphabetic())
356}
357
358/// Resolve an ordinary path against the current directory, as Win32 would.
359fn resolve(path: &Wtf16Str) -> Result<Wtf16String, PathError> {
360    // `Wtf16Str` is a borrowed slice with no terminator, so the input is copied
361    // into an owned value first: `GetFullPathNameW` takes a `PCWSTR`.
362    let input = Wtf16String::from_units(path.as_units());
363    let mut resolved = Wtf16String::with_capacity(MAX_PATH);
364    // SAFETY: `input` has no interior NUL (checked by `prepare`) so its
365    // terminated pointer is a valid C string, and `resolved` has room for
366    // `MAX_PATH` content units plus the terminator this call writes. The
367    // buffer is not observed through any other method until `set_len_from_ffi`
368    // below restores the always-terminated invariant.
369    let written = unsafe {
370        GetFullPathNameW(
371            input.as_terminated_ptr(),
372            MAX_PATH as u32,
373            resolved.as_mut_ptr(),
374            core::ptr::null_mut(),
375        )
376    };
377
378    if written == 0 {
379        let failure = PathError::with_last_os(PathFailure::PathResolution);
380        // The buffer was handed to Win32 and may hold anything; restore the
381        // empty-string invariant before the value is dropped or observed.
382        // SAFETY: zero content units are trivially initialised and within
383        // capacity.
384        unsafe { resolved.set_len_from_ffi(0) };
385        return Err(failure);
386    }
387
388    let written = written as usize;
389    // A return at or above the buffer size is the "needed this much including
390    // the terminator" form, which here can only mean the resolved path does not
391    // fit the ordinary limit. Nothing usable was written, so the invariant is
392    // restored the same way as on failure.
393    if written > MAX_PATH_CONTENT {
394        // SAFETY: as above.
395        unsafe { resolved.set_len_from_ffi(0) };
396        return Err(PathError::new(PathFailure::PathTooLong));
397    }
398
399    // SAFETY: `GetFullPathNameW` wrote `written` content units plus its own
400    // terminator, and `written` is within the requested capacity.
401    unsafe { resolved.set_len_from_ffi(written) };
402    if resolved.is_empty() {
403        // Defensive: a non-zero return with no content would leave a path that
404        // names nothing, which must not reach a worker.
405        return Err(PathError::new(PathFailure::PathResolution));
406    }
407    Ok(resolved)
408}
409
410#[cfg(test)]
411mod tests;