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,
}
}
}