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