diff --git a/CHANGELOG.md b/CHANGELOG.md index f964c3a6..07ed6fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Allow dynamic adjustment of layout ratio via `rt.mgr.ratio` ([#2964]) - Support `.deb` packages ([#2807], [#3128], [#3209]) - Port several widespread GUI keys to the input component ([#2849]) -- Support invalid UTF-8 paths throughout the codebase ([#2884], [#2889], [#2890], [#2895], [#3023], [#3290]) +- Support invalid UTF-8 paths throughout the codebase ([#2884], [#2889], [#2890], [#2895], [#3023], [#3290], [#3369]) - Allow upgrading only specific packages with `ya pkg` ([#2841]) - Respect the user's `image_filter` setting in the preset ImageMagick previewer ([#3286]) - Allow custom mouse click behavior for individual files ([#2925]) @@ -1542,3 +1542,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3317]: https://github.com/sxyazi/yazi/pull/3317 [#3360]: https://github.com/sxyazi/yazi/pull/3360 [#3364]: https://github.com/sxyazi/yazi/pull/3364 +[#3369]: https://github.com/sxyazi/yazi/pull/3369 diff --git a/yazi-binding/src/path.rs b/yazi-binding/src/path.rs index be37b940..137a1d84 100644 --- a/yazi-binding/src/path.rs +++ b/yazi-binding/src/path.rs @@ -79,7 +79,7 @@ impl Path { } } - fn strip_prefix(&self, base: Value) -> mlua::Result> { + fn strip_prefix(&self, base: Value) -> mlua::Result> { let strip = match base { Value::String(s) => self.try_strip_prefix(StrandCow::with(self.kind(), &*s.as_bytes())?), Value::UserData(ud) => self.try_strip_prefix(&*ud.borrow::()?), @@ -87,7 +87,7 @@ impl Path { }; Ok(match strip { - Ok(p) => Some(Path::new(p)), + Ok(p) => Some(Self::new(p)), Err(StripPrefixError::Exotic | StripPrefixError::NotPrefix) => None, Err(e @ StripPrefixError::WrongEncoding) => Err(e.into_lua_err())?, }) diff --git a/yazi-binding/src/url.rs b/yazi-binding/src/url.rs index f787c7f3..9def6a38 100644 --- a/yazi-binding/src/url.rs +++ b/yazi-binding/src/url.rs @@ -108,7 +108,7 @@ impl Url { match other { Value::String(s) => { let b = s.as_bytes(); - let (scheme, path) = SchemeCow::parse(&*b)?; + let (scheme, path) = SchemeCow::parse(&b)?; if scheme == self.scheme() { Self::new(self.try_join(path).into_lua_err()?).into_lua(lua) } else { diff --git a/yazi-parser/src/app/plugin.rs b/yazi-parser/src/app/plugin.rs index 2413a573..5232ea8f 100644 --- a/yazi-parser/src/app/plugin.rs +++ b/yazi-parser/src/app/plugin.rs @@ -29,7 +29,7 @@ impl TryFrom for PluginOpt { }; let args = if let Ok(s) = c.second() { - let (words, last) = yazi_shared::shell::split_unix(s, true)?; + let (words, last) = yazi_shared::shell::unix::split(s, true)?; Cmd::parse_args(words, last)? } else { Default::default() diff --git a/yazi-parser/src/mgr/search.rs b/yazi-parser/src/mgr/search.rs index ffef434e..10459077 100644 --- a/yazi-parser/src/mgr/search.rs +++ b/yazi-parser/src/mgr/search.rs @@ -24,7 +24,7 @@ impl TryFrom for SearchOpt { (c.str(0).parse()?, "".into()) }; - let Ok(args) = yazi_shared::shell::split_unix(c.str("args"), false) else { + let Ok(args) = yazi_shared::shell::unix::split(c.str("args"), false) else { bail!("Invalid 'args' argument in SearchOpt"); }; diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index 1e63f64f..72a41303 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -144,11 +144,11 @@ impl UserData for Command { let mut me = ud.borrow_mut::()?; match arg { Value::String(s) => { - me.inner.arg(OsStr::from_wtf8(&*s.as_bytes())?); + me.inner.arg(OsStr::from_wtf8(&s.as_bytes())?); } Value::Table(t) => { for s in t.sequence_values::() { - me.inner.arg(OsStr::from_wtf8(&*s?.as_bytes())?); + me.inner.arg(OsStr::from_wtf8(&s?.as_bytes())?); } } _ => return Err("arg must be a string or table of strings".into_lua_err()), @@ -165,7 +165,7 @@ impl UserData for Command { |_, (ud, key, value): (AnyUserData, mlua::String, mlua::String)| { ud.borrow_mut::()? .inner - .env(OsStr::from_wtf8(&*key.as_bytes())?, OsStr::from_wtf8(&*value.as_bytes())?); + .env(OsStr::from_wtf8(&key.as_bytes())?, OsStr::from_wtf8(&value.as_bytes())?); Ok(ud) }, ); diff --git a/yazi-plugin/src/utils/text.rs b/yazi-plugin/src/utils/text.rs index 22b861e2..0b34d0e2 100644 --- a/yazi-plugin/src/utils/text.rs +++ b/yazi-plugin/src/utils/text.rs @@ -15,11 +15,11 @@ impl Utils { pub(super) fn quote(lua: &Lua) -> mlua::Result { lua.create_function(|lua, (s, unix): (mlua::String, Option)| { - let s = s.to_str()?; + let b = s.as_bytes(); let s = match unix { - Some(true) => yazi_shared::shell::escape_unix(s.as_ref()), - Some(false) => yazi_shared::shell::escape_windows(s.as_ref()), - None => yazi_shared::shell::escape_native(s.as_ref()), + Some(true) => yazi_shared::shell::unix::escape_os_bytes(&b), + Some(false) => yazi_shared::shell::windows::escape_os_bytes(&b), + None => yazi_shared::shell::escape_os_bytes(&b), }; lua.create_string(&*s) }) diff --git a/yazi-scheduler/src/process/shell.rs b/yazi-scheduler/src/process/shell.rs index 1b6c704f..0266a111 100644 --- a/yazi-scheduler/src/process/shell.rs +++ b/yazi-scheduler/src/process/shell.rs @@ -2,7 +2,7 @@ use std::{ffi::OsString, process::Stdio}; use anyhow::Result; use tokio::process::{Child, Command}; -use yazi_fs::{Cwd, FsUrl}; +use yazi_fs::Cwd; use yazi_shared::url::{AsUrl, UrlCow}; pub(crate) struct ShellOpt { @@ -32,6 +32,7 @@ pub(crate) async fn shell(opt: ShellOpt) -> Result { #[cfg(unix)] return Ok(unsafe { + use yazi_fs::FsUrl; use yazi_shared::url::AsUrl; Command::new("sh") @@ -59,8 +60,10 @@ pub(crate) async fn shell(opt: ShellOpt) -> Result { .stdin(opt.stdio()) .stdout(opt.stdio()) .stderr(opt.stdio()) - .raw_arg("/C") + .env("=", r#""^\n\n""#) + .raw_arg(r#"/Q /S /D /V:OFF /E:ON /C ""#) .raw_arg(opt.cmd) + .raw_arg(r#"""#) .current_dir(cwd) .kill_on_drop(!opt.orphan) .spawn()?, diff --git a/yazi-shared/src/event/cmd.rs b/yazi-shared/src/event/cmd.rs index 352765c1..e7ad0469 100644 --- a/yazi-shared/src/event/cmd.rs +++ b/yazi-shared/src/event/cmd.rs @@ -231,7 +231,7 @@ impl FromStr for Cmd { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let (mut words, last) = crate::shell::split_unix(s, true)?; + let (mut words, last) = crate::shell::unix::split(s, true)?; if words.is_empty() || words[0].is_empty() { bail!("command name cannot be empty"); } diff --git a/yazi-shared/src/shell/error.rs b/yazi-shared/src/shell/error.rs new file mode 100644 index 00000000..a151602d --- /dev/null +++ b/yazi-shared/src/shell/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SplitError { + #[error("missing closing single quote")] + MissingSingleQuote, + #[error("missing closing double quote")] + MissingDoubleQuote, + #[error("missing quote after escape slash")] + MissingQuoteAfterSlash, +} diff --git a/yazi-shared/src/shell/mod.rs b/yazi-shared/src/shell/mod.rs index c2e4abcc..69ecf016 100644 --- a/yazi-shared/src/shell/mod.rs +++ b/yazi-shared/src/shell/mod.rs @@ -1,31 +1,27 @@ //! Escape characters that may have special meaning in a shell, including -//! spaces. This is a modified version of the [`shell-escape`], [`shell-words`] -//! and [`this PR`]. +//! spaces. This is a modified version of the [`shell-escape`], [`shell-words`], +//! [Rust std] and [this PR]. //! //! [`shell-escape`]: https://crates.io/crates/shell-escape //! [`shell-words`]: https://crates.io/crates/shell-words -//! [`this PR`]: https://github.com/sfackler/shell-escape/pull/9 +//! [Rust std]: https://github.com/rust-lang/rust/blob/main/library/std/src/sys/args/windows.rs#L220 +//! [this PR]: https://github.com/sfackler/shell-escape/pull/9 use std::{borrow::Cow, ffi::OsStr}; -mod unix; -mod windows; +yazi_macro::mod_pub!(unix, windows); + +yazi_macro::mod_flat!(error); #[inline] -pub fn escape_unix(s: &str) -> Cow<'_, str> { unix::escape_str(s) } - -#[inline] -pub fn escape_windows(s: &str) -> Cow<'_, str> { windows::escape_str(s) } - -#[inline] -pub fn escape_native(s: &str) -> Cow<'_, str> { +pub fn escape_os_bytes(b: &[u8]) -> Cow<'_, [u8]> { #[cfg(unix)] { - escape_unix(s) + unix::escape_os_bytes(b) } #[cfg(windows)] { - escape_windows(s) + windows::escape_os_bytes(b) } } @@ -40,22 +36,3 @@ pub fn escape_os_str(s: &OsStr) -> Cow<'_, OsStr> { windows::escape_os_str(s) } } - -#[inline] -pub fn split_unix(s: &str, eoo: bool) -> anyhow::Result<(Vec, Option)> { - unix::split(s, eoo).map_err(|()| anyhow::anyhow!("missing closing quote")) -} - -#[cfg(windows)] -pub fn split_windows(s: &str) -> anyhow::Result> { Ok(windows::split(s)?) } - -pub fn split_native(s: &str) -> anyhow::Result> { - #[cfg(unix)] - { - Ok(split_unix(s, false)?.0) - } - #[cfg(windows)] - { - split_windows(s) - } -} diff --git a/yazi-shared/src/shell/unix.rs b/yazi-shared/src/shell/unix.rs index 75d1b47e..42364c81 100644 --- a/yazi-shared/src/shell/unix.rs +++ b/yazi-shared/src/shell/unix.rs @@ -1,40 +1,25 @@ use std::{borrow::Cow, mem}; -pub fn escape_str(s: &str) -> Cow<'_, str> { - match escape_slice(s.as_bytes()) { - Cow::Borrowed(_) => Cow::Borrowed(s), - Cow::Owned(v) => String::from_utf8(v).expect("Invalid bytes returned by escape_slice()").into(), - } -} +use crate::shell::SplitError; -#[cfg(unix)] -pub fn escape_os_str(s: &std::ffi::OsStr) -> Cow<'_, std::ffi::OsStr> { - use std::os::unix::ffi::{OsStrExt, OsStringExt}; - - match escape_slice(s.as_bytes()) { - Cow::Borrowed(_) => Cow::Borrowed(s), - Cow::Owned(v) => std::ffi::OsString::from_vec(v).into(), - } -} - -fn escape_slice(s: &[u8]) -> Cow<'_, [u8]> { - if !s.is_empty() && s.iter().copied().all(allowed) { - return Cow::Borrowed(s); +pub fn escape_os_bytes(b: &[u8]) -> Cow<'_, [u8]> { + if !b.is_empty() && b.iter().copied().all(allowed) { + return Cow::Borrowed(b); } - let mut escaped = Vec::with_capacity(s.len() + 2); + let mut escaped = Vec::with_capacity(b.len() + 2); escaped.push(b'\''); - for &b in s { - match b { + for &c in b { + match c { b'\'' | b'!' => { escaped.reserve(4); escaped.push(b'\''); escaped.push(b'\\'); - escaped.push(b); + escaped.push(c); escaped.push(b'\''); } - _ => escaped.push(b), + _ => escaped.push(c), } } @@ -42,11 +27,21 @@ fn escape_slice(s: &[u8]) -> Cow<'_, [u8]> { escaped.into() } +#[cfg(unix)] +pub fn escape_os_str(s: &std::ffi::OsStr) -> Cow<'_, std::ffi::OsStr> { + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + + match escape_os_bytes(s.as_bytes()) { + Cow::Borrowed(_) => Cow::Borrowed(s), + Cow::Owned(v) => std::ffi::OsString::from_vec(v).into(), + } +} + fn allowed(b: u8) -> bool { matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'=' | b'/' | b',' | b'.' | b'+') } -pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), ()> { +pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), SplitError> { enum State { /// Within a delimiter. Delimiter, @@ -138,7 +133,7 @@ pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), ()> { } }, SingleQuoted => match c { - None => return Err(()), + None => return Err(SplitError::MissingSingleQuote), Some('\'') => Unquoted, Some(c) => { word.push(c); @@ -146,7 +141,7 @@ pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), ()> { } }, DoubleQuoted => match c { - None => return Err(()), + None => return Err(SplitError::MissingDoubleQuote), Some('\"') => Unquoted, Some('\\') => DoubleQuotedBackslash, Some(c) => { @@ -155,7 +150,7 @@ pub fn split(s: &str, eoo: bool) -> Result<(Vec, Option), ()> { } }, DoubleQuotedBackslash => match c { - None => return Err(()), + None => return Err(SplitError::MissingQuoteAfterSlash), Some('\n') => DoubleQuoted, Some(c @ '$') | Some(c @ '`') | Some(c @ '"') | Some(c @ '\\') => { word.push(c); @@ -183,47 +178,25 @@ mod tests { use super::*; #[test] - fn test_escape_str() { - assert_eq!(escape_str(""), r#"''"#); - assert_eq!(escape_str(" "), r#"' '"#); - assert_eq!(escape_str("*"), r#"'*'"#); + fn test_escape_os_bytes() { + let cases: &[(&[u8], &[u8])] = &[ + (b"", br#"''"#), + (b" ", br#"' '"#), + (b"*", br#"'*'"#), + (b"--aaa=bbb-ccc", b"--aaa=bbb-ccc"), + (br#"--features="default""#, br#"'--features="default"'"#), + (b"linker=gcc -L/foo -Wl,bar", br#"'linker=gcc -L/foo -Wl,bar'"#), + ( + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", + ), + (br#"'!\$`\\\n "#, br#"''\'''\!'\$`\\\n '"#), + (&[0x66, 0x6f, 0x80, 0x6f], &[b'\'', 0x66, 0x6f, 0x80, 0x6f, b'\'']), + ]; - assert_eq!(escape_str("--aaa=bbb-ccc"), "--aaa=bbb-ccc"); - assert_eq!(escape_str(r#"--features="default""#), r#"'--features="default"'"#); - assert_eq!(escape_str("linker=gcc -L/foo -Wl,bar"), r#"'linker=gcc -L/foo -Wl,bar'"#); - - assert_eq!( - escape_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"), - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", - ); - assert_eq!(escape_str(r#"'!\$`\\\n "#), r#"''\'''\!'\$`\\\n '"#); - } - - #[cfg(unix)] - #[test] - fn test_escape_os_str() { - use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; - - fn from_str(input: &str, expected: &str) { from_bytes(input.as_bytes(), expected.as_bytes()) } - - fn from_bytes(input: &[u8], expected: &[u8]) { - assert_eq!(escape_os_str(OsStr::from_bytes(input)), OsStr::from_bytes(expected)); + for &(input, expected) in cases { + let escaped = escape_os_bytes(input); + assert_eq!(escaped, expected, "Failed to escape: {:?}", String::from_utf8_lossy(input)); } - - from_str("", r#"''"#); - from_str(" ", r#"' '"#); - from_str("*", r#"'*'"#); - - from_str("--aaa=bbb-ccc", "--aaa=bbb-ccc"); - from_str(r#"--features="default""#, r#"'--features="default"'"#); - from_str("linker=gcc -L/foo -Wl,bar", r#"'linker=gcc -L/foo -Wl,bar'"#); - - from_str( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", - ); - from_str(r#"'!\$`\\\n "#, r#"''\'''\!'\$`\\\n '"#); - - from_bytes(&[0x66, 0x6f, 0x80, 0x6f], &[b'\'', 0x66, 0x6f, 0x80, 0x6f, b'\'']); } } diff --git a/yazi-shared/src/shell/windows.rs b/yazi-shared/src/shell/windows.rs index aa142e39..29ddc83b 100644 --- a/yazi-shared/src/shell/windows.rs +++ b/yazi-shared/src/shell/windows.rs @@ -1,82 +1,68 @@ -use std::{borrow::Cow, iter::repeat_n}; +use std::borrow::Cow; -pub fn escape_str(s: &str) -> Cow<'_, str> { - let bytes = s.as_bytes(); - if !bytes.is_empty() && !bytes.iter().any(|&c| matches!(c, b' ' | b'"' | b'\n' | b'\t')) { - return Cow::Borrowed(s); +pub fn escape_os_bytes(b: &[u8]) -> Cow<'_, [u8]> { + let quote = needs_quotes(b); + let mut buf = Vec::with_capacity(b.len() + quote as usize * 2); + + if quote { + buf.push(b'"'); } - let mut escaped = Vec::with_capacity(bytes.len() + 2); - escaped.push(b'"'); - - let mut chars = bytes.iter().copied().peekable(); - loop { - let mut slashes = 0; - while chars.next_if_eq(&b'\\').is_some() { - slashes += 1; - } - match chars.next() { - Some(b'"') => { - escaped.reserve(slashes * 2 + 2); - escaped.extend(repeat_n(b'\\', slashes * 2 + 1)); - escaped.push(b'"'); - } - Some(b) => { - escaped.reserve(slashes + 1); - escaped.extend(repeat_n(b'\\', slashes)); - escaped.push(b); - } - None => { - escaped.reserve(slashes * 2); - escaped.extend(repeat_n(b'\\', slashes * 2)); - break; - } + // Loop through the string, escaping `\` only if followed by `"`. + // And escaping `"` by doubling them. + let mut backslashes: usize = 0; + for &c in b { + if c == b'\\' { + backslashes += 1; + } else { + if c == b'"' { + buf.extend((0..backslashes).map(|_| b'\\')); + buf.push(b'"'); + } else if c == b'%' { + buf.extend_from_slice(b"%%cd:~,"); + } else if c == b'\r' || c == b'\n' { + buf.extend_from_slice(b"%=%"); + backslashes = 0; + continue; + } + backslashes = 0; } + buf.push(c); } - escaped.push(b'"'); - Cow::Owned(unsafe { String::from_utf8_unchecked(escaped) }) + if quote { + buf.extend((0..backslashes).map(|_| b'\\')); + buf.push(b'"'); + } + + buf.into() } #[cfg(windows)] pub fn escape_os_str(s: &std::ffi::OsStr) -> Cow<'_, std::ffi::OsStr> { - use std::os::windows::ffi::{OsStrExt, OsStringExt}; + use crate::FromWtf8Vec; - let wide = s.encode_wide(); - if !s.is_empty() && !wide.clone().into_iter().any(disallowed) { - return Cow::Borrowed(s); + match escape_os_bytes(s.as_encoded_bytes()) { + Cow::Borrowed(_) => Cow::Borrowed(s), + Cow::Owned(v) => std::ffi::OsString::from_wtf8_vec(v).expect("valid WTF-8").into(), + } +} + +fn needs_quotes(arg: &[u8]) -> bool { + static UNQUOTED: &[u8] = br"#$*+-./:?@\_"; + + if arg.is_empty() || arg.last() == Some(&b'\\') { + return true; } - let mut escaped: Vec = Vec::with_capacity(s.len() + 2); - escaped.push(b'"' as _); - - let mut chars = wide.into_iter().peekable(); - loop { - let mut slashes = 0; - while chars.next_if_eq(&(b'\\' as _)).is_some() { - slashes += 1; - } - match chars.next() { - Some(c) if c == b'"' as _ => { - escaped.reserve(slashes * 2 + 2); - escaped.extend(repeat_n(b'\\' as u16, slashes * 2 + 1)); - escaped.push(b'"' as _); - } - Some(c) => { - escaped.reserve(slashes + 1); - escaped.extend(repeat_n(b'\\' as u16, slashes)); - escaped.push(c); - } - None => { - escaped.reserve(slashes * 2); - escaped.extend(repeat_n(b'\\' as u16, slashes * 2)); - break; - } + for c in arg { + if c.is_ascii_control() { + return true; + } else if c.is_ascii() && !(c.is_ascii_alphanumeric() || UNQUOTED.contains(c)) { + return true; } } - - escaped.push(b'"' as _); - std::ffi::OsString::from_wide(&escaped).into() + false } #[cfg(windows)] @@ -84,11 +70,11 @@ pub fn split(s: &str) -> std::io::Result> { use std::os::windows::ffi::OsStrExt; let s: Vec<_> = std::ffi::OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect(); - split_slice(&s) + split_wide(&s) } #[cfg(windows)] -fn split_slice(s: &[u16]) -> std::io::Result> { +fn split_wide(s: &[u16]) -> std::io::Result> { use std::mem::MaybeUninit; use windows_sys::{Win32::{Foundation::LocalFree, UI::Shell::CommandLineToArgvW}, core::PCWSTR}; @@ -114,31 +100,36 @@ fn split_slice(s: &[u16]) -> std::io::Result> { Ok(res) } -#[cfg(windows)] -fn disallowed(b: u16) -> bool { - match char::from_u32(b as u32) { - Some(c) => matches!(c, ' ' | '"' | '\n' | '\t'), - None => true, - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn test_escape_str() { - assert_eq!(escape_str(""), r#""""#); - assert_eq!(escape_str(r#""""#), r#""\"\"""#); + fn test_escape_os_bytes() { + let cases: &[(&[u8], &[u8])] = &[ + // Empty string + (b"", br#""""#), + (br#""""#, br#""""""""#), + // No escaping needed + (b"--aaa=bbb-ccc", br#""--aaa=bbb-ccc""#), + // Paths with spaces + (br#"\path\to\my documents\"#, br#""\path\to\my documents\\""#), + // Strings with quotes + (br#"--features="default""#, br#""--features=""default""""#), + // Nested quotes + (br#""--features=\"default\"""#, br#""""--features=\\""default\\""""""#), + // Complex command + (b"linker=gcc -L/foo -Wl,bar", br#""linker=gcc -L/foo -Wl,bar""#), + // Variable expansion + (b"%APPDATA%.txt", br#""%%cd:~,%APPDATA%%cd:~,%.txt""#), + // Unicode characters + ("이것은 테스트".as_bytes(), r#""이것은 테스트""#.as_bytes()), + ]; - assert_eq!(escape_str("--aaa=bbb-ccc"), "--aaa=bbb-ccc"); - assert_eq!(escape_str(r#"\path\to\my documents\"#), r#""\path\to\my documents\\""#); - - assert_eq!(escape_str(r#"--features="default""#), r#""--features=\"default\"""#); - assert_eq!(escape_str(r#""--features=\"default\"""#), r#""\"--features=\\\"default\\\"\"""#); - assert_eq!(escape_str("linker=gcc -L/foo -Wl,bar"), r#""linker=gcc -L/foo -Wl,bar""#); - - assert_eq!(escape_str("이것은 테스트"), r#""이것은 테스트""#); + for &(input, expected) in cases { + let escaped = escape_os_bytes(input); + assert_eq!(escaped, expected, "Failed to escape: {:?}", String::from_utf8_lossy(input)); + } } #[cfg(windows)] @@ -146,41 +137,23 @@ mod tests { fn test_escape_os_str() { use std::{ffi::OsString, os::windows::ffi::OsStringExt}; - fn from_str(input: &str, expected: &str) { - let observed = OsString::from(input); - let expected = OsString::from(expected); - assert_eq!(escape_os_str(observed.as_os_str()), expected.as_os_str()); + #[rustfmt::skip] + let cases: &[(OsString, OsString)] = &[ + // Surrogate pairs and special characters + ( + OsString::from_wide(&[0x1055, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065]), + OsString::from_wide(&[0x1055, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065]), + ), + // Surrogate pair with quotes + ( + OsString::from_wide(&[0xd801, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065]), + OsString::from_wide(&[0xd801, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065]), + ), + ]; + + for (input, expected) in cases { + let escaped = escape_os_str(&input); + assert_eq!(&*escaped, expected, "Failed to escape: {:?}", input.to_string_lossy()); } - - fn from_bytes(input: &[u16], expected: &[u16]) { - let observed = OsString::from_wide(input); - let expected = OsString::from_wide(expected); - assert_eq!(escape_os_str(observed.as_os_str()), expected.as_os_str()); - } - - from_str("", r#""""#); - from_str(r#""""#, r#""\"\"""#); - - from_str("--aaa=bbb-ccc", "--aaa=bbb-ccc"); - from_str(r#"\path\to\my documents\"#, r#""\path\to\my documents\\""#); - - from_str(r#"--features="default""#, r#""--features=\"default\"""#); - from_str(r#""--features=\"default\"""#, r#""\"--features=\\\"default\\\"\"""#); - from_str("linker=gcc -L/foo -Wl,bar", r#""linker=gcc -L/foo -Wl,bar""#); - - from_bytes(&[0x1055, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065], &[ - 0x1055, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065, - ]); - from_bytes(&[0xd801, 0x006e, 0x0069, 0x0063, 0x006f, 0x0064, 0x0065], &[ - b'"' as u16, - 0xd801, - 0x006e, - 0x0069, - 0x0063, - 0x006f, - 0x0064, - 0x0065, - b'"' as u16, - ]); } } diff --git a/yazi-shared/src/strand/conversion.rs b/yazi-shared/src/strand/conversion.rs index e8e628e6..35258cfa 100644 --- a/yazi-shared/src/strand/conversion.rs +++ b/yazi-shared/src/strand/conversion.rs @@ -63,8 +63,8 @@ impl AsStrand for PathDyn<'_> { impl AsStrand for PathBufDyn { fn as_strand(&self) -> Strand<'_> { match self { - PathBufDyn::Os(p) => Strand::Os(p.as_os_str()), - PathBufDyn::Unix(p) => Strand::Bytes(p.as_bytes()), + Self::Os(p) => Strand::Os(p.as_os_str()), + Self::Unix(p) => Strand::Bytes(p.as_bytes()), } } } diff --git a/yazi-shared/src/strand/kind.rs b/yazi-shared/src/strand/kind.rs index dd2e0883..e1034715 100644 --- a/yazi-shared/src/strand/kind.rs +++ b/yazi-shared/src/strand/kind.rs @@ -10,8 +10,8 @@ pub enum StrandKind { impl From for StrandKind { fn from(value: PathKind) -> Self { match value { - PathKind::Os => StrandKind::Os, - PathKind::Unix => StrandKind::Bytes, + PathKind::Os => Self::Os, + PathKind::Unix => Self::Bytes, } } }