path-uri: reject reserved Windows device names

This commit is contained in:
Adam Perry
2026-06-13 04:58:43 +00:00
parent 67e9065c0e
commit cb801e5199
2 changed files with 34 additions and 0 deletions

View File

@@ -336,6 +336,19 @@ fn is_valid_windows_component(component: &str) -> bool {
.chars()
.any(|character| character <= '\u{1f}' || r#"<>:"/\|?*"#.contains(character))
&& !component.ends_with([' ', '.'])
&& !is_reserved_windows_component(component)
}
fn is_reserved_windows_component(component: &str) -> bool {
let stem = component.split('.').next().unwrap_or(component);
let stem = stem.to_ascii_uppercase();
matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL" | "CLOCK$")
|| stem.strip_prefix("COM").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
})
|| stem.strip_prefix("LPT").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
})
}
fn reject_opaque_base(path: &PathUri) -> Result<(), ApiPathStringError> {

View File

@@ -493,3 +493,24 @@ fn relative_resolution_rejects_incompatible_and_opaque_bases() {
Ok(PathUri::parse("file:///tmp").expect("absolute URI"))
);
}
#[test]
fn native_resolution_rejects_reserved_windows_device_names() {
let base = PathUri::parse("file:///C:/workspace").expect("Windows URI");
for path in [
r"C:\AUX",
r"C:\prn.txt",
r"C:\COM9.log",
r"C:\lpt1",
r"child\NUL.txt",
] {
assert!(
matches!(
base.resolve_native(path, PathConvention::Windows),
Err(ApiPathStringError::InvalidNativePath { .. })
),
"expected reserved Windows path to fail: {path}"
);
}
}