use core::fmt::{self, Formatter, Write};
use unicode_width::UnicodeWidthChar;
const SPECIAL_SHELL_CHARS: &[u8] = b"|&;<>()$`\"'*?[]=,{} ";
const SPECIAL_SHELL_CHARS_START: &[char] = &['~', '#', '@', '!'];
const DOUBLE_UNSAFE: &[u8] = b"\"`$";
pub(crate) fn write(
f: &mut Formatter<'_>,
text: &str,
force_quote: bool,
external: bool,
) -> fmt::Result {
match text {
"" if external => {
return f.write_str(r#"'""'"#);
}
"--%" if external => {
return f.write_str(r#"'"--%"'"#);
}
"--%" => {
return f.write_str("'--%'");
}
_ => (),
}
let mut is_single_safe = true;
let mut is_double_safe = true;
let mut has_ascii_double = false;
let mut requires_quote = force_quote;
let mut is_bidi = false;
if !requires_quote {
let mut chars = text.chars();
if let Some(first) = chars.next() {
let second = chars.next();
if SPECIAL_SHELL_CHARS_START.contains(&first) {
requires_quote = true;
}
if !external {
if !requires_quote && first.is_ascii_digit() {
requires_quote = true;
}
if !requires_quote && first == '.' {
if let Some(second) = second {
if second.is_ascii_digit() {
requires_quote = true;
}
}
}
}
if !requires_quote && unicode::is_dash(first) {
if second.is_none() || second == Some('-') {
} else if external {
if text.find(&[':', '.'][..]).is_some() {
requires_quote = true;
}
} else {
requires_quote = true;
}
}
if !requires_quote && first.width().unwrap_or(0) == 0 {
requires_quote = true;
}
} else {
requires_quote = true;
}
}
for ch in text.chars() {
if ch.is_ascii() {
let ch = ch as u8;
if ch == b'\'' {
is_single_safe = false;
}
if ch == b'"' {
has_ascii_double = true;
}
if is_double_safe && DOUBLE_UNSAFE.contains(&ch) {
is_double_safe = false;
}
if !requires_quote && SPECIAL_SHELL_CHARS.contains(&ch) {
requires_quote = true;
}
if ch.is_ascii_control() {
return write_escaped(f, text.chars().map(Ok), external);
}
} else {
if !requires_quote && unicode::is_whitespace(ch) {
requires_quote = true;
}
if (!requires_quote || is_double_safe) && unicode::is_double_quote(ch) {
is_double_safe = false;
requires_quote = true;
}
if (!requires_quote || is_single_safe) && unicode::is_single_quote(ch) {
is_single_safe = false;
requires_quote = true;
}
if crate::is_bidi(ch) {
is_bidi = true;
}
if crate::requires_escape(ch) {
return write_escaped(f, text.chars().map(Ok), external);
}
}
}
if is_bidi && crate::is_suspicious_bidi(text) {
return write_escaped(f, text.chars().map(Ok), external);
}
if !requires_quote {
f.write_str(text)
} else if external && has_ascii_double {
write_external_escaped(f, text)
} else if is_single_safe {
write_simple(f, text, '\'')
} else if is_double_safe {
write_simple(f, text, '\"')
} else {
write_single_escaped(f, text)
}
}
fn write_simple(f: &mut Formatter<'_>, text: &str, quote: char) -> fmt::Result {
f.write_char(quote)?;
f.write_str(text)?;
f.write_char(quote)?;
Ok(())
}
fn write_single_escaped(f: &mut Formatter<'_>, text: &str) -> fmt::Result {
f.write_char('\'')?;
let mut pos = 0;
for (index, _) in text.match_indices(unicode::is_single_quote) {
f.write_str(&text[pos..index])?;
f.write_char('\'')?;
pos = index;
}
f.write_str(&text[pos..])?;
f.write_char('\'')?;
Ok(())
}
fn write_external_escaped(f: &mut Formatter<'_>, text: &str) -> fmt::Result {
f.write_char('\'')?;
let mut pos = 0;
for (index, quote) in text.match_indices(|ch: char| ch == '"' || unicode::is_single_quote(ch)) {
f.write_str(&text[pos..index])?;
if quote == "\"" {
let backslashes = text[..index]
.chars()
.rev()
.take_while(|&ch| ch == '\\')
.count()
+ 1;
for _ in 0..backslashes {
f.write_char('\\')?;
}
} else {
f.write_char('\'')?;
}
pos = index;
}
f.write_str(&text[pos..])?;
f.write_char('\'')?;
Ok(())
}
pub(crate) fn write_escaped(
f: &mut Formatter<'_>,
text: impl Iterator<Item = Result<char, u16>>,
external: bool,
) -> fmt::Result {
f.write_char('"')?;
let mut backslashes: u32 = 0;
for ch in text {
match ch {
Ok(ch) => {
match ch {
'\0' => f.write_str("`0")?,
'\r' => f.write_str("`r")?,
'\n' => f.write_str("`n")?,
'\t' => f.write_str("`t")?,
'\x07' => f.write_str("`a")?,
'\x08' => f.write_str("`b")?,
'\x0b' => f.write_str("`v")?,
'\x0c' => f.write_str("`f")?,
ch if crate::requires_escape(ch) || crate::is_bidi(ch) => {
write!(f, "`u{{{:02X}}}", ch as u32)?
}
'`' => f.write_str("``")?,
'$' => f.write_str("`$")?,
'"' if external => {
for _ in 0..backslashes {
f.write_char('\\')?;
}
f.write_char('\\')?;
f.write_char('`')?;
f.write_char('"')?;
}
ch if unicode::is_double_quote(ch) => {
f.write_char('`')?;
f.write_char(ch)?;
}
ch => f.write_char(ch)?,
}
if ch == '\\' {
backslashes += 1;
} else {
backslashes = 0;
}
}
Err(unit) => write!(f, "`u{{{:04X}}}", unit)?,
}
}
f.write_char('"')?;
Ok(())
}
mod unicode {
pub(crate) fn is_whitespace(ch: char) -> bool {
match ch {
' ' | '\t' | '\x0B' | '\x0C' => true,
'\u{00A0}' | '\u{0085}' => true,
'\u{2800}' => true,
c => is_separator(c),
}
}
fn is_separator(ch: char) -> bool {
matches!(
ch,
'\u{0020}'
| '\u{00A0}'
| '\u{1680}'
| '\u{2000}'
| '\u{2001}'
| '\u{2002}'
| '\u{2003}'
| '\u{2004}'
| '\u{2005}'
| '\u{2006}'
| '\u{2007}'
| '\u{2008}'
| '\u{2009}'
| '\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
)
}
pub(crate) fn is_dash(ch: char) -> bool {
matches!(ch, '-' | '\u{2013}' | '\u{2014}' | '\u{2015}')
}
pub(crate) fn is_single_quote(ch: char) -> bool {
matches!(ch, '\'' | '\u{2018}' | '\u{2019}' | '\u{201A}' | '\u{201B}')
}
pub(crate) fn is_double_quote(ch: char) -> bool {
matches!(ch, '"' | '\u{201C}' | '\u{201D}' | '\u{201E}')
}
}