Skip to main content

std\sys\path/
windows.rs

1use crate::ffi::{OsStr, OsString};
2use crate::path::{Path, PathBuf};
3use crate::sys::api::utf16;
4use crate::sys::pal::{c, fill_utf16_buf, os2path, to_u16s};
5use crate::{io, ptr};
6
7#[cfg(test)]
8mod tests;
9
10pub use super::windows_prefix::parse_prefix;
11
12path_separator_bytes!(b'\\', b'/');
13
14pub const HAS_PREFIXES: bool = true;
15
16/// A null terminated wide string.
17#[repr(transparent)]
18pub struct WCStr([u16]);
19
20impl WCStr {
21    /// Convert a slice to a WCStr without checks.
22    ///
23    /// Though it is memory safe, the slice should also not contain interior nulls
24    /// as this may lead to unwanted truncation.
25    ///
26    /// # Safety
27    ///
28    /// The slice must end in a null.
29    pub unsafe fn from_wchars_with_null_unchecked(s: &[u16]) -> &Self {
30        unsafe { &*(s as *const [u16] as *const Self) }
31    }
32
33    pub fn as_ptr(&self) -> *const u16 {
34        self.0.as_ptr()
35    }
36
37    pub fn count_bytes(&self) -> usize {
38        self.0.len()
39    }
40}
41
42#[inline]
43pub fn with_native_path<T>(path: &Path, f: &dyn Fn(&WCStr) -> io::Result<T>) -> io::Result<T> {
44    let path = maybe_verbatim(path)?;
45    // SAFETY: maybe_verbatim returns null-terminated strings
46    let path = unsafe { WCStr::from_wchars_with_null_unchecked(&path) };
47    f(path)
48}
49
50#[inline]
51pub const fn is_verbatim_sep(b: u8) -> bool {
52    b == b'\\'
53}
54
55pub fn is_verbatim(path: &[u16]) -> bool {
56    path.starts_with(utf16!(r"\\?\")) || path.starts_with(utf16!(r"\??\"))
57}
58
59/// Returns true if `path` looks like a lone filename.
60pub(crate) fn is_file_name(path: &OsStr) -> bool {
61    !path.as_encoded_bytes().iter().copied().any(is_sep_byte)
62}
63pub(crate) fn has_trailing_slash(path: &OsStr) -> bool {
64    let is_verbatim = path.as_encoded_bytes().starts_with(br"\\?\");
65    let is_separator = if is_verbatim { is_verbatim_sep } else { is_sep_byte };
66    if let Some(&c) = path.as_encoded_bytes().last() { is_separator(c) } else { false }
67}
68
69/// Appends a suffix to a path.
70///
71/// Can be used to append an extension without removing an existing extension.
72pub(crate) fn append_suffix(path: PathBuf, suffix: &OsStr) -> PathBuf {
73    let mut path = OsString::from(path);
74    path.push(suffix);
75    path.into()
76}
77
78/// Returns a UTF-16 encoded path capable of bypassing the legacy `MAX_PATH` limits.
79///
80/// This path may or may not have a verbatim prefix.
81pub(crate) fn maybe_verbatim(path: &Path) -> io::Result<Vec<u16>> {
82    let path = to_u16s(path)?;
83    get_long_path(path, true)
84}
85
86/// Gets a normalized absolute path that can bypass path length limits.
87///
88/// Setting prefer_verbatim to true suggests a stronger preference for verbatim
89/// paths even when not strictly necessary. This allows the Windows API to avoid
90/// repeating our work. However, if the path may be given back to users or
91/// passed to other application then it's preferable to use non-verbatim paths
92/// when possible. Non-verbatim paths are better understood by users and handled
93/// by more software.
94pub(crate) fn get_long_path(mut path: Vec<u16>, prefer_verbatim: bool) -> io::Result<Vec<u16>> {
95    // Normally the MAX_PATH is 260 UTF-16 code units (including the NULL).
96    // However, for APIs such as CreateDirectory[1], the limit is 248.
97    //
98    // [1]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createdirectorya#parameters
99    const LEGACY_MAX_PATH: usize = 248;
100    // UTF-16 encoded code points, used in parsing and building UTF-16 paths.
101    // All of these are in the ASCII range so they can be cast directly to `u16`.
102    const SEP: u16 = b'\\' as _;
103    const ALT_SEP: u16 = b'/' as _;
104    const QUERY: u16 = b'?' as _;
105    const COLON: u16 = b':' as _;
106    const DOT: u16 = b'.' as _;
107    const U: u16 = b'U' as _;
108    const N: u16 = b'N' as _;
109    const C: u16 = b'C' as _;
110
111    // \\?\
112    const VERBATIM_PREFIX: &[u16] = &[SEP, SEP, QUERY, SEP];
113    // \??\
114    const NT_PREFIX: &[u16] = &[SEP, QUERY, QUERY, SEP];
115    // \\?\UNC\
116    const UNC_PREFIX: &[u16] = &[SEP, SEP, QUERY, SEP, U, N, C, SEP];
117
118    if path.starts_with(VERBATIM_PREFIX) || path.starts_with(NT_PREFIX) || path == [0] {
119        // Early return for paths that are already verbatim or empty.
120        return Ok(path);
121    } else if path.len() < LEGACY_MAX_PATH {
122        // Early return if an absolute path is less < 260 UTF-16 code units.
123        // This is an optimization to avoid calling `GetFullPathNameW` unnecessarily.
124        match path.as_slice() {
125            // Starts with `D:`, `D:\`, `D:/`, etc.
126            // Does not match if the path starts with a `\` or `/`.
127            [drive, COLON, 0] | [drive, COLON, SEP | ALT_SEP, ..]
128                if *drive != SEP && *drive != ALT_SEP =>
129            {
130                return Ok(path);
131            }
132            // Starts with `\\`, `//`, etc
133            [SEP | ALT_SEP, SEP | ALT_SEP, ..] => return Ok(path),
134            _ => {}
135        }
136    }
137
138    // Firstly, get the absolute path using `GetFullPathNameW`.
139    // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew
140    let lpfilename = path.as_ptr();
141    fill_utf16_buf(
142        // SAFETY: `fill_utf16_buf` ensures the `buffer` and `size` are valid.
143        // `lpfilename` is a pointer to a null terminated string that is not
144        // invalidated until after `GetFullPathNameW` returns successfully.
145        |buffer, size| unsafe { c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()) },
146        |mut absolute| {
147            path.clear();
148
149            // Only prepend the prefix if needed.
150            if prefer_verbatim || absolute.len() + 1 >= LEGACY_MAX_PATH {
151                // Secondly, add the verbatim prefix. This is easier here because we know the
152                // path is now absolute and fully normalized (e.g. `/` has been changed to `\`).
153                let prefix = match absolute {
154                    // C:\ => \\?\C:\
155                    [_, COLON, SEP, ..] => VERBATIM_PREFIX,
156                    // \\.\ => \\?\
157                    [SEP, SEP, DOT, SEP, ..] => {
158                        absolute = &absolute[4..];
159                        VERBATIM_PREFIX
160                    }
161                    // Leave \\?\ and \??\ as-is.
162                    [SEP, SEP, QUERY, SEP, ..] | [SEP, QUERY, QUERY, SEP, ..] => &[],
163                    // \\ => \\?\UNC\
164                    [SEP, SEP, ..] => {
165                        absolute = &absolute[2..];
166                        UNC_PREFIX
167                    }
168                    // Anything else we leave alone.
169                    _ => &[],
170                };
171
172                path.reserve_exact(prefix.len() + absolute.len() + 1);
173                path.extend_from_slice(prefix);
174            } else {
175                path.reserve_exact(absolute.len() + 1);
176            }
177            path.extend_from_slice(absolute);
178            path.push(0);
179        },
180    )?;
181    Ok(path)
182}
183
184/// Make a Windows path absolute.
185pub(crate) fn absolute(path: &Path) -> io::Result<PathBuf> {
186    let path = path.as_os_str();
187    let prefix = parse_prefix(path);
188    // Verbatim paths should not be modified.
189    if prefix.map(|x| x.is_verbatim()).unwrap_or(false) {
190        // NULs in verbatim paths are rejected for consistency.
191        if path.as_encoded_bytes().contains(&0) {
192            return Err(io::const_error!(
193                io::ErrorKind::InvalidInput,
194                "strings passed to WinAPI cannot contain NULs",
195            ));
196        }
197        return Ok(path.to_owned().into());
198    }
199
200    let path = to_u16s(path)?;
201    let lpfilename = path.as_ptr();
202    fill_utf16_buf(
203        // SAFETY: `fill_utf16_buf` ensures the `buffer` and `size` are valid.
204        // `lpfilename` is a pointer to a null terminated string that is not
205        // invalidated until after `GetFullPathNameW` returns successfully.
206        |buffer, size| unsafe { c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()) },
207        os2path,
208    )
209}
210
211pub(crate) fn is_absolute(path: &Path) -> bool {
212    path.has_root() && path.prefix().is_some()
213}
214
215/// Test that the path is absolute, fully qualified and unchanged when processed by the Windows API.
216///
217/// For example:
218///
219/// - `C:\path\to\file` will return true.
220/// - `C:\path\to\nul` returns false because the Windows API will convert it to \\.\NUL
221/// - `C:\path\to\..\file` returns false because it will be resolved to `C:\path\file`.
222///
223/// This is a useful property because it means the path can be converted from and to and verbatim
224/// path just by changing the prefix.
225pub(crate) fn is_absolute_exact(path: &[u16]) -> bool {
226    // This is implemented by checking that passing the path through
227    // GetFullPathNameW does not change the path in any way.
228
229    // Windows paths are limited to i16::MAX length
230    // though the API here accepts a u32 for the length.
231    if path.is_empty() || path.len() > u32::MAX as usize || path.last() != Some(&0) {
232        return false;
233    }
234    // The path returned by `GetFullPathNameW` must be the same length as the
235    // given path, otherwise they're not equal.
236    let buffer_len = path.len();
237    let mut new_path = Vec::with_capacity(buffer_len);
238    let result = unsafe {
239        c::GetFullPathNameW(
240            path.as_ptr(),
241            new_path.capacity() as u32,
242            new_path.as_mut_ptr(),
243            crate::ptr::null_mut(),
244        )
245    };
246    // Note: if non-zero, the returned result is the length of the buffer without the null termination
247    if result == 0 || result as usize != buffer_len - 1 {
248        false
249    } else {
250        // SAFETY: `GetFullPathNameW` initialized `result` bytes and does not exceed `nBufferLength - 1` (capacity).
251        unsafe {
252            new_path.set_len((result as usize) + 1);
253        }
254        path == &new_path
255    }
256}