use crate::error::*;
use unicode_categories::UnicodeCategories;
fn needs_double_quote(c: char) -> bool {
matches!(c, '\'' | '\\') || c.is_whitespace() || c.is_separator() || c.is_other()
}
fn needs_quote(c: char) -> bool {
matches!(
c,
'"' | ' '
| '('
| ')'
| '&'
| '~'
| '$'
| '#'
| '`'
| ';'
| '*'
| '?'
| '!'
| '['
| '>'
| '<'
| '|'
)
}
fn append_quoted(out: &mut String, c: char) {
let s = match c {
' ' => r" ",
'$' => r"\$",
'`' => r"\`",
'\\' => r"\\",
'"' => r#"\""#,
'\x07' => r"\a",
'\x08' => r"\b",
'\x1b' => r"\e",
'\x0c' => r"\f",
'\x0b' => r"\v",
c if c.is_other() || c.is_separator() => {
out.extend(c.escape_default());
return;
}
c => {
out.push(c);
return;
}
};
out.push_str(s);
}
pub fn escape(s: &str) -> String {
let mut must_quote = false;
let must_double_quote = s.chars().any(|x| {
if needs_quote(x) {
must_quote = true;
false
} else {
needs_double_quote(x)
}
});
if !must_double_quote {
if !must_quote {
return s.to_string();
}
return format!("'{s}'");
}
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
append_quoted(&mut out, c);
}
out.push('"');
out
}
enum UnescapeState {
None,
Single,
Double,
DoubleEscape,
DoubleEscapeUnicode,
DoubleEscapeUnicodeBraced((u32, usize)),
}
pub fn unescape(s: &str) -> Result<String> {
let mut state = UnescapeState::None;
s.chars()
.try_fold(String::with_capacity(s.len()), |mut acc, c| {
match state {
UnescapeState::None => match c {
'\'' => state = UnescapeState::Single,
'"' => state = UnescapeState::Double,
c => acc.push(c),
},
UnescapeState::Single => match c {
'\'' => state = UnescapeState::None,
c => acc.push(c),
},
UnescapeState::Double => match c {
'"' => state = UnescapeState::None,
'\\' => state = UnescapeState::DoubleEscape,
c => acc.push(c),
},
UnescapeState::DoubleEscape => {
match c {
' ' | '\\' | '\'' | '"' | '$' | '`' => acc.push(c),
'a' => acc.push('\x07'),
'b' => acc.push('\x08'),
'e' | 'E' => acc.push('\x1B'),
'f' => acc.push('\x0C'),
'n' => acc.push('\n'),
'r' => acc.push('\r'),
't' => acc.push('\t'),
'v' => acc.push('\x0B'),
'u' => {
state = UnescapeState::DoubleEscapeUnicode;
return Ok(acc);
}
c => {
return Err(KeyringError::Generic(format!(
"invalid escape character: {c}"
)));
}
}
state = UnescapeState::Double;
}
UnescapeState::DoubleEscapeUnicode => match c {
'{' => state = UnescapeState::DoubleEscapeUnicodeBraced((0, 0)),
c => {
return Err(KeyringError::Generic(format!(
"expected unicode escape brace: {c}"
)));
}
},
UnescapeState::DoubleEscapeUnicodeBraced(v) => match c {
'}' => match v {
(_, 0) => {
return Err(KeyringError::Generic("empty unicode escape".to_string()));
}
(v, _) => {
let uc = char::from_u32(v).ok_or_else(|| {
KeyringError::Generic(format!("invalid unicode character: {v:x}"))
})?;
acc.push(uc);
state = UnescapeState::Double;
}
},
'0'..='9' | 'a'..='f' | 'A'..='F' => match v {
(_, 6..=0xFFFFFFFF) => {
return Err(KeyringError::Generic(
"overlong unicode escape".to_string(),
));
}
(v, cnt) => {
state = UnescapeState::DoubleEscapeUnicodeBraced((
v << 4 | c.to_digit(16).unwrap(),
cnt + 1,
));
}
},
c => {
return Err(KeyringError::Generic(format!(
"invalid character in unicode escape: {c}"
)));
}
},
};
Ok(acc)
})
}