pub fn decode_json_string(src: &str) -> String {
let b = src.as_bytes();
let mut out = String::with_capacity(src.len());
let mut i = 0;
while i < b.len() {
if b[i] != b'\\' {
let start = i;
i += 1;
while i < b.len() && (b[i] & 0xC0) == 0x80 {
i += 1;
}
out.push_str(&src[start..i]);
continue;
}
i += 1;
let Some(&c) = b.get(i) else { break };
i += 1;
match c {
b'r' => out.push('\r'),
b'n' => out.push('\n'),
b'\\' => out.push('\\'),
b'f' => out.push('\u{c}'),
b'b' => out.push('\u{8}'),
b't' => out.push('\t'),
b'v' => out.push('\u{b}'),
b'0' => out.push('\0'),
b'\n' => {}
b'\r' => {
if b.get(i) == Some(&b'\n') {
i += 1;
}
}
b'u' => {
let Some(cp) = hex_to_digit(b, i, 4) else {
out.push('u');
continue;
};
i += 4;
let ch = if (0xD800..0xDC00).contains(&cp) {
match (b.get(i), b.get(i + 1), hex_to_digit(b, i + 2, 4)) {
(Some(b'\\'), Some(b'u'), Some(lo)) if (0xDC00..0xE000).contains(&lo) => {
i += 6;
let scalar = 0x1_0000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
char::from_u32(scalar)
}
_ => None,
}
} else {
char::from_u32(cp)
};
out.push(ch.unwrap_or('?'));
}
b'x' => {
let Some(byte) = hex_to_digit(b, i, 2) else {
out.push('x');
continue;
};
i += 2;
out.push(byte as u8 as char);
}
other => out.push(other as char),
}
}
out
}
pub fn decode_json_string_token(tok: &str) -> Option<String> {
let t = tok.trim();
let inner = t
.strip_prefix('"')
.and_then(|r| r.strip_suffix('"'))
.or_else(|| t.strip_prefix('\'').and_then(|r| r.strip_suffix('\'')))?;
Some(decode_json_string(inner))
}
fn hex_to_digit(b: &[u8], at: usize, n: usize) -> Option<u32> {
let mut v: u32 = 0;
for k in 0..n {
let d = (*b.get(at + k)? as char).to_digit(16)?;
v = v << 4 | d;
}
Some(v)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn yajl_escapes() {
assert_eq!(decode_json_string(r"a\tb"), "a\tb");
assert_eq!(decode_json_string(r"a\nb"), "a\nb");
assert_eq!(decode_json_string(r"a\rb"), "a\rb");
assert_eq!(decode_json_string(r"a\fb"), "a\u{c}b");
assert_eq!(decode_json_string(r"a\bb"), "a\u{8}b");
assert_eq!(decode_json_string(r"a\vb"), "a\u{b}b");
assert_eq!(decode_json_string(r"x\\ny"), "x\\ny");
assert_eq!(decode_json_string(r#"a\"b"#), "a\"b");
assert_eq!(decode_json_string(r"a\0b"), "a\0b");
assert_eq!(decode_json_string(r"a\x41b"), "aAb");
assert_eq!(decode_json_string(r"aAb"), "aAb");
assert_eq!(decode_json_string(r"a\/b"), "a/b");
assert_eq!(decode_json_string(r"a\qb"), "aqb");
}
#[test]
fn line_continuation_contributes_nothing() {
assert_eq!(decode_json_string("a\\\nb"), "ab");
assert_eq!(decode_json_string("a\\\r\nb"), "ab");
}
#[test]
fn unicode_escapes_and_surrogate_pairs() {
assert_eq!(decode_json_string("\\u0041"), "A");
assert_eq!(decode_json_string("\\uD83D\\uDE00"), "\u{1F600}");
assert_eq!(decode_json_string("\\uD83Dx"), "?x");
}
#[test]
fn plain_text_is_untouched() {
assert_eq!(decode_json_string("hi there"), "hi there");
assert_eq!(decode_json_string("REC:AI.VAL"), "REC:AI.VAL");
}
#[test]
fn only_a_quoted_token_is_a_string() {
assert_eq!(
decode_json_string_token(r#""a\tb""#).as_deref(),
Some("a\tb")
);
assert_eq!(decode_json_string_token(r"'a\tb'").as_deref(), Some("a\tb"));
assert_eq!(decode_json_string_token("5"), None);
assert_eq!(decode_json_string_token("true"), None);
assert_eq!(decode_json_string_token("{a:1}"), None);
}
}