Skip to main content

harn_vm/
windows_path.rs

1//! Windows extended-length (`\\?\` verbatim) path semantics, owned in one
2//! place so the `\\?\` / `\\?\UNC\` prefix rules live at a single source of
3//! truth.
4//!
5//! Two paired directions:
6//! - [`strip_windows_verbatim_prefix`] removes the prefix so a canonicalized
7//!   path parses under normal Windows rules (and can re-enter `/`-joining
8//!   code, or prefix-match a plain `C:\` mount point). It is pure string logic
9//!   and cross-platform: a well-formed absolute path off Windows never carries
10//!   the prefix, so callers on any platform may normalize a Windows-shaped
11//!   string through it.
12//! - [`wide_maybe_verbatim`] adds the prefix to a `MoveFileExW` operand so a
13//!   durable write survives a path longer than the legacy 260-char `MAX_PATH`.
14//!   It mirrors the standard library's conservative `maybe_verbatim` rules and
15//!   is Windows-only.
16
17use std::borrow::Cow;
18
19/// Strip a Windows verbatim (`\\?\`) prefix from a path string:
20/// `\\?\UNC\server\share` -> `\\server\share`, `\\?\C:\dir` -> `C:\dir`. Inputs
21/// without the prefix — every well-formed Unix path, plain Windows paths, and
22/// `\\.\` device paths — are returned unchanged.
23pub fn strip_windows_verbatim_prefix(text: &str) -> Cow<'_, str> {
24    if let Some(rest) = text.strip_prefix(r"\\?\UNC\") {
25        Cow::Owned(format!(r"\\{rest}"))
26    } else if let Some(rest) = text.strip_prefix(r"\\?\") {
27        Cow::Borrowed(rest)
28    } else {
29        Cow::Borrowed(text)
30    }
31}
32
33/// Build a NUL-terminated UTF-16 buffer for `path`, prepending the `\\?\`
34/// extended-length prefix so `MoveFileExW` accepts paths longer than the legacy
35/// 260-char `MAX_PATH`.
36///
37/// std applies this automatically for its own file APIs (`File::open`,
38/// `create_dir_all`), but a hand-rolled `MoveFileExW` call does not, so any
39/// operand over the limit fails with `ERROR_PATH_NOT_FOUND`. Mirroring std's
40/// conservative rules, only drive/UNC-absolute paths already in backslash
41/// normal form are rewritten; a verbatim/device path, a relative path, or one
42/// containing `/`, `.`, or `..` (which a verbatim prefix would treat literally)
43/// is passed through unchanged.
44#[cfg(windows)]
45pub(crate) fn wide_maybe_verbatim(path: &std::path::Path) -> Vec<u16> {
46    use std::os::windows::ffi::OsStrExt;
47    use std::path::{Component, Prefix};
48
49    fn nul_terminated(mut wide: Vec<u16>) -> Vec<u16> {
50        wide.push(0);
51        wide
52    }
53
54    let raw: Vec<u16> = path.as_os_str().encode_wide().collect();
55    const BACKSLASH: u16 = b'\\' as u16;
56    const SLASH: u16 = b'/' as u16;
57    const QUESTION: u16 = b'?' as u16;
58    const DOT: u16 = b'.' as u16;
59    // Already verbatim (`\\?\`) or a device path (`\\.\`): leave untouched.
60    if raw.starts_with(&[BACKSLASH, BACKSLASH, QUESTION, BACKSLASH])
61        || raw.starts_with(&[BACKSLASH, BACKSLASH, DOT, BACKSLASH])
62    {
63        return nul_terminated(raw);
64    }
65    // A forward slash is a literal filename character under a verbatim prefix,
66    // so such paths are ineligible.
67    if raw.contains(&SLASH) {
68        return nul_terminated(raw);
69    }
70    let mut components = path.components();
71    let is_supported_prefix = matches!(
72        components.next(),
73        Some(Component::Prefix(prefix))
74            if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::UNC(_, _))
75    );
76    if !is_supported_prefix || !matches!(components.next(), Some(Component::RootDir)) {
77        return nul_terminated(raw);
78    }
79    // `.`/`..` components would resolve literally under a verbatim prefix.
80    if components.any(|component| !matches!(component, Component::Normal(_))) {
81        return nul_terminated(raw);
82    }
83    // Re-read the prefix kind to choose the correct verbatim form.
84    let prefix_kind = path
85        .components()
86        .next()
87        .and_then(|component| match component {
88            Component::Prefix(prefix) => Some(prefix.kind()),
89            _ => None,
90        });
91    let prefixed = match prefix_kind {
92        // `C:\...` -> `\\?\C:\...`
93        Some(Prefix::Disk(_)) => {
94            let mut out: Vec<u16> = r"\\?\".encode_utf16().collect();
95            out.extend_from_slice(&raw);
96            out
97        }
98        // `\\server\share\...` -> `\\?\UNC\server\share\...` (drop one leading `\`)
99        Some(Prefix::UNC(_, _)) => {
100            let mut out: Vec<u16> = r"\\?\UNC".encode_utf16().collect();
101            out.extend_from_slice(&raw[1..]);
102            out
103        }
104        _ => raw,
105    };
106    nul_terminated(prefixed)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn strips_drive_and_unc_verbatim_prefixes() {
115        assert_eq!(
116            strip_windows_verbatim_prefix(r"\\?\C:\Users\runner\Temp\harn-abc"),
117            r"C:\Users\runner\Temp\harn-abc"
118        );
119        assert_eq!(
120            strip_windows_verbatim_prefix(r"\\?\UNC\server\share\dir"),
121            r"\\server\share\dir"
122        );
123    }
124
125    #[test]
126    fn passes_through_non_verbatim_paths_unchanged() {
127        // Plain Windows, every Unix path, `\\.\` device paths, relative paths,
128        // slash-bearing and dot paths all lack the `\\?\` prefix.
129        assert_eq!(
130            strip_windows_verbatim_prefix(r"C:\Temp\harn-abc"),
131            r"C:\Temp\harn-abc"
132        );
133        assert_eq!(
134            strip_windows_verbatim_prefix("/tmp/harn-abc/child"),
135            "/tmp/harn-abc/child"
136        );
137        assert_eq!(
138            strip_windows_verbatim_prefix(r"\\.\PhysicalDrive0"),
139            r"\\.\PhysicalDrive0"
140        );
141        assert_eq!(
142            strip_windows_verbatim_prefix(r"relative\dir"),
143            r"relative\dir"
144        );
145        assert_eq!(strip_windows_verbatim_prefix(r"C:\a\..\b"), r"C:\a\..\b");
146        assert_eq!(
147            strip_windows_verbatim_prefix("C:/forward/slash"),
148            "C:/forward/slash"
149        );
150    }
151
152    #[test]
153    fn strips_prefix_from_paths_longer_than_max_path() {
154        let deep = format!(r"\\?\C:\{}", "segment\\".repeat(40));
155        let stripped = strip_windows_verbatim_prefix(&deep);
156        assert!(stripped.len() > 260, "test path must exceed MAX_PATH");
157        #[expect(clippy::string_slice, reason = "test input is ASCII")]
158        let expected = &deep[r"\\?\".len()..];
159        assert_eq!(stripped, expected);
160        assert!(!stripped.starts_with(r"\\?\"));
161    }
162}