use core::fmt::{self, Formatter, Write};
use core::str::from_utf8;
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) -> fmt::Result {
let mut is_single_safe = true;
let mut is_double_safe = true;
let mut requires_quote = force_quote;
let mut is_bidi = false;
if !requires_quote {
if let Some(first) = text.chars().next() {
if SPECIAL_SHELL_CHARS_START.contains(&first) {
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 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.as_bytes());
}
} else {
if !requires_quote && (ch.is_whitespace() || ch == '\u{2800}') {
requires_quote = true;
}
if crate::is_bidi(ch) {
is_bidi = true;
}
if crate::requires_escape(ch) {
return write_escaped(f, text.as_bytes());
}
}
}
if is_bidi && crate::is_suspicious_bidi(text) {
return write_escaped(f, text.as_bytes());
}
if !requires_quote {
f.write_str(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 {
let mut iter = text.split('\'');
if let Some(chunk) = iter.next() {
if !chunk.is_empty() {
write_simple(f, chunk, '\'')?;
}
}
for chunk in iter {
f.write_str("\\'")?;
if !chunk.is_empty() {
write_simple(f, chunk, '\'')?;
}
}
Ok(())
}
pub(crate) fn write_escaped(f: &mut Formatter<'_>, text: &[u8]) -> fmt::Result {
f.write_str("$'")?;
let mut in_escape = false;
for chunk in from_utf8_iter(text) {
match chunk {
Ok(chunk) => {
for ch in chunk.chars() {
let was_escape = in_escape;
in_escape = false;
match ch {
'\n' => f.write_str("\\n")?,
'\t' => f.write_str("\\t")?,
'\r' => f.write_str("\\r")?,
ch if crate::requires_escape(ch) || crate::is_bidi(ch) => {
for &byte in ch.encode_utf8(&mut [0; 4]).as_bytes() {
write!(f, "\\x{:02X}", byte)?;
}
in_escape = true;
}
'\\' | '\'' => {
f.write_char('\\')?;
f.write_char(ch)?;
}
ch if was_escape && ch.is_ascii_hexdigit() => {
f.write_str("'$'")?;
f.write_char(ch)?;
}
ch => {
f.write_char(ch)?;
}
}
}
}
Err(unit) => {
write!(f, "\\x{:02X}", unit)?;
in_escape = true;
}
}
}
f.write_char('\'')?;
Ok(())
}
fn from_utf8_iter(bytes: &[u8]) -> impl Iterator<Item = Result<&str, u8>> {
struct Iter<'a> {
bytes: &'a [u8],
}
impl<'a> Iterator for Iter<'a> {
type Item = Result<&'a str, u8>;
fn next(&mut self) -> Option<Self::Item> {
if self.bytes.is_empty() {
return None;
}
match from_utf8(self.bytes) {
Ok(text) => {
self.bytes = &[];
Some(Ok(text))
}
Err(err) if err.valid_up_to() == 0 => {
let res = self.bytes[0];
self.bytes = &self.bytes[1..];
Some(Err(res))
}
Err(err) => {
let (valid, rest) = self.bytes.split_at(err.valid_up_to());
self.bytes = rest;
Some(Ok(from_utf8(valid).unwrap()))
}
}
}
}
Iter { bytes }
}
#[cfg(feature = "std")]
#[cfg(test)]
mod tests {
use super::*;
use std::vec::Vec;
#[test]
fn test_utf8_iter() {
type ByteStr = &'static [u8];
type Chunk = Result<&'static str, u8>;
const CASES: &[(ByteStr, &[Chunk])] = &[
(b"", &[]),
(b"hello", &[Ok("hello")]),
(b"\xFF", &[Err(b'\xFF')]),
(b"\xC2", &[Err(b'\xC2')]),
(b"\xF4\x8F", &[Err(b'\xF4'), Err(b'\x8F')]),
(b"\xFF\xFF", &[Err(b'\xFF'), Err(b'\xFF')]),
(b"hello\xC2", &[Ok("hello"), Err(b'\xC2')]),
(b"\xFFhello", &[Err(b'\xFF'), Ok("hello")]),
(b"\xFF\xC2hello", &[Err(b'\xFF'), Err(b'\xC2'), Ok("hello")]),
(b"foo\xFFbar", &[Ok("foo"), Err(b'\xFF'), Ok("bar")]),
(
b"foo\xF4\x8Fbar",
&[Ok("foo"), Err(b'\xF4'), Err(b'\x8F'), Ok("bar")],
),
(
b"foo\xFF\xC2bar",
&[Ok("foo"), Err(b'\xFF'), Err(b'\xC2'), Ok("bar")],
),
];
for &(case, expected) in CASES {
assert_eq!(
from_utf8_iter(case).collect::<Vec<_>>().as_slice(),
expected
);
}
}
}