Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 74 additions & 29 deletions codex-rs/tui/src/clipboard_paste.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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();
Expand All @@ -258,38 +263,18 @@ pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
// 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<String> = 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
Expand Down Expand Up @@ -339,6 +324,36 @@ fn convert_windows_path_to_wsl(input: &str) -> Option<PathBuf> {
Some(result)
}

fn normalize_windows_path(input: &str) -> Option<PathBuf> {
// 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
Expand Down Expand Up @@ -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]
Expand Down
103 changes: 74 additions & 29 deletions codex-rs/tui2/src/clipboard_paste.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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();
Expand All @@ -258,38 +263,18 @@ pub fn normalize_pasted_path(pasted: &str) -> Option<PathBuf> {
// 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<String> = 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
Expand Down Expand Up @@ -339,6 +324,36 @@ fn convert_windows_path_to_wsl(input: &str) -> Option<PathBuf> {
Some(result)
}

fn normalize_windows_path(input: &str) -> Option<PathBuf> {
// 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
Expand Down Expand Up @@ -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]
Expand Down
Loading