pub(crate) fn unquote_c_path(path: &str) -> String {
let bytes = path.as_bytes();
if bytes.len() < 2 || bytes[0] != b'"' || bytes[bytes.len() - 1] != b'"' {
return path.to_string();
}
let inner = &bytes[1..bytes.len() - 1];
let mut out: Vec<u8> = Vec::with_capacity(inner.len());
let mut i = 0;
while i < inner.len() {
if inner[i] != b'\\' || i + 1 >= inner.len() {
out.push(inner[i]);
i += 1;
continue;
}
let next = inner[i + 1];
if (b'0'..=b'7').contains(&next) {
let mut value: u32 = 0;
let mut k = i + 1;
while k < inner.len() && k < i + 4 && (b'0'..=b'7').contains(&inner[k]) {
value = value * 8 + u32::from(inner[k] - b'0');
k += 1;
}
out.push(value as u8);
i = k;
} else {
let decoded = match next {
b'a' => 0x07,
b'b' => 0x08,
b't' => b'\t',
b'n' => b'\n',
b'v' => 0x0b,
b'f' => 0x0c,
b'r' => b'\r',
other => other, };
out.push(decoded);
i += 2;
}
}
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unquote_c_path_decodes_octal_and_named_escapes() {
assert_eq!(
unquote_c_path("\"src/f\\303\\266\\303\\266.py\""),
"src/föö.py"
);
assert_eq!(unquote_c_path("\"a\\tb\\\"c\\\\d\""), "a\tb\"c\\d");
assert_eq!(
unquote_c_path("\"\\a\\b\\n\\v\\f\\r\""),
"\u{7}\u{8}\n\u{b}\u{c}\r"
);
assert_eq!(unquote_c_path("\"\\1015\""), "A5");
}
#[test]
fn unquote_c_path_leaves_an_unquoted_path_unchanged() {
assert_eq!(unquote_c_path("src/föö.py"), "src/föö.py");
assert_eq!(unquote_c_path("\""), "\"");
assert_eq!(unquote_c_path(""), "");
assert_eq!(unquote_c_path("\"a\\\""), "a\\");
}
#[test]
fn a_path_quoted_on_only_one_end_is_not_a_quoted_path() {
assert_eq!(unquote_c_path("\"abc"), "\"abc");
assert_eq!(unquote_c_path("abc\""), "abc\"");
}
#[test]
fn an_empty_quoted_path_decodes_to_the_empty_string() {
assert_eq!(unquote_c_path("\"\""), "");
}
#[test]
fn an_octal_escape_shorter_than_three_digits_ends_at_the_closing_quote() {
assert_eq!(unquote_c_path("\"\\12\""), "\n");
}
}