Skip to main content

turbo_tasks_fs/
windows.rs

1use std::path::{Path, PathBuf};
2
3/// Converts `path` into the verbatim, drive-letter-case-folded representation used internally by
4/// [`crate::DiskFileSystem`] on Windows.
5///
6/// This is the purely lexical counterpart to [`std::fs::canonicalize`]: `to_verbatim`
7/// (`GetFullPathNameW`) makes the path absolute and verbatim (`\\?\`-prefixed) but, unlike
8/// `canonicalize`, does not touch the disk — it resolves neither symlinks nor 8.3 short names. The
9/// drive letter is then upper-cased to match the form `GetFinalPathNameByHandle` (and thus
10/// `canonicalize`) produces.
11///
12/// No-op on non-Windows platforms (returns `path` unchanged).
13pub fn to_verbatim_with_case_folded_disk(path: &Path) -> std::io::Result<PathBuf> {
14    #[cfg(windows)]
15    {
16        use std::{
17            ffi::OsString,
18            os::windows::ffi::{OsStrExt, OsStringExt},
19            path::{Component, Prefix},
20        };
21
22        use omnipath::WinPathExt;
23
24        // `to_verbatim` guarantees an absolute, verbatim path from here on, so there's no
25        // non-verbatim case to guard against below.
26        let path = path.to_verbatim()?;
27
28        // Only `\\?\C:\...` (`VerbatimDisk`) paths carry a drive letter; verbatim UNC
29        // (`\\?\UNC\...`) and other verbatim device paths (`\\?\prefix`) don't, so there's nothing
30        // to case-fold for those.
31        //
32        // We can't read the letter from `VerbatimDisk(disk)` because that value is already
33        // normalized to uppercase, so it wouldn't tell us whether the underlying path needs
34        // rewriting.
35        let is_verbatim_disk = matches!(
36            path.components().next(),
37            Some(Component::Prefix(prefix)) if matches!(prefix.kind(), Prefix::VerbatimDisk(_))
38        );
39        if !is_verbatim_disk {
40            return Ok(path);
41        }
42
43        // The layout is `\\?\C:\...`, so the drive letter is the 5th UTF-16 code unit (index 4)
44        // and is guaranteed to be a-z or A-Z.
45        if let Some(disk) = path.as_os_str().encode_wide().nth(4).map(|disk| disk as u8)
46            && disk.is_ascii_lowercase()
47        {
48            // we must encode/decode because OsString's internal encoding is opaque/unstable
49            let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
50            wide[4] = u16::from(disk.to_ascii_uppercase());
51            return Ok(PathBuf::from(OsString::from_wide(&wide)));
52        }
53
54        Ok(path)
55    }
56
57    #[cfg(not(windows))]
58    Ok(path.to_path_buf())
59}
60
61#[cfg(all(test, windows))]
62mod tests {
63    use std::path::Path;
64
65    use super::*;
66
67    fn fold(path: &str) -> String {
68        to_verbatim_with_case_folded_disk(Path::new(path))
69            .unwrap()
70            .to_str()
71            .unwrap()
72            .to_owned()
73    }
74
75    #[test]
76    fn upper_cases_lower_drive_letter() {
77        // `to_verbatim` passes an already-verbatim path through unchanged, so these exercise the
78        // drive-letter case-folding in isolation.
79        assert_eq!(fold(r"\\?\c:\foo\bar"), r"\\?\C:\foo\bar");
80        assert_eq!(fold(r"\\?\z:\"), r"\\?\Z:\");
81    }
82
83    #[test]
84    fn ignores_verbatim_paths_without_a_drive() {
85        assert_eq!(
86            fold(r"\\?\UNC\server\share\foo"),
87            r"\\?\UNC\server\share\foo"
88        );
89    }
90}