#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum EscapeError {
#[error(
"the escape sequence '{sequence}' at byte {at} is illegal; the legal forms are \\r \\n \\t \\\\ \\\" \\' and \\u"
)]
IllegalEscape {
sequence: String,
at: usize,
},
#[error(
"the string body ends with an unpaired backslash at byte {at}; a literal backslash is written \\\\"
)]
DanglingBackslash {
at: usize,
},
#[error(
"the unicode escape at byte {at} carries {digits} hex digits; the \\u forms take 4 (BMP) or 8 (non-BMP)"
)]
MalformedUnicodeEscape {
digits: usize,
at: usize,
},
#[error(
"the 8-digit unicode escape '\\u{escape}' denotes U+{code_point:04X}, outside the U+10000-U+10FFFF range the 8-digit form encodes"
)]
NonBmpEscapeOutOfRange {
escape: String,
code_point: u32,
},
#[error(
"the 8-digit unicode escape '\\u{escape}' opens with a high surrogate but its second half is not a low surrogate (DC00-DFFF)"
)]
MalformedSurrogatePair {
escape: String,
},
#[error(
"the unicode escape '\\u{escape}' names the surrogate code point U+{code_point:04X}, which is not a character"
)]
LoneSurrogate {
escape: String,
code_point: u32,
},
}
const HIGH_SURROGATE_FIRST: u32 = 0xD800;
const HIGH_SURROGATE_LAST: u32 = 0xDBFF;
const LOW_SURROGATE_FIRST: u32 = 0xDC00;
const LOW_SURROGATE_LAST: u32 = 0xDFFF;
const NON_BMP_FIRST: u32 = 0x0001_0000;
const NON_BMP_LAST: u32 = 0x0010_FFFF;
pub fn decode(inner: &str) -> Result<String, EscapeError> {
if !inner.contains('\\') {
return Ok(inner.to_owned());
}
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
let mut at = 0usize;
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
at += c.len_utf8();
continue;
}
match chars.next() {
Some('r') => out.push('\r'),
Some('n') => out.push('\n'),
Some('t') => out.push('\t'),
Some('\\') => out.push('\\'),
Some('"') => out.push('"'),
Some('\'') => out.push('\''),
Some('u') => at += decode_unicode(&mut chars, &mut out, at)?,
Some(other) => {
return Err(EscapeError::IllegalEscape {
sequence: format!("\\{other}"),
at,
});
}
None => return Err(EscapeError::DanglingBackslash { at }),
}
at += 2;
}
Ok(out)
}
pub fn validate(inner: &str) -> Result<(), EscapeError> {
decode(inner).map(|_| ())
}
pub fn decode_string_literal(raw: &str) -> Result<String, EscapeError> {
decode(strip_delimiters(raw, '"'))
}
pub fn decode_character_literal(raw: &str) -> Result<String, EscapeError> {
decode(strip_delimiters(raw, '\''))
}
fn strip_delimiters(raw: &str, delimiter: char) -> &str {
raw.strip_prefix(delimiter)
.and_then(|s| s.strip_suffix(delimiter))
.unwrap_or(raw)
}
fn decode_unicode(
chars: &mut std::str::Chars<'_>,
out: &mut String,
at: usize,
) -> Result<usize, EscapeError> {
let digits: String = chars
.clone()
.take(8)
.take_while(char::is_ascii_hexdigit)
.collect();
let digit_count = digits.chars().count();
let first: String = digits.chars().take(4).collect();
let first_value = if digit_count >= 4 {
hex_value(&first)
} else {
None
};
let Some(first_value) = first_value else {
return Err(EscapeError::MalformedUnicodeEscape {
digits: digit_count,
at,
});
};
let has_eight = digit_count >= 8;
if has_eight && (HIGH_SURROGATE_FIRST..=HIGH_SURROGATE_LAST).contains(&first_value) {
let second: String = digits.chars().skip(4).take(4).collect();
let low = hex_value(&second)
.filter(|low| (LOW_SURROGATE_FIRST..=LOW_SURROGATE_LAST).contains(low));
let Some(low) = low else {
return Err(EscapeError::MalformedSurrogatePair { escape: digits });
};
let code_point = NON_BMP_FIRST
+ (first_value - HIGH_SURROGATE_FIRST) * 0x400
+ (low - LOW_SURROGATE_FIRST);
push_non_bmp(code_point, digits, out)?;
advance(chars, 8);
return Ok(8);
}
if has_eight && first_value <= 0x0001 {
let Some(code_point) = hex_value(&digits) else {
return Err(EscapeError::MalformedUnicodeEscape {
digits: digit_count,
at,
});
};
push_non_bmp(code_point, digits, out)?;
advance(chars, 8);
return Ok(8);
}
if (HIGH_SURROGATE_FIRST..=LOW_SURROGATE_LAST).contains(&first_value) {
return Err(EscapeError::LoneSurrogate {
escape: first,
code_point: first_value,
});
}
let Some(ch) = char::from_u32(first_value) else {
return Err(EscapeError::LoneSurrogate {
escape: first,
code_point: first_value,
});
};
out.push(ch);
advance(chars, 4);
Ok(4)
}
fn push_non_bmp(code_point: u32, escape: String, out: &mut String) -> Result<(), EscapeError> {
if !(NON_BMP_FIRST..=NON_BMP_LAST).contains(&code_point) {
return Err(EscapeError::NonBmpEscapeOutOfRange { escape, code_point });
}
let Some(ch) = char::from_u32(code_point) else {
return Err(EscapeError::NonBmpEscapeOutOfRange { escape, code_point });
};
out.push(ch);
Ok(())
}
fn hex_value(digits: &str) -> Option<u32> {
if digits.is_empty() || digits.len() > 8 {
return None;
}
u32::from_str_radix(digits, 16).ok()
}
fn advance(chars: &mut std::str::Chars<'_>, n: usize) {
for _ in 0..n {
chars.next();
}
}
#[cfg(test)]
mod tests {
use super::{EscapeError, decode};
fn decoded(inner: &str) -> String {
decode(inner).expect("the fixture should decode")
}
#[test]
fn the_six_quoted_forms_decode() {
assert_eq!(decoded(r"a\rb\nc\td\\e"), "a\rb\nc\td\\e");
assert_eq!(decoded(r#"\""#), "\"");
assert_eq!(decoded(r"\'"), "'");
}
#[test]
fn four_digit_escapes_decode_bmp_code_points() {
assert_eq!(decoded(r"\u0041"), "A");
assert_eq!(decoded(r"\u00E9t\u00e9"), "\u{e9}t\u{e9}");
assert_eq!(decoded(r"\uFFFD"), "\u{FFFD}");
}
#[test]
fn surrogate_pairs_decode_non_bmp_code_points() {
assert_eq!(decoded(r"\uD800DC00"), "\u{10000}");
assert_eq!(decoded(r"\uDBFFDFFF"), "\u{10FFFF}");
assert_eq!(decoded(r"\uD83DDE00"), "\u{1F600}");
}
#[test]
fn zero_filled_eight_digit_escapes_decode() {
assert_eq!(decoded(r"\u00010000"), "\u{10000}");
assert_eq!(decoded(r"\u0001F600"), "\u{1F600}");
}
#[test]
fn an_eight_digit_bmp_value_is_refused() {
assert_eq!(
decode(r"\u0000FFFF"),
Err(EscapeError::NonBmpEscapeOutOfRange {
escape: "0000FFFF".to_owned(),
code_point: 0xFFFF,
})
);
}
#[test]
fn a_high_surrogate_without_a_low_half_is_refused() {
assert_eq!(
decode(r"\uD8000041"),
Err(EscapeError::MalformedSurrogatePair {
escape: "D8000041".to_owned(),
})
);
}
#[test]
fn lone_and_reversed_surrogates_are_refused() {
assert_eq!(
decode(r"\uD800"),
Err(EscapeError::LoneSurrogate {
escape: "D800".to_owned(),
code_point: 0xD800,
})
);
assert_eq!(
decode(r"\uDC00D800"),
Err(EscapeError::LoneSurrogate {
escape: "DC00".to_owned(),
code_point: 0xDC00,
})
);
}
#[test]
fn hex_text_after_a_four_digit_escape_stays_literal() {
assert_eq!(decoded(r"\u00410042"), "A0042");
}
#[test]
fn a_backslash_sequence_outside_the_sanctioned_set_is_refused() {
for (body, sequence, at) in [
(r"\q", r"\q", 0),
(r"a\d", r"\d", 1),
(r"ab\s.*", r"\s", 2),
(r"\0", r"\0", 0),
(r"\ ", r"\ ", 0),
(r"\U0041", r"\U", 0),
] {
assert_eq!(
decode(body),
Err(EscapeError::IllegalEscape {
sequence: sequence.to_owned(),
at,
}),
"{body:?} must be refused"
);
}
}
#[test]
fn the_illegal_escape_offset_counts_authored_bytes() {
assert_eq!(
decode("\u{e9}\\q"),
Err(EscapeError::IllegalEscape {
sequence: r"\q".to_owned(),
at: 2,
})
);
assert_eq!(
decode(r"\t\q"),
Err(EscapeError::IllegalEscape {
sequence: r"\q".to_owned(),
at: 2,
})
);
assert_eq!(
decode("\\u00E9\\q"),
Err(EscapeError::IllegalEscape {
sequence: r"\q".to_owned(),
at: 6,
})
);
}
#[test]
fn an_unpaired_trailing_backslash_is_refused() {
assert_eq!(decode("\\"), Err(EscapeError::DanglingBackslash { at: 0 }));
assert_eq!(
decode(r"a\\b\"),
Err(EscapeError::DanglingBackslash { at: 4 })
);
}
#[test]
fn a_unicode_escape_shorter_than_four_digits_is_refused() {
for (body, digits) in [(r"\u", 0), (r"\u4", 1), (r"\u00", 2), (r"\uFFF", 3)] {
assert_eq!(
decode(body),
Err(EscapeError::MalformedUnicodeEscape { digits, at: 0 }),
"{body:?} must be refused"
);
}
}
#[test]
fn the_whole_sanctioned_set_decodes_together() {
assert_eq!(
decoded("\\r\\n\\t\\\\\\\"\\'\\u00E9\\uD83DDE00\\u0001F600"),
"\r\n\t\\\"'\u{e9}\u{1F600}\u{1F600}"
);
}
}