Skip to main content

windows_namespace_request_sys/
anchored.rs

1// Copyright (c) Mike Grier.
2
3//! A path as the caller gave it, with a relative one fixed to the current directory of the moment
4//! the request was built (D-19).
5//!
6//! The process current directory is shared and mutable, and a request is performed later, on any
7//! thread. So a relative path is grafted onto the current directory **when the request is built**
8//! and the result is what `perform` hands to Win32, however the current directory has changed since.
9//!
10//! # The graft
11//!
12//! The captured directory, a backslash unless it already ends in one (a drive root does), then the
13//! child. Nothing is combined, canonicalised, length-checked or prefixed with `\\?\`:
14//! `CreateFileW` normalises `.`, `..` and trailing dots and spaces on the final path itself, and
15//! Win32 decides whether a long result is acceptable to the host.
16//!
17//! # Only a clearly safe child is grafted
18//!
19//! All of these must hold, and a child that fails any of them is kept exactly as given, for Win32 to
20//! resolve when the call is made:
21//!
22//! 1. it is not empty;
23//! 2. it does not begin with `\` or `/`, which rules out root-relative, UNC, `\\?\` and `\\.\`;
24//! 3. it contains no `:`, which rules out drive-relative `C:a` and `C:` and a stream `name:stream`;
25//! 4. if the captured directory is a UNC path, the child has no `..` component;
26//! 5. no component of the child is a reserved device name: `CON`, `PRN`, `AUX`, `NUL`, `COM` or
27//!    `LPT` with a digit, `CONIN$` or `CONOUT$`, compared without regard to ASCII case, after
28//!    removing everything from the first `.` and any trailing spaces;
29//! 6. the captured directory does not begin with `\\?\` or `\\.\`. A path with either prefix is not
30//!    normalised by Win32, so a child joined onto one would be taken literally, `.` and `..`
31//!    components, forward slashes and trailing dots and spaces among what would then mean something
32//!    else than they meant relative to that directory.
33//!
34//! The rules err toward not grafting. Where they are wider than Microsoft's list, the child is
35//! passed through, which is what happens to every form that is not grafted. The digits of rule 5
36//! are `0` to `9` and the superscripts `U+00B9`, `U+00B2` and `U+00B3` that the documentation also
37//! names. A directory that begins with two separators of either kind counts as UNC for rule 4.
38//!
39//! The current directory is read only when it is needed: a path that rules 1, 2, 3 and 5 already
40//! refuse makes no call. Rules 4 and 6 are about the directory itself, so they are decided after it
41//! is read.
42//!
43//! # The one thing refused
44//!
45//! An interior NUL, which a `PCWSTR` cannot carry: Win32 would read the path as ending there and
46//! act on a shorter path than the caller named. It is refused when the request is built, where the
47//! caller can still act on it, and so is a failure to read the current directory. Nothing else about
48//! a path is checked here.
49
50use std::fmt;
51
52use windows_sys::Win32::System::Environment::GetCurrentDirectoryW;
53use wtf_string::{Wtf16Str, Wtf16String};
54
55use crate::outcome::{Win32Error, perform_nonzero};
56
57/// How many times the buffer is grown before giving up, as in [`crate::full_path`].
58const MAX_ATTEMPTS: usize = 8;
59
60/// The buffer size the first attempt uses, in characters.
61const FIRST_ATTEMPT_CHARS: usize = 260;
62
63const BACKSLASH: u16 = b'\\' as u16;
64const SLASH: u16 = b'/' as u16;
65const COLON: u16 = b':' as u16;
66const DOT: u16 = b'.' as u16;
67const QUESTION: u16 = b'?' as u16;
68const SPACE: u16 = b' ' as u16;
69const DIGIT_ZERO: u16 = b'0' as u16;
70const DIGIT_NINE: u16 = b'9' as u16;
71
72/// The superscript one, two and three, which Microsoft's list also names after `COM` and `LPT`.
73const SUPERSCRIPT_DIGITS: [u16; 3] = [0x00B9, 0x00B2, 0x00B3];
74
75/// The names that are reserved whole. Changing one changes which children are grafted.
76const RESERVED_NAMES: [&[u8]; 6] = [b"CON", b"PRN", b"AUX", b"NUL", b"CONIN$", b"CONOUT$"];
77
78/// The names that are reserved with a digit after them.
79const RESERVED_NAMES_WITH_DIGIT: [&[u8]; 2] = [b"COM", b"LPT"];
80
81/// Why a path could not be captured for a request.
82///
83/// The failures are the two a path can have at construction: it cannot be carried to Win32 as it
84/// is, or the current directory it is to be fixed to could not be read. The second mirrors
85/// [`crate::full_path::FullPathError`], which shares the retry shape and so the vocabulary.
86#[derive(Debug)]
87#[non_exhaustive]
88pub enum PathCaptureError {
89    /// The path contains a NUL before its end. Win32 reads a path up to its first NUL, so it would
90    /// act on a shorter path than the one named.
91    InteriorNul,
92    /// `GetCurrentDirectoryW` was refused, with the raw code unaltered.
93    CurrentDirectory(Win32Error),
94    /// The size of the current directory kept changing, so the retry was abandoned.
95    CurrentDirectoryUnstable {
96        /// How many attempts were made before giving up.
97        attempts: usize,
98    },
99}
100
101impl fmt::Display for PathCaptureError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::InteriorNul => f.write_str("the path contains an interior NUL"),
105            Self::CurrentDirectory(error) => write!(f, "GetCurrentDirectoryW: {error}"),
106            Self::CurrentDirectoryUnstable { attempts } => write!(
107                f,
108                "GetCurrentDirectoryW: the required size changed on each of {attempts} attempts"
109            ),
110        }
111    }
112}
113
114impl std::error::Error for PathCaptureError {
115    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
116        match self {
117            Self::CurrentDirectory(error) => Some(error),
118            Self::InteriorNul | Self::CurrentDirectoryUnstable { .. } => None,
119        }
120    }
121}
122
123/// A path as given, and the path to hand to Win32 for it.
124///
125/// The two are the same unless the path was grafted.
126#[derive(Clone, Debug, PartialEq, Eq, Hash)]
127pub(crate) struct AnchoredPath {
128    given: Wtf16String,
129    grafted: Option<Wtf16String>,
130}
131
132impl AnchoredPath {
133    /// Anchors `given` to the process current directory as it is now.
134    ///
135    /// # Errors
136    ///
137    /// [`PathCaptureError::InteriorNul`] for a path with a NUL before its end, and otherwise the
138    /// failure of `GetCurrentDirectoryW`, which is called only for a path whose shape permits a
139    /// graft.
140    pub(crate) fn new(given: Wtf16String) -> Result<Self, PathCaptureError> {
141        if given.has_interior_nul() {
142            return Err(PathCaptureError::InteriorNul);
143        }
144        if !has_graftable_shape(given.as_units()) {
145            return Ok(Self {
146                given,
147                grafted: None,
148            });
149        }
150        let root = capture_current_directory()?;
151        Ok(Self::anchored_at(given, &root))
152    }
153
154    /// Anchors `given` to `root`, which stands for the current directory.
155    ///
156    /// No call is made, so what the rules do is testable without touching process state.
157    pub(crate) fn anchored_at(given: Wtf16String, root: &Wtf16Str) -> Self {
158        let grafted =
159            graft(given.as_units(), root.as_units()).map(|units| Wtf16String::from_units(&units));
160        Self { given, grafted }
161    }
162
163    /// The path exactly as the caller gave it.
164    #[must_use]
165    pub(crate) fn given(&self) -> &Wtf16Str {
166        &self.given
167    }
168
169    /// The path that is handed to Win32.
170    #[must_use]
171    pub(crate) fn win32(&self) -> &Wtf16Str {
172        self.grafted.as_ref().unwrap_or(&self.given)
173    }
174
175    /// Whether the path was grafted onto the captured directory.
176    #[cfg(test)]
177    pub(crate) fn is_grafted(&self) -> bool {
178        self.grafted.is_some()
179    }
180
181    /// A NUL-terminated pointer to [`win32`](Self::win32), for a Win32 call.
182    ///
183    /// Borrows from this value and must not outlive it.
184    pub(crate) fn win32_terminated(&self) -> *const u16 {
185        self.grafted
186            .as_ref()
187            .unwrap_or(&self.given)
188            .as_terminated_ptr()
189    }
190}
191
192/// The root joined to the child, when the child may be grafted onto that root.
193fn graft(child: &[u16], root: &[u16]) -> Option<Vec<u16>> {
194    if root.is_empty() || !has_graftable_shape(child) {
195        return None;
196    }
197    if is_verbatim_or_device(root) {
198        return None;
199    }
200    if is_unc(root) && has_parent_component(child) {
201        return None;
202    }
203
204    let needs_separator = root.last() != Some(&BACKSLASH);
205    let mut joined = Vec::with_capacity(root.len() + 1 + child.len());
206    joined.extend_from_slice(root);
207    if needs_separator {
208        joined.push(BACKSLASH);
209    }
210    joined.extend_from_slice(child);
211    Some(joined)
212}
213
214/// Rules 1, 2, 3 and 5: what can be decided without the directory.
215fn has_graftable_shape(child: &[u16]) -> bool {
216    let Some(first) = child.first() else {
217        return false;
218    };
219    if is_separator(*first) || child.contains(&COLON) {
220        return false;
221    }
222    !components(child).any(is_reserved_device_name)
223}
224
225/// Rule 6: a directory with the `\\?\` or `\\.\` prefix, which only backslashes make.
226fn is_verbatim_or_device(root: &[u16]) -> bool {
227    matches!(root, [BACKSLASH, BACKSLASH, prefix, BACKSLASH, ..] if *prefix == QUESTION || *prefix == DOT)
228}
229
230/// Rule 4's two halves: a directory that is a UNC path, and a child with a `..` component.
231fn is_unc(root: &[u16]) -> bool {
232    matches!(root, [first, second, ..] if is_separator(*first) && is_separator(*second))
233}
234
235fn has_parent_component(child: &[u16]) -> bool {
236    components(child).any(|component| component == [DOT, DOT])
237}
238
239fn is_separator(unit: u16) -> bool {
240    unit == BACKSLASH || unit == SLASH
241}
242
243fn components(path: &[u16]) -> impl Iterator<Item = &[u16]> {
244    path.split(|unit| is_separator(*unit))
245}
246
247/// Rule 5, for one component.
248fn is_reserved_device_name(component: &[u16]) -> bool {
249    let stem = component.split(|unit| *unit == DOT).next().unwrap_or(&[]);
250    let end = stem
251        .iter()
252        .rposition(|unit| *unit != SPACE)
253        .map_or(0, |last| last + 1);
254    let stem = &stem[..end];
255
256    if RESERVED_NAMES.iter().any(|name| equals_ascii(stem, name)) {
257        return true;
258    }
259    let Some((digit, name)) = stem.split_last() else {
260        return false;
261    };
262    let is_digit = (DIGIT_ZERO..=DIGIT_NINE).contains(digit) || SUPERSCRIPT_DIGITS.contains(digit);
263    is_digit
264        && RESERVED_NAMES_WITH_DIGIT
265            .iter()
266            .any(|reserved| equals_ascii(name, reserved))
267}
268
269/// Whether `units` is `name`, ignoring ASCII case. A unit that is not ASCII never matches.
270fn equals_ascii(units: &[u16], name: &[u8]) -> bool {
271    units.len() == name.len()
272        && units.iter().zip(name).all(|(unit, expected)| {
273            u8::try_from(*unit).is_ok_and(|unit| unit.eq_ignore_ascii_case(expected))
274        })
275}
276
277fn capture_current_directory() -> Result<Wtf16String, PathCaptureError> {
278    read_current_directory(|buffer| {
279        let capacity = u32::try_from(buffer.len()).unwrap_or(u32::MAX);
280        perform_nonzero(|| {
281            // SAFETY: the buffer is writable for `capacity` characters.
282            unsafe { GetCurrentDirectoryW(capacity, buffer.as_mut_ptr()) }
283        })
284    })
285}
286
287/// The retry around the call, with the call as a parameter so that its answers can be chosen.
288fn read_current_directory(
289    mut call: impl FnMut(&mut [u16]) -> Result<u32, Win32Error>,
290) -> Result<Wtf16String, PathCaptureError> {
291    let mut buffer = vec![0_u16; FIRST_ATTEMPT_CHARS];
292
293    for _ in 0..MAX_ATTEMPTS {
294        let written = call(&mut buffer).map_err(PathCaptureError::CurrentDirectory)? as usize;
295        if written < buffer.len() {
296            // Success: `written` excludes the terminator.
297            return Ok(Wtf16String::from_units(&buffer[..written]));
298        }
299        // Too small: `written` is the size required including the terminator.
300        buffer.resize(written, 0);
301    }
302
303    Err(PathCaptureError::CurrentDirectoryUnstable {
304        attempts: MAX_ATTEMPTS,
305    })
306}
307
308#[cfg(test)]
309mod tests;