feat: add support for %@ argument extension on Windows to align with Unix platforms (#1319)

This commit is contained in:
三咲雅 · Misaki Masa 2024-07-22 19:06:02 +08:00 committed by GitHub
parent 66e83f3e18
commit a9eb218a5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 461 additions and 147 deletions

1
Cargo.lock generated
View file

@ -2935,6 +2935,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"uzers",
"vergen-gitcl",
"yazi-boot",

View file

@ -1 +1 @@
{"version":"0.2","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP"],"language":"en","flagWords":[]}
{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip"],"version":"0.2","language":"en","flagWords":[]}

View file

@ -26,6 +26,9 @@ tokio = { version = "1.38.1", features = [ "full" ] }
tokio-stream = "0.1.15"
tokio-util = "0.7.11"
# Logging
tracing = { version = "0.1.40", features = [ "max_level_debug", "release_max_level_warn" ] }
[build-dependencies]
vergen-gitcl = { version = "1.0.0", features = [ "build" ] }

View file

@ -4,6 +4,7 @@ use anyhow::{bail, Context, Result};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use tokio::{io::AsyncWriteExt, select, sync::mpsc, task::JoinHandle, time};
use tracing::error;
use yazi_shared::RoCell;
use crate::{body::{Body, BodyBye, BodyHi}, ClientReader, ClientWriter, Payload, Pubsub, Server, Stream};
@ -54,8 +55,8 @@ impl Client {
continue;
} else if line.starts_with("hey,") {
Self::handle_hey(&line);
} else {
Payload::from_str(&line).map(|p| p.emit()).ok();
} else if let Err(e) = Payload::from_str(&line).map(|p| p.emit()) {
error!("Could not parse payload:\n{line}\n\nError:\n{e}");
}
}
}

View file

@ -41,10 +41,10 @@ impl Cha {
reg.add_field_method_get("accessed", |_, me| {
Ok(me.atime.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok()))
});
reg.add_method("permissions", |_, me, ()| {
reg.add_method("permissions", |_, _me, ()| {
Ok(
#[cfg(unix)]
Some(yazi_shared::fs::permissions(me.perm, me.is_dummy())),
Some(yazi_shared::fs::permissions(_me.perm, _me.is_dummy())),
#[cfg(windows)]
None::<String>,
)

View file

@ -1,7 +1,7 @@
use mlua::{IntoLuaMulti, Lua, Value};
use tokio::fs;
use crate::{bindings::Cast, cha::Cha, url::UrlRef};
use crate::{bindings::Cast, cha::Cha, url::{Url, UrlRef}};
pub fn install(lua: &Lua) -> mlua::Result<()> {
lua.globals().raw_set(
@ -34,6 +34,13 @@ pub fn install(lua: &Lua) -> mlua::Result<()> {
}
})?,
),
(
"unique_name",
lua.create_async_function(|lua, url: UrlRef| async move {
// FIXME: handle errors
Url::cast(lua, yazi_shared::fs::unique_name(url.clone()).await)
})?,
),
])?,
)
}

View file

@ -12,9 +12,9 @@ impl Utils {
"quote",
lua.create_function(|_, (s, unix): (mlua::String, Option<bool>)| {
let s = match unix {
Some(true) => shell_escape::unix::escape(s.to_str()?.into()),
Some(false) => shell_escape::windows::escape(s.to_str()?.into()),
None => shell_escape::escape(s.to_str()?.into()),
Some(true) => yazi_shared::escape::unix(s.to_str()?),
Some(false) => yazi_shared::escape::windows(s.to_str()?),
None => yazi_shared::escape::native(s.to_str()?),
};
Ok(s.into_owned())
})?,

View file

@ -46,13 +46,10 @@ pub fn shell(opt: ShellOpt) -> Result<Child> {
#[cfg(windows)]
{
let args: Vec<String> = opt.args.iter().map(|s| s.to_string_lossy().into_owned()).collect();
let args_: Vec<&str> = args.iter().map(|s| s.as_ref()).collect();
let expanded = parser::parse(opt.cmd.to_string_lossy().as_ref(), &args_);
Ok(
Command::new("cmd")
.arg("/C")
.args(&expanded)
Command::new("cmd.exe")
.raw_arg("/C")
.raw_arg(parser::parse(&opt.cmd, &opt.args))
.stdin(opt.stdio())
.stdout(opt.stdio())
.stderr(opt.stdio())
@ -64,156 +61,173 @@ pub fn shell(opt: ShellOpt) -> Result<Child> {
#[cfg(windows)]
mod parser {
use std::{iter::Peekable, str::Chars};
use std::{ffi::{OsStr, OsString}, iter::Peekable, os::windows::ffi::{EncodeWide, OsStrExt, OsStringExt}};
pub(super) fn parse(cmd: &str, args: &[&str]) -> Vec<String> {
let mut it = cmd.chars().peekable();
let mut expanded = vec![];
while let Some(c) = it.next() {
if c.is_whitespace() {
continue;
}
let mut s = String::new();
if c == '\'' {
while let Some(c) = it.next() {
if c == '\'' {
break;
}
next_string(&mut it, args, &mut s, c);
}
expanded.push(s);
} else if c == '"' {
while let Some(c) = it.next() {
if c == '"' {
break;
}
next_string(&mut it, args, &mut s, c);
}
expanded.push(s);
} else if c == '%' && it.peek().is_some_and(|&c| c == '*') {
it.next();
expanded.extend(args.iter().skip(1).map(|&s| s.to_owned()));
} else {
next_string(&mut it, args, &mut s, c);
while let Some(c) = it.next() {
if c.is_whitespace() {
break;
}
next_string(&mut it, args, &mut s, c);
}
expanded.push(s);
}
}
expanded
macro_rules! w {
($c:literal) => {
$c as u16
};
}
fn next_string(it: &mut Peekable<Chars<'_>>, args: &[&str], s: &mut String, c: char) {
if c == '\\' {
match it.next() {
Some('\\') => s.push('\\'), // \\ ==> \
Some('\'') => s.push('\''), // \' ==> '
Some('"') => s.push('"'), // \" ==> "
Some('%') => s.push('%'), // \% ==> %
Some('n') => s.push('\n'), // \n ==> '\n'
Some('t') => s.push('\t'), // \t ==> '\t'
Some('r') => s.push('\r'), // \r ==> '\r'
Some(c) => {
s.push('\\');
s.push(c);
}
None => s.push('\\'),
}
} else if c == '%' {
match it.peek() {
Some('*') => {
if args.len() > 1 {
s.push_str(&args[1..].join(" "));
}
it.next();
}
Some(n) if n.is_ascii_digit() => {
let mut pos = n.to_string();
it.next();
while let Some(&n) = it.peek() {
if n.is_ascii_digit() {
pos.push(it.next().unwrap());
} else {
break;
}
}
if let Some(arg) = args.get(pos.parse::<usize>().unwrap()) {
s.push_str(arg);
}
}
_ => s.push('%'),
}
} else {
s.push(c);
pub(super) fn parse(cmd: &OsStr, args: &[OsString]) -> OsString {
if cmd.len() < 2 {
return cmd.to_owned();
}
let mut buf = Vec::with_capacity(cmd.len());
let mut it = cmd.encode_wide().peekable();
while let Some(c) = it.next() {
if c == w!('%') {
let n = visit_percent(it.clone(), &mut buf, &args, false);
if n == 0 {
buf.push(c);
} else {
it.by_ref().take(n).for_each(drop);
}
} else if c == w!('"') && it.peek().is_some_and(|&c| c == w!('%')) {
it.next();
let n = visit_percent(it.clone(), &mut buf, &args, true);
if n == 0 {
buf.push(c);
buf.push(w!('%'));
} else {
it.by_ref().take(n).for_each(drop);
}
} else {
buf.push(c);
}
}
OsString::from_wide(&buf)
}
fn visit_percent(
mut it: Peekable<EncodeWide>,
buf: &mut Vec<u16>,
args: &[OsString],
quote: bool,
) -> usize {
let Some(c) = it.next().and_then(|c| char::from_u32(c as _)) else {
return 0;
};
let mut pos = None;
if c.is_ascii_digit() {
let mut p = c.to_string();
while let Some(n) = it.peek().and_then(|&c| char::from_u32(c as _)) {
if n.is_ascii_digit() {
it.next();
p.push(n);
} else {
break;
}
}
pos = Some(p);
}
if quote && !it.next().is_some_and(|e| e == w!('"')) {
return 0;
}
if let Some(p) = pos {
if let Some(arg) = args.get(p.parse::<usize>().unwrap()) {
if quote {
buf.extend(yazi_shared::escape::os_str(arg).encode_wide());
} else {
buf.extend(arg.encode_wide());
}
}
return p.len() + quote as usize;
}
if c != '*' && c != '@' {
return 0;
}
let mut s = OsString::new();
for (i, arg) in args.iter().skip(1).enumerate() {
if i > 0 {
s.push(" ");
}
if c == '*' {
s.push(yazi_shared::escape::os_str(arg));
} else {
s.push(arg);
}
}
if quote {
buf.extend(yazi_shared::escape::os_str(&s).encode_wide());
} else {
buf.extend(s.encode_wide());
}
1 + quote as usize
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
fn parse(cmd: &str, args: &[&str]) -> String {
let cmd = OsString::from(cmd);
let args: Vec<_> = args.iter().map(|&s| OsString::from(s)).collect();
super::parse(&cmd, &args).to_str().unwrap().to_owned()
}
#[test]
fn test_no_quote() {
let args = parse("echo abc xyz %0 %2", &["000", "111", "222"]);
assert_eq!(args, ["echo", "abc", "xyz", "000", "222"]);
let s = parse("echo abc xyz %0 %2", &["000", "111", "222"]);
assert_eq!(s, "echo abc xyz 000 222");
let args = parse(" echo abc xyz %1 %2 ", &["", "111", "222"]);
assert_eq!(args, ["echo", "abc", "xyz", "111", "222"]);
let s = parse(" echo abc xyz %1 %2 ", &["", "111", "222"]);
assert_eq!(s, " echo abc xyz 111 222 ");
}
#[test]
fn test_single_quote() {
let args = parse("echo 'abc xyz' '%1' %2", &["000", "111", "222"]);
assert_eq!(args, ["echo", "abc xyz", "111", "222"]);
let s = parse("echo 'abc xyz' '%1' %2", &["000", "111", "222"]);
assert_eq!(s, "echo 'abc xyz' '111' 222");
let args = parse(r#"echo 'abc ""xyz' '%1' %2"#, &["", "111", "222"]);
assert_eq!(args, ["echo", r#"abc ""xyz"#, "111", "222"]);
let s = parse(r#"echo 'abc ""xyz' '%1' %2"#, &["", "111", "222"]);
assert_eq!(s, r#"echo 'abc ""xyz' '111' 222"#);
}
#[test]
fn test_double_quote() {
let args = parse("echo \"abc ' 'xyz\" \"%1\" %2 %3", &["", "111", "222"]);
assert_eq!(args, ["echo", "abc ' 'xyz", "111", "222", ""]);
let s = parse(r#"echo "abc ' 'xyz" "%1" %2 %3"#, &["", "111", "222"]);
assert_eq!(s, r#"echo "abc ' 'xyz" 111 222 "#);
}
#[test]
fn test_escaped() {
let args = parse("echo \"a\tbc ' 'x\nyz\" \"\\%1\" %2 %3", &["", "111", "22 2"]);
assert_eq!(args, ["echo", "a\tbc ' 'x\nyz", "%1", "22 2", ""]);
let s = parse(r#"echo "a bc ' 'x\nyz" "\%1" "\"%2"" %3"#, &["", "111", "22 2"]);
assert_eq!(s, r#"echo "a bc ' 'x\nyz" "\111" "\"22 2"" "#);
}
#[test]
fn test_percent_star() {
let args = parse("echo %* xyz", &[]);
assert_eq!(args, ["echo", "xyz"]);
let s = parse("echo %* xyz", &[]);
assert_eq!(s, "echo xyz");
let args = parse("echo %* xyz", &["000", "111", "222"]);
assert_eq!(args, ["echo", "111", "222", "xyz"]);
let s = parse("echo %* xyz", &["000", "111", "222"]);
assert_eq!(s, "echo 111 222 xyz");
let args = parse("echo '%*' xyz", &["000", "111", "222"]);
assert_eq!(args, ["echo", "111 222", "xyz"]);
let s = parse("echo '%*' xyz", &["000", "111", "22 2"]);
assert_eq!(s, r#"echo '111 "22 2"' xyz"#);
let args = parse("echo -C%* xyz", &[]);
assert_eq!(args, ["echo", "-C", "xyz"]);
let s = parse("echo -C%* xyz", &[]);
assert_eq!(s, "echo -C xyz");
let args = parse("echo -C%* xyz", &["000", "111", "222"]);
assert_eq!(args, ["echo", "-C111 222", "xyz"]);
let s = parse("echo -C%* xyz", &["000", " 111", "222"]);
assert_eq!(s, r#"echo -C" 111" 222 xyz"#);
}
#[test]
fn test_env_var() {
let args = parse(" %EDITOR% %* xyz", &["000", "111", "222"]);
assert_eq!(args, ["%EDITOR%", "111", "222", "xyz"]);
let s = parse(r#"%EDITOR% %@ "%@" %* "%*" xyz"#, &["000", "1 11", "222"]);
assert_eq!(s, r#"%EDITOR% 1 11 222 "1 11 222" "1 11" 222 "\"1 11\" 222" xyz"#);
}
}
}

View file

@ -6,7 +6,7 @@ use parking_lot::Mutex;
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, task::JoinHandle};
use yazi_config::{open::Opener, plugin::{Fetcher, Preloader}, TASKS};
use yazi_dds::Pump;
use yazi_shared::{event::Data, fs::{unique_path, Url}, Throttle};
use yazi_shared::{event::Data, fs::{unique_name, Url}, Throttle};
use super::{Ongoing, TaskProg, TaskStage};
use crate::{file::{File, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash}, plugin::{Plugin, PluginOpEntry}, prework::{Prework, PreworkOpFetch, PreworkOpLoad, PreworkOpSize}, process::{Process, ProcessOpBg, ProcessOpBlock, ProcessOpOrphan}, TaskKind, TaskOp, HIGH, LOW, NORMAL};
@ -97,7 +97,7 @@ impl Scheduler {
_ = self.micro.try_send(
async move {
if !force {
to = unique_path(to).await;
to = unique_name(to).await;
}
file
.paste(FileOpPaste { id, from, to, meta: None, cut: true, follow: false, retry: 0 })
@ -122,7 +122,7 @@ impl Scheduler {
_ = self.micro.try_send(
async move {
if !force {
to = unique_path(to).await;
to = unique_name(to).await;
}
file
.paste(FileOpPaste { id, from, to, meta: None, cut: false, follow, retry: 0 })
@ -142,7 +142,7 @@ impl Scheduler {
_ = self.micro.try_send(
async move {
if !force {
to = unique_path(to).await;
to = unique_name(to).await;
}
file
.link(FileOpLink { id, from, to, meta: None, resolve: false, relative, delete: false })
@ -167,7 +167,7 @@ impl Scheduler {
_ = self.micro.try_send(
async move {
if !force {
to = unique_path(to).await;
to = unique_name(to).await;
}
file.hardlink(FileOpHardlink { id, from, to, meta: None, follow }).await.ok();
}

View file

@ -0,0 +1,41 @@
//! Escape characters that may have special meaning in a shell, including
//! spaces. This is a modified version of the [`shell-escape`] crate and [`this
//! PR`].
//!
//! [`shell-escape`]: https://crates.io/crates/shell-escape
//! [`this PR`]: https://github.com/sfackler/shell-escape/pull/9
use std::{borrow::Cow, ffi::OsStr};
mod unix;
mod windows;
#[inline]
pub fn unix(s: &str) -> Cow<str> { unix::from_str(s) }
#[inline]
pub fn windows(s: &str) -> Cow<str> { windows::from_str(s) }
#[inline]
pub fn native(s: &str) -> Cow<str> {
#[cfg(unix)]
{
unix::from_str(s)
}
#[cfg(windows)]
{
windows::from_str(s)
}
}
#[inline]
pub fn os_str(s: &OsStr) -> Cow<OsStr> {
#[cfg(unix)]
{
unix::from_os_str(s)
}
#[cfg(windows)]
{
windows::from_os_str(s)
}
}

View file

@ -0,0 +1,97 @@
use std::borrow::Cow;
pub fn from_str(s: &str) -> Cow<str> {
match from_slice(s.as_bytes()) {
Cow::Borrowed(_) => Cow::Borrowed(s),
Cow::Owned(v) => String::from_utf8(v).expect("Invalid bytes returned from from_slice()").into(),
}
}
#[cfg(unix)]
pub fn from_os_str(s: &std::ffi::OsStr) -> Cow<std::ffi::OsStr> {
use std::os::unix::ffi::{OsStrExt, OsStringExt};
match from_slice(s.as_bytes()) {
Cow::Borrowed(_) => Cow::Borrowed(s),
Cow::Owned(v) => std::ffi::OsString::from_vec(v).into(),
}
}
fn from_slice(s: &[u8]) -> Cow<[u8]> {
if !s.is_empty() && s.iter().copied().all(allowed) {
return Cow::Borrowed(s);
}
let mut escaped = Vec::with_capacity(s.len() + 2);
escaped.push(b'\'');
for &b in s {
match b {
b'\'' | b'!' => {
escaped.reserve(4);
escaped.push(b'\'');
escaped.push(b'\\');
escaped.push(b);
escaped.push(b'\'');
}
_ => escaped.push(b),
}
}
escaped.push(b'\'');
escaped.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'+')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_str() {
assert_eq!(from_str(""), r#"''"#);
assert_eq!(from_str(" "), r#"' '"#);
assert_eq!(from_str("*"), r#"'*'"#);
assert_eq!(from_str("--aaa=bbb-ccc"), "--aaa=bbb-ccc");
assert_eq!(from_str(r#"--features="default""#), r#"'--features="default"'"#);
assert_eq!(from_str("linker=gcc -L/foo -Wl,bar"), r#"'linker=gcc -L/foo -Wl,bar'"#);
assert_eq!(
from_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"),
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+",
);
assert_eq!(from_str(r#"'!\$`\\\n "#), r#"''\'''\!'\$`\\\n '"#);
}
#[cfg(unix)]
#[test]
fn test_from_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!(from_os_str(OsStr::from_bytes(input)), OsStr::from_bytes(expected));
}
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'\'']);
}
}

View file

@ -0,0 +1,149 @@
use std::{borrow::Cow, iter::repeat};
pub fn from_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);
}
let mut escaped = String::with_capacity(bytes.len() + 2);
escaped.push('"');
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('\\').take(slashes * 2 + 1));
escaped.push('"');
}
Some(c) => {
escaped.reserve(slashes + 1);
escaped.extend(repeat('\\').take(slashes));
escaped.push(c as _);
}
None => {
escaped.reserve(slashes * 2);
escaped.extend(repeat('\\').take(slashes * 2));
break;
}
}
}
escaped.push('"');
escaped.into()
}
#[cfg(windows)]
pub fn from_os_str(s: &std::ffi::OsStr) -> Cow<std::ffi::OsStr> {
use std::os::windows::ffi::{OsStrExt, OsStringExt};
let wide = s.encode_wide();
if !s.is_empty() && !wide.clone().into_iter().any(disallowed) {
return Cow::Borrowed(s);
}
let mut escaped: Vec<u16> = 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(b'\\' as u16).take(slashes * 2 + 1));
escaped.push(b'"' as _);
}
Some(c) => {
escaped.reserve(slashes + 1);
escaped.extend(repeat(b'\\' as u16).take(slashes));
escaped.push(c);
}
None => {
escaped.reserve(slashes * 2);
escaped.extend(repeat(b'\\' as u16).take(slashes * 2));
break;
}
}
}
escaped.push(b'"' as _);
std::ffi::OsString::from_wide(&escaped).into()
}
#[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_from_str() {
assert_eq!(from_str(""), r#""""#);
assert_eq!(from_str(r#""""#), r#""\"\"""#);
assert_eq!(from_str("--aaa=bbb-ccc"), "--aaa=bbb-ccc");
assert_eq!(from_str(r#"\path\to\my documents\"#), r#""\path\to\my documents\\""#);
assert_eq!(from_str(r#"--features="default""#), r#""--features=\"default\"""#);
assert_eq!(from_str(r#""--features=\"default\"""#), r#""\"--features=\\\"default\\\"\"""#);
assert_eq!(from_str("linker=gcc -L/foo -Wl,bar"), r#""linker=gcc -L/foo -Wl,bar""#);
}
#[cfg(windows)]
#[test]
fn test_from_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!(from_os_str(observed.as_os_str()), expected.as_os_str());
}
fn from_bytes(input: &[u16], expected: &[u16]) {
let observed = OsString::from_wide(input);
let expected = OsString::from_wide(expected);
assert_eq!(from_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,
]);
}
}

View file

@ -55,12 +55,14 @@ pub fn ends_with_slash(p: &Path) -> bool {
if let [.., last] = b { *last == MAIN_SEPARATOR as u8 } else { false }
}
pub async fn unique_path(mut p: Url) -> Url {
let Some(stem) = p.file_stem().map(|s| s.to_owned()) else {
return p;
// FIXME: should return a `std::io::Result` to handle errors such as
// permission denied
pub async fn unique_name(mut u: Url) -> Url {
let Some(stem) = u.file_stem().map(|s| s.to_owned()) else {
return u;
};
let ext = p
let ext = u
.extension()
.map(|s| {
let mut n = OsString::with_capacity(s.len() + 1);
@ -70,20 +72,18 @@ pub async fn unique_path(mut p: Url) -> Url {
})
.unwrap_or_default();
let mut i = 0;
while maybe_exists(&p).await {
i += 1;
let mut i = 1u64;
while maybe_exists(&u).await {
let mut name = OsString::with_capacity(stem.len() + ext.len() + 5);
name.push(&stem);
name.push(format!("_{i}"));
if !ext.is_empty() {
name.push(&ext);
}
name.push("_");
name.push(i.to_string());
name.push(&ext);
p.set_file_name(name);
u.set_file_name(name);
i += 1;
}
p
u
}
// Parameters

View file

@ -5,6 +5,7 @@ mod condition;
mod debounce;
mod env;
mod errors;
pub mod escape;
pub mod event;
pub mod fs;
mod layer;