-
Notifications
You must be signed in to change notification settings - Fork 35
fix(windows): preserve unsupported verbatim namespaces #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ualtinok
merged 2 commits into
cortexkit:main
from
TreyThomasCodes:fix/windows-verbatim-path-validation
Sep 8, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| //! Windows extended-length path normalization. | ||
| //! | ||
| //! `std::fs::canonicalize` returns paths in the extended-length namespace on | ||
| //! Windows. Only DOS-drive and UNC names have a safe non-verbatim spelling; | ||
| //! other namespaces (for example `\\?\Volume{GUID}\`) must retain their prefix. | ||
|
|
||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| /// Normalize a Windows path for comparisons and Win32 APIs. | ||
| /// | ||
| /// Valid DOS and UNC extended-length paths lose their verbatim prefix. Other | ||
| /// namespaces remain untouched because they cannot be represented safely | ||
| /// without that prefix. Separators and drive letter casing are also normalized. | ||
| pub fn normalize_windows_path(path: &Path) -> PathBuf { | ||
| let raw = path.to_string_lossy().replace('/', "\\"); | ||
| let mut normalized = non_verbatim_path_text(&raw).unwrap_or(raw); | ||
| if normalized.as_bytes().get(1) == Some(&b':') { | ||
| let drive = normalized.as_bytes()[0]; | ||
| if drive.is_ascii_lowercase() { | ||
| normalized.replace_range(0..1, &(drive as char).to_ascii_uppercase().to_string()); | ||
| } | ||
| } | ||
| PathBuf::from(normalized) | ||
| } | ||
|
|
||
| /// String form of the strict verbatim-prefix conversion, for APIs that require a command line | ||
| /// or URI rather than a [`Path`]. | ||
| pub fn non_verbatim_path_text(path: &str) -> Option<String> { | ||
| for prefix in [r"\\?\UNC\", r"\\??\UNC\", r"\??\UNC\"] { | ||
| if let Some(tail) = strip_ascii_prefix(path, prefix) { | ||
| if is_safe_unc_tail(tail) { | ||
| return Some(format!(r"\\{tail}")); | ||
| } | ||
| return None; | ||
| } | ||
| } | ||
|
|
||
| for prefix in [r"\\?\", r"\\??\", r"\??\"] { | ||
| if let Some(tail) = strip_ascii_prefix(path, prefix) { | ||
| let bytes = tail.as_bytes(); | ||
| if bytes.len() >= 3 | ||
| && bytes[0].is_ascii_alphabetic() | ||
| && bytes[1] == b':' | ||
| && matches!(bytes[2], b'\\' | b'/') | ||
| && !has_dot_component(tail) | ||
| { | ||
| return Some(tail.to_string()); | ||
|
TreyThomasCodes marked this conversation as resolved.
|
||
| } | ||
| return None; | ||
| } | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| fn is_safe_unc_tail(tail: &str) -> bool { | ||
| let mut components = tail.split(['\\', '/']); | ||
| components.next().is_some_and(|server| !server.is_empty()) | ||
| && components.next().is_some_and(|share| !share.is_empty()) | ||
| && !has_dot_component(tail) | ||
| } | ||
|
|
||
| fn has_dot_component(path: &str) -> bool { | ||
| path.split(['\\', '/']) | ||
| .any(|component| matches!(component, "." | "..")) | ||
| } | ||
|
|
||
| fn strip_ascii_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { | ||
| let head = value.get(..prefix.len())?; | ||
| if head.eq_ignore_ascii_case(prefix) { | ||
| value.get(prefix.len()..) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::{non_verbatim_path_text, normalize_windows_path}; | ||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| #[test] | ||
| fn converts_only_valid_dos_and_unc_verbatim_paths() { | ||
| assert_eq!( | ||
| non_verbatim_path_text(r"\\?\C:\cache\server.cmd"), | ||
| Some(r"C:\cache\server.cmd".to_string()) | ||
| ); | ||
| assert_eq!( | ||
| non_verbatim_path_text(r"\\?\unc\host\share\server.cmd"), | ||
| Some(r"\\host\share\server.cmd".to_string()) | ||
| ); | ||
| assert_eq!( | ||
| normalize_windows_path(Path::new(r"\\??\d:\repo")), | ||
| PathBuf::from(r"D:\repo") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn preserves_unsupported_or_malformed_verbatim_namespaces() { | ||
| for path in [ | ||
| r"\\?\Volume{1234}\server.cmd", | ||
| r"\\?\UNC\host", | ||
| r"\\?\UNC\\host\share", | ||
| r"\\??\UNC\\host\share", | ||
| r"\\?\UNC\host\share\..\file", | ||
| r"\\?\C:\repo\..\other", | ||
| r"\\?\C:relative", | ||
| r"\\?\relative", | ||
| r"C:\ordinary\path", | ||
| ] { | ||
| assert_eq!(non_verbatim_path_text(path), None, "{path}"); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.