#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ControlStyle {
HexU,
HexX,
}
pub(crate) struct EscapeSpec {
pub named: &'static [(char, &'static str)],
pub control: ControlStyle,
pub also_escape: &'static [char],
}
pub(crate) const JSON_ESCAPES: EscapeSpec = EscapeSpec {
named: &[
('"', "\\\""),
('\\', "\\\\"),
('\n', "\\n"),
('\r', "\\r"),
('\t', "\\t"),
('\u{08}', "\\b"),
('\u{0c}', "\\f"),
],
control: ControlStyle::HexU,
also_escape: &[],
};
pub(crate) const TOML_ESCAPES: EscapeSpec = EscapeSpec {
named: JSON_ESCAPES.named,
control: ControlStyle::HexU,
also_escape: &['\u{7f}'],
};
pub(crate) const OML_ESCAPES: EscapeSpec = EscapeSpec {
named: &[
('"', "\\\""),
('\\', "\\\\"),
('\n', "\\n"),
('\r', "\\r"),
('\t', "\\t"),
],
control: ControlStyle::HexU,
also_escape: &[],
};
pub(crate) const YAML_ESCAPES: EscapeSpec = EscapeSpec {
named: &[
('"', "\\\""),
('\\', "\\\\"),
('\n', "\\n"),
('\t', "\\t"),
('\u{0085}', "\\N"),
],
control: ControlStyle::HexX,
also_escape: &[],
};
pub(crate) fn write_quoted(s: &str, spec: &EscapeSpec, out: &mut String) {
out.push('"');
for c in s.chars() {
if let Some((_, escaped)) = spec.named.iter().find(|(named_c, _)| *named_c == c) {
out.push_str(escaped);
} else if (c as u32) < 0x20 || spec.also_escape.contains(&c) {
match spec.control {
ControlStyle::HexU => out.push_str(&format!("\\u{:04x}", c as u32)),
ControlStyle::HexX => out.push_str(&format!("\\x{:02x}", c as u32)),
}
} else {
out.push(c);
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
fn old_json(s: &str) -> String {
let mut out = String::new();
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
fn old_toml(s: &str) -> String {
let mut out = String::new();
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 || c == '\u{7f}' => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
out
}
fn old_oml(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
fn old_yaml_quoted_branch(s: &str) -> String {
let mut out = String::new();
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\u{0085}' => out.push_str("\\N"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\x{:02x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
fn new_out(s: &str, spec: &EscapeSpec) -> String {
let mut out = String::new();
write_quoted(s, spec, &mut out);
out
}
fn exhaustive_single_char_corpus() -> Vec<String> {
let mut v = Vec::new();
for cp in 0u32..=0x10FFFF {
if let Some(c) = char::from_u32(cp) {
v.push(c.to_string());
}
}
v
}
fn extra_corpus() -> Vec<String> {
vec![
String::new(),
"plain ascii, nothing to escape".to_string(),
"quote\"quote\"quote\"".to_string(),
"back\\slash\\back\\slash".to_string(),
"\r\r\r\r".to_string(),
"\n\n\n\n".to_string(),
"\t\t\t\t".to_string(),
"\u{08}\u{08}\u{0c}\u{0c}".to_string(),
"\u{01}\u{01}\u{01}".to_string(),
"\u{7f}\u{7f}\u{7f}".to_string(),
"\u{0085}\u{0085}".to_string(),
"mixed \" \\ \n \r \t \u{08} \u{0c} \u{01} \u{7f} \u{0085} end".to_string(),
"unicode: héllo wörld 日本語 🎉🎉🎉".to_string(),
"\u{1}\"\\\n\r\t\u{1}\"\\\n\r\t".to_string(),
]
}
#[test]
fn json_equivalence_exhaustive_and_extra_corpus() {
let mut checked = 0usize;
for s in exhaustive_single_char_corpus()
.into_iter()
.chain(extra_corpus())
{
assert_eq!(
old_json(&s),
new_out(&s, &JSON_ESCAPES),
"mismatch for {s:?}"
);
checked += 1;
}
assert!(checked > 1_100_000, "expected >1.1M cases, got {checked}");
}
#[test]
fn toml_equivalence_exhaustive_and_extra_corpus() {
let mut checked = 0usize;
for s in exhaustive_single_char_corpus()
.into_iter()
.chain(extra_corpus())
{
assert_eq!(
old_toml(&s),
new_out(&s, &TOML_ESCAPES),
"mismatch for {s:?}"
);
checked += 1;
}
assert!(checked > 1_100_000, "expected >1.1M cases, got {checked}");
}
#[test]
fn oml_equivalence_exhaustive_and_extra_corpus() {
let mut checked = 0usize;
for s in exhaustive_single_char_corpus()
.into_iter()
.chain(extra_corpus())
{
assert_eq!(old_oml(&s), new_out(&s, &OML_ESCAPES), "mismatch for {s:?}");
checked += 1;
}
assert!(checked > 1_100_000, "expected >1.1M cases, got {checked}");
}
#[test]
fn yaml_equivalence_exhaustive_and_extra_corpus() {
let mut checked = 0usize;
for s in exhaustive_single_char_corpus()
.into_iter()
.chain(extra_corpus())
{
assert_eq!(
old_yaml_quoted_branch(&s),
new_out(&s, &YAML_ESCAPES),
"mismatch for {s:?}"
);
checked += 1;
}
assert!(checked > 1_100_000, "expected >1.1M cases, got {checked}");
}
#[test]
fn json_escapes_every_occurrence_not_just_the_first() {
let out = new_out("a\"b\"c\\d\\e\u{01}f\u{01}", &JSON_ESCAPES);
assert_eq!(out, "\"a\\\"b\\\"c\\\\d\\\\e\\u0001f\\u0001\"");
}
#[test]
fn toml_escapes_every_occurrence_not_just_the_first() {
let out = new_out("a\u{7f}b\u{7f}c\"d\"e", &TOML_ESCAPES);
assert_eq!(out, "\"a\\u007fb\\u007fc\\\"d\\\"e\"");
}
#[test]
fn oml_escapes_every_occurrence_not_just_the_first() {
let out = new_out("a\rb\rc\nd\ne", &OML_ESCAPES);
assert_eq!(out, "\"a\\rb\\rc\\nd\\ne\"");
}
#[test]
fn yaml_escapes_every_occurrence_not_just_the_first() {
let out = new_out("a\u{0085}b\u{0085}c\"d\"e", &YAML_ESCAPES);
assert_eq!(out, "\"a\\Nb\\Nc\\\"d\\\"e\"");
}
#[test]
fn adjacent_repeats_of_the_same_illegal_char_are_all_escaped() {
assert_eq!(
new_out("\u{01}\u{01}\u{01}", &JSON_ESCAPES),
"\"\\u0001\\u0001\\u0001\""
);
assert_eq!(
new_out("\u{7f}\u{7f}\u{7f}", &TOML_ESCAPES),
"\"\\u007f\\u007f\\u007f\""
);
assert_eq!(new_out("\r\r\r", &OML_ESCAPES), "\"\\r\\r\\r\"");
assert_eq!(
new_out("\u{0085}\u{0085}\u{0085}", &YAML_ESCAPES),
"\"\\N\\N\\N\""
);
}
}