Fixed TUI regression related to image paste in WSL (#9473)

This affects quoted paths in WSL. The fix is to normalize quoted Windows
paths before WSL conversion.

This addresses #9456
This commit is contained in:
Eric Traut 2026-01-18 23:02:02 -08:00 committed by GitHub
parent a5e5d7a384
commit a803467f52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 148 additions and 58 deletions

View file

@ -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();
@ -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
@ -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
@ -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]

View file

@ -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();
@ -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
@ -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
@ -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]