use anyhow::anyhow;
fn unesc(c: &char) -> u8 {
match c {
'a' => b'\x07',
'b' => b'\x08',
'f' => b'\x0C',
'n' => b'\x0A',
'r' => b'\x0D',
't' => b'\x09',
'v' => b'\x0B',
'\\' => b'\\',
'\'' => b'\'',
'"' => b'"',
_ => unreachable!(),
}
}
pub enum DecodedSequence {
String(String),
Bytes(Vec<u8>),
}
pub fn unquote(quoted_str: &str) -> anyhow::Result<DecodedSequence> {
let mut quoted = quoted_str;
let mut is_byte = false;
let mut raw = false;
if quoted.starts_with('r') {
raw = true;
quoted = "ed[1..]
}
if quoted.starts_with('b') {
is_byte = true;
quoted = "ed[1..]
}
if quoted.len() < 2 {
return Err(anyhow!("string literal too short"));
}
let first = quoted.chars().next().unwrap();
if first != '"' && first != '\'' || first != quoted.chars().last().unwrap() {
return Err(anyhow!("string literal {quoted} has invalid quotes"));
}
let quote = quoted.chars().next().unwrap();
if quoted.len() >= 6
&& quoted.chars().nth(1).unwrap() == quote
&& quoted.chars().nth(2).unwrap() == quote
&& quoted[..3] == quoted[quoted.len() - 3..]
{
quoted = "ed[3..quoted.len() - 3]
} else {
quoted = "ed[1..quoted.len() - 1]
}
let unquote_chars = if raw { "\r" } else { "\\\r" };
if !quoted.chars().any(|x| unquote_chars.contains(x)) {
return if is_byte {
Ok(DecodedSequence::Bytes(quoted.into()))
} else {
Ok(DecodedSequence::String(quoted.to_string()))
};
}
let mut buf: Vec<u8> = vec![];
loop {
match quoted.chars().position(|c| unquote_chars.contains(c)) {
Some(i) => {
(quoted[..i]).chars().for_each(|c| buf.push(c as u8));
quoted = "ed[i..];
}
_ => {
quoted.chars().for_each(|c| buf.push(c as u8));
break;
}
}
if quoted.starts_with('\r') {
buf.push(b'\n');
quoted = if quoted.len() > 1 && quoted.chars().nth(1).unwrap() == '\n' {
"ed[2..]
} else {
"ed[1..]
};
continue;
}
if quoted.len() == 1 {
return Err(anyhow!("truncated escape sequence \\"));
}
match quoted.chars().nth(1) {
Some('\n') =>
{
quoted = "ed[2..]
}
Some('a' | 'b' | 'f' | 'n' | 'r' | 't' | 'v' | '\\' | '\'' | '"') => {
buf.push(unesc("ed.chars().nth(1).unwrap()));
quoted = "ed[2..]
}
Some('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7') => {
let mut n = quoted.chars().nth(1).unwrap().to_digit(8).unwrap();
quoted = "ed[2..];
for i in 1..3 {
if quoted.is_empty()
|| quoted.chars().nth(i).unwrap() < '0'
|| '7' < quoted.chars().next().unwrap()
{
break;
}
n = n * 8 + quoted.chars().next().unwrap().to_digit(8).unwrap();
quoted = "ed[1..];
}
if !is_byte && n > 127 {
return Err(anyhow!(
"non-ASCII octal escape \\{n:o} (use \\u{n:04x} for the UTF-8 encoding of U+{n:04x})",
));
}
if n >= 256 {
return Err(anyhow!("invalid escape sequence \\{n:03}o"));
}
buf.push(0 ) }
Some('x') => {
if quoted.len() < 4 {
return Err(anyhow!("truncated escape sequence {quoted}"));
}
match u32::from_str_radix("ed[2..4], 16) {
Ok(n) => {
if !is_byte && n > 127 {
return Err(anyhow!(
"non-ASCII hex escape {} (use \\u{n:04X} for the UTF-8 encoding of U+{n:04x})",
"ed[..4],
));
}
let decoded_ch = char::from_u32(n);
if decoded_ch.is_none() {
return Err(anyhow!("invalid Unicode code point U{n:04x}"));
}
let mut tmp: [u8; 4] = [0; 4];
let encoded = char::encode_utf8(decoded_ch.unwrap(), &mut tmp);
encoded.as_bytes().iter().for_each(|b| buf.push(*b));
quoted = "ed[4..]
}
_ => return Err(anyhow!("could not parse unicode codepoint {quoted}")),
}
}
Some('u' | 'U') => {
let mut sz = 6;
if quoted.chars().nth(1).unwrap() == 'U' {
sz = 10
}
if quoted.len() < sz {
return Err(anyhow!("truncated escape sequence {quoted}"));
}
match u32::from_str_radix("ed[2..sz], 16) {
Ok(n) => {
if (0xd800u32..0xe000u32).contains(&n) {
return Err(anyhow!("invalid Unicode code point U{n:04x}"));
}
if n > 0x10FFFFu32 {
return Err(anyhow!(
"code point out of range: {} (max \\U{:08x})",
"ed[..sz],
n
));
}
let decoded_ch = char::from_u32(n);
if decoded_ch.is_none() {
return Err(anyhow!("invalid Unicode code point U{n:04x}"));
}
let mut tmp: [u8; 8] = [0; 8];
let encoded = char::encode_utf8(decoded_ch.unwrap(), &mut tmp); encoded.as_bytes().iter().for_each(|b| buf.push(*b));
quoted = "ed[sz..]
}
_ => return Err(anyhow!("failed to parse unicode code point: {quoted}")),
}
}
_ =>
{
return Err(anyhow!("invalid escape sequence \\{quoted}"));
}
}
}
if is_byte {
return Ok(DecodedSequence::Bytes(buf));
}
let buf_utf8 = String::from_utf8(buf)?;
Ok(DecodedSequence::String(buf_utf8))
}
pub fn quote(s: &str) -> String {
let mut buf = "\"".to_string();
for c in s.chars() {
if c == '\'' {
buf.push(c);
continue;
}
c.escape_default().for_each(|c| buf.push(c));
}
buf.push('"');
buf
}