From cb801e51990d8402ff7fb56df3c1bbb8cb89fe11 Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Sat, 13 Jun 2026 04:58:43 +0000 Subject: [PATCH] path-uri: reject reserved Windows device names --- .../utils/path-uri/src/api_path_string.rs | 13 ++++++++++++ .../path-uri/src/api_path_string_tests.rs | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index 6876f56e28..ddb142b904 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -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> { diff --git a/codex-rs/utils/path-uri/src/api_path_string_tests.rs b/codex-rs/utils/path-uri/src/api_path_string_tests.rs index 73f4056665..1154ca84b3 100644 --- a/codex-rs/utils/path-uri/src/api_path_string_tests.rs +++ b/codex-rs/utils/path-uri/src/api_path_string_tests.rs @@ -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}" + ); + } +}