use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
use crate::ascii::Char;
pub fn escape<T: Into<OsString>>(s: T) -> Vec<u8> {
let sin = s.into().into_vec();
match escape_prepare(&sin) {
Prepared::Empty => vec![b'\'', b'\''],
Prepared::Inert => sin,
Prepared::Escape(esc) => {
let size: usize = esc.iter().map(escape_size).sum();
let mut sout = Vec::with_capacity(size + 3);
escape_chars(esc, &mut sout); sout
}
}
}
pub fn quote<T: Into<OsString>>(s: T) -> OsString {
OsString::from_vec(escape(s))
}
pub fn escape_into<T: Into<OsString>>(s: T, sout: &mut Vec<u8>) {
let sin = s.into().into_vec();
match escape_prepare(&sin) {
Prepared::Empty => sout.extend(b"''"),
Prepared::Inert => sout.extend(sin),
Prepared::Escape(esc) => {
let size: usize = esc.iter().map(escape_size).sum();
sout.reserve(size + 3);
escape_chars(esc, sout); }
}
}
enum Prepared {
Empty,
Inert,
Escape(Vec<Char>),
}
fn escape_prepare(sin: &[u8]) -> Prepared {
let esc: Vec<_> = sin.iter().map(Char::from).collect();
if esc.is_empty() {
Prepared::Empty
} else if esc.iter().all(Char::is_inert) {
Prepared::Inert
} else {
Prepared::Escape(esc)
}
}
fn escape_chars(esc: Vec<Char>, sout: &mut Vec<u8>) {
sout.extend(b"$'");
for mode in esc {
use Char::*;
match mode {
Bell => sout.extend(b"\\a"),
Backspace => sout.extend(b"\\b"),
Escape => sout.extend(b"\\e"),
FormFeed => sout.extend(b"\\f"),
NewLine => sout.extend(b"\\n"),
CarriageReturn => sout.extend(b"\\r"),
HorizontalTab => sout.extend(b"\\t"),
VerticalTab => sout.extend(b"\\v"),
Control(ch) => sout.extend(format!("\\x{:02X}", ch).bytes()),
Backslash => sout.extend(b"\\\\"),
SingleQuote => sout.extend(b"\\'"),
DoubleQuote => sout.extend(b"\""),
Delete => sout.extend(b"\\x7F"),
PrintableInert(ch) => sout.push(ch),
Printable(ch) => sout.push(ch),
Extended(ch) => sout.extend(format!("\\x{:02X}", ch).bytes()),
}
}
sout.push(b'\'');
}
fn escape_size(char: &Char) -> usize {
use Char::*;
match char {
Bell => 2,
Backspace => 2,
Escape => 2,
FormFeed => 2,
NewLine => 2,
CarriageReturn => 2,
HorizontalTab => 2,
VerticalTab => 2,
Control(_) => 4,
Backslash => 2,
SingleQuote => 2,
DoubleQuote => 1,
Delete => 4,
PrintableInert(_) => 1,
Printable(_) => 1,
Extended(_) => 4,
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use std::os::unix::prelude::OsStringExt;
use crate::find_bins;
use super::{escape, escape_into, quote};
#[test]
fn test_lowercase_ascii() {
assert_eq!(
escape("abcdefghijklmnopqrstuvwxyz"),
b"abcdefghijklmnopqrstuvwxyz"
);
}
#[test]
fn test_uppercase_ascii() {
assert_eq!(
escape("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
);
}
#[test]
fn test_numbers() {
assert_eq!(escape("0123456789"), b"0123456789");
}
#[test]
fn test_punctuation() {
assert_eq!(escape("-_=/,.+"), b"$'-_=/,.+'");
}
#[test]
fn test_empty_string() {
assert_eq!(escape(""), b"''");
}
#[test]
fn test_basic_escapes() {
assert_eq!(escape(r#"woo"wah""#), br#"$'woo"wah"'"#);
}
#[test]
#[allow(non_snake_case)]
fn test_control_characters() {
assert_eq!(escape(&"\x00"), b"$'\\x00'");
assert_eq!(escape(&"\x07"), b"$'\\a'");
assert_eq!(escape(&"\x00"), b"$'\\x00'");
assert_eq!(escape(&"\x06"), b"$'\\x06'");
assert_eq!(escape(&"\x7F"), b"$'\\x7F'");
}
#[test]
fn test_escape_into_plain() {
let mut buffer = Vec::new();
escape_into("hello", &mut buffer);
assert_eq!(buffer, b"hello");
}
#[test]
fn test_escape_into_with_escapes() {
let mut buffer = Vec::new();
escape_into("-_=/,.+", &mut buffer);
assert_eq!(buffer, b"$'-_=/,.+'");
}
#[test]
fn test_roundtrip() {
use std::process::Command;
let mut script = b"echo -n ".to_vec();
let string: OsString = OsString::from_vec((1u8..=u8::MAX).collect());
escape_into(&string, &mut script);
let script = OsString::from_vec(script);
for bin in find_bins("bash") {
let output = Command::new(bin).arg("-c").arg(&script).output().unwrap();
let result = OsString::from_vec(output.stdout);
assert_eq!(result, string);
}
}
#[test]
fn test_quote() {
assert_eq!(
quote("abcdefghijklmnopqrstuvwxyz"),
OsString::from("abcdefghijklmnopqrstuvwxyz"),
);
}
}