diff --git a/codex-rs/tui/src/clipboard_paste.rs b/codex-rs/tui/src/clipboard_paste.rs index 5863c728b09..4d28b365fed 100644 --- a/codex-rs/tui/src/clipboard_paste.rs +++ b/codex-rs/tui/src/clipboard_paste.rs @@ -244,9 +244,14 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag /// - shell-escaped single paths (via `shlex`) pub fn normalize_pasted_path(pasted: &str) -> Option { let pasted = pasted.trim(); + let unquoted = pasted + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .or_else(|| pasted.strip_prefix('\'').and_then(|s| s.strip_suffix('\''))) + .unwrap_or(pasted); // file:// URL → filesystem path - if let Ok(url) = url::Url::parse(pasted) + if let Ok(url) = url::Url::parse(unquoted) && url.scheme() == "file" { return url.to_file_path().ok(); @@ -258,38 +263,18 @@ pub fn normalize_pasted_path(pasted: &str) -> Option { // Detect unquoted Windows paths and bypass POSIX shlex which // treats backslashes as escapes (e.g., C:\Users\Alice\file.png). // Also handles UNC paths (\\server\share\path). - let looks_like_windows_path = { - // Drive letter path: C:\ or C:/ - let drive = pasted - .chars() - .next() - .map(|c| c.is_ascii_alphabetic()) - .unwrap_or(false) - && pasted.get(1..2) == Some(":") - && pasted - .get(2..3) - .map(|s| s == "\\" || s == "/") - .unwrap_or(false); - // UNC path: \\server\share - let unc = pasted.starts_with("\\\\"); - drive || unc - }; - if looks_like_windows_path { - #[cfg(target_os = "linux")] - { - if is_probably_wsl() - && let Some(converted) = convert_windows_path_to_wsl(pasted) - { - return Some(converted); - } - } - return Some(PathBuf::from(pasted)); + if let Some(path) = normalize_windows_path(unquoted) { + return Some(path); } // shell-escaped single path → unescaped let parts: Vec = shlex::Shlex::new(pasted).collect(); if parts.len() == 1 { - return parts.into_iter().next().map(PathBuf::from); + let part = parts.into_iter().next()?; + if let Some(path) = normalize_windows_path(&part) { + return Some(path); + } + return Some(PathBuf::from(part)); } None @@ -339,6 +324,36 @@ fn convert_windows_path_to_wsl(input: &str) -> Option { Some(result) } +fn normalize_windows_path(input: &str) -> Option { + // Drive letter path: C:\ or C:/ + let drive = input + .chars() + .next() + .map(|c| c.is_ascii_alphabetic()) + .unwrap_or(false) + && input.get(1..2) == Some(":") + && input + .get(2..3) + .map(|s| s == "\\" || s == "/") + .unwrap_or(false); + // UNC path: \\server\share + let unc = input.starts_with("\\\\"); + if !drive && !unc { + return None; + } + + #[cfg(target_os = "linux")] + { + if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(input) + { + return Some(converted); + } + } + + Some(PathBuf::from(input)) +} + /// Infer an image format for the provided path based on its extension. pub fn pasted_image_format(path: &Path) -> EncodedImageFormat { match path @@ -438,9 +453,39 @@ mod pasted_paths_tests { #[test] fn normalize_single_quoted_windows_path() { let input = r"'C:\\Users\\Alice\\My File.jpeg'"; + let unquoted = r"C:\\Users\\Alice\\My File.jpeg"; let result = normalize_pasted_path(input).expect("should trim single quotes on windows path"); - assert_eq!(result, PathBuf::from(r"C:\\Users\\Alice\\My File.jpeg")); + #[cfg(target_os = "linux")] + let expected = if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(unquoted) + { + converted + } else { + PathBuf::from(unquoted) + }; + #[cfg(not(target_os = "linux"))] + let expected = PathBuf::from(unquoted); + assert_eq!(result, expected); + } + + #[test] + fn normalize_double_quoted_windows_path() { + let input = r#""C:\\Users\\Alice\\My File.jpeg""#; + let unquoted = r"C:\\Users\\Alice\\My File.jpeg"; + let result = + normalize_pasted_path(input).expect("should trim double quotes on windows path"); + #[cfg(target_os = "linux")] + let expected = if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(unquoted) + { + converted + } else { + PathBuf::from(unquoted) + }; + #[cfg(not(target_os = "linux"))] + let expected = PathBuf::from(unquoted); + assert_eq!(result, expected); } #[test] diff --git a/codex-rs/tui2/src/clipboard_paste.rs b/codex-rs/tui2/src/clipboard_paste.rs index 5863c728b09..4d28b365fed 100644 --- a/codex-rs/tui2/src/clipboard_paste.rs +++ b/codex-rs/tui2/src/clipboard_paste.rs @@ -244,9 +244,14 @@ pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImag /// - shell-escaped single paths (via `shlex`) pub fn normalize_pasted_path(pasted: &str) -> Option { let pasted = pasted.trim(); + let unquoted = pasted + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .or_else(|| pasted.strip_prefix('\'').and_then(|s| s.strip_suffix('\''))) + .unwrap_or(pasted); // file:// URL → filesystem path - if let Ok(url) = url::Url::parse(pasted) + if let Ok(url) = url::Url::parse(unquoted) && url.scheme() == "file" { return url.to_file_path().ok(); @@ -258,38 +263,18 @@ pub fn normalize_pasted_path(pasted: &str) -> Option { // Detect unquoted Windows paths and bypass POSIX shlex which // treats backslashes as escapes (e.g., C:\Users\Alice\file.png). // Also handles UNC paths (\\server\share\path). - let looks_like_windows_path = { - // Drive letter path: C:\ or C:/ - let drive = pasted - .chars() - .next() - .map(|c| c.is_ascii_alphabetic()) - .unwrap_or(false) - && pasted.get(1..2) == Some(":") - && pasted - .get(2..3) - .map(|s| s == "\\" || s == "/") - .unwrap_or(false); - // UNC path: \\server\share - let unc = pasted.starts_with("\\\\"); - drive || unc - }; - if looks_like_windows_path { - #[cfg(target_os = "linux")] - { - if is_probably_wsl() - && let Some(converted) = convert_windows_path_to_wsl(pasted) - { - return Some(converted); - } - } - return Some(PathBuf::from(pasted)); + if let Some(path) = normalize_windows_path(unquoted) { + return Some(path); } // shell-escaped single path → unescaped let parts: Vec = shlex::Shlex::new(pasted).collect(); if parts.len() == 1 { - return parts.into_iter().next().map(PathBuf::from); + let part = parts.into_iter().next()?; + if let Some(path) = normalize_windows_path(&part) { + return Some(path); + } + return Some(PathBuf::from(part)); } None @@ -339,6 +324,36 @@ fn convert_windows_path_to_wsl(input: &str) -> Option { Some(result) } +fn normalize_windows_path(input: &str) -> Option { + // Drive letter path: C:\ or C:/ + let drive = input + .chars() + .next() + .map(|c| c.is_ascii_alphabetic()) + .unwrap_or(false) + && input.get(1..2) == Some(":") + && input + .get(2..3) + .map(|s| s == "\\" || s == "/") + .unwrap_or(false); + // UNC path: \\server\share + let unc = input.starts_with("\\\\"); + if !drive && !unc { + return None; + } + + #[cfg(target_os = "linux")] + { + if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(input) + { + return Some(converted); + } + } + + Some(PathBuf::from(input)) +} + /// Infer an image format for the provided path based on its extension. pub fn pasted_image_format(path: &Path) -> EncodedImageFormat { match path @@ -438,9 +453,39 @@ mod pasted_paths_tests { #[test] fn normalize_single_quoted_windows_path() { let input = r"'C:\\Users\\Alice\\My File.jpeg'"; + let unquoted = r"C:\\Users\\Alice\\My File.jpeg"; let result = normalize_pasted_path(input).expect("should trim single quotes on windows path"); - assert_eq!(result, PathBuf::from(r"C:\\Users\\Alice\\My File.jpeg")); + #[cfg(target_os = "linux")] + let expected = if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(unquoted) + { + converted + } else { + PathBuf::from(unquoted) + }; + #[cfg(not(target_os = "linux"))] + let expected = PathBuf::from(unquoted); + assert_eq!(result, expected); + } + + #[test] + fn normalize_double_quoted_windows_path() { + let input = r#""C:\\Users\\Alice\\My File.jpeg""#; + let unquoted = r"C:\\Users\\Alice\\My File.jpeg"; + let result = + normalize_pasted_path(input).expect("should trim double quotes on windows path"); + #[cfg(target_os = "linux")] + let expected = if is_probably_wsl() + && let Some(converted) = convert_windows_path_to_wsl(unquoted) + { + converted + } else { + PathBuf::from(unquoted) + }; + #[cfg(not(target_os = "linux"))] + let expected = PathBuf::from(unquoted); + assert_eq!(result, expected); } #[test]