use std::borrow::Cow;
pub(crate) fn quote_bare_denormals(b: &[u8]) -> Cow<'_, [u8]> {
if !contains(b, b"inf") && !contains(b, b"nan") {
return Cow::Borrowed(b);
}
let mut out: Vec<u8> = Vec::with_capacity(b.len() + 16);
let mut in_str = false;
let mut i = 0;
while i < b.len() {
let c = b[i];
if in_str {
out.push(c);
if c == b'\\' && i + 1 < b.len() {
i += 1;
out.push(b[i]);
} else if c == b'"' {
in_str = false;
}
i += 1;
continue;
}
if c == b'"' {
in_str = true;
out.push(c);
i += 1;
continue;
}
if let Some(&prev) = out.last() {
if !matches!(prev, b':' | b',' | b'[' | b' ' | b'\t' | b'\n') {
out.push(c);
i += 1;
continue;
}
}
let rest = &b[i..];
let tok = if rest.starts_with(b"-inf") {
4
} else if rest.starts_with(b"inf") || rest.starts_with(b"nan") {
3
} else {
0
};
if tok > 0 && (rest.len() == tok || matches!(rest[tok], b',' | b'}' | b']')) {
out.push(b'"');
out.extend_from_slice(&rest[..tok]);
out.push(b'"');
i += tok;
continue;
}
out.push(c);
i += 1;
}
Cow::Owned(out)
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Json {
Null,
Bool(bool),
Number(String),
Str(Vec<u8>),
Array(Vec<Json>),
Object(Vec<(Vec<u8>, Json)>),
}
impl Json {
pub(crate) fn kind(&self) -> &'static str {
match self {
Json::Null => "null",
Json::Bool(_) => "bool",
Json::Number(_) => "number",
Json::Str(_) => "string",
Json::Array(_) => "array",
Json::Object(_) => "object",
}
}
pub(crate) fn scalar_text(&self) -> Option<(Cow<'_, str>, bool)> {
match self {
Json::Number(n) => Some((Cow::Borrowed(n.as_str()), false)),
Json::Str(s) => Some((String::from_utf8_lossy(s), true)),
_ => None,
}
}
}
pub(crate) fn parse_exact(text: &str) -> Option<Json> {
let bytes = trim_json_ws(text.as_bytes());
if bytes.is_empty() {
return None;
}
let mut p = Parser { b: bytes, i: 0 };
let v = p.value()?;
p.ws();
if p.i != p.b.len() {
return None; }
Some(v)
}
pub(crate) fn parse_exact_bytes(b: &[u8]) -> Option<Json> {
if let Ok(text) = std::str::from_utf8(b) {
return parse_exact(text);
}
let b = trim_json_ws(b);
if b.is_empty() {
return None;
}
let mut p = Parser { b, i: 0 };
let v = p.value()?;
p.ws();
if p.i != p.b.len() {
return None;
}
Some(v)
}
fn trim_json_ws(mut b: &[u8]) -> &[u8] {
while let [first, rest @ ..] = b {
if matches!(first, b' ' | b'\t' | b'\n' | b'\r') {
b = rest;
} else {
break;
}
}
while let [rest @ .., last] = b {
if matches!(last, b' ' | b'\t' | b'\n' | b'\r') {
b = rest;
} else {
break;
}
}
b
}
pub(crate) fn decode_string_at(b: &[u8], at: usize) -> Option<(Vec<u8>, usize)> {
let mut p = Parser { b, i: at };
p.ws();
let s = p.string()?;
Some((s, p.i))
}
pub(crate) fn value_extent(b: &[u8], at: usize) -> Option<(usize, usize)> {
let mut s = Scan { b, i: at };
s.ws();
let start = s.i;
s.value()?;
Some((start, s.i))
}
struct Scan<'a> {
b: &'a [u8],
i: usize,
}
impl Scan<'_> {
fn ws(&mut self) {
while self.i < self.b.len() && matches!(self.b[self.i], b' ' | b'\t' | b'\n' | b'\r') {
self.i += 1;
}
}
fn peek(&self) -> Option<u8> {
self.b.get(self.i).copied()
}
fn lit(&mut self, word: &[u8]) -> Option<()> {
if self.b[self.i..].starts_with(word) {
self.i += word.len();
Some(())
} else {
None
}
}
fn value(&mut self) -> Option<()> {
self.ws();
match self.peek()? {
b'n' => self.lit(b"null"),
b't' => self.lit(b"true"),
b'f' => self.lit(b"false"),
b'"' => self.string(),
b'[' => self.seq(b']'),
b'{' => self.seq(b'}'),
c if c == b'-' || c.is_ascii_digit() => self.number(),
_ => None,
}
}
fn string(&mut self) -> Option<()> {
if self.peek()? != b'"' {
return None;
}
self.i += 1;
loop {
let c = self.peek()?;
self.i += 1;
match c {
b'"' => return Some(()),
b'\\' => {
self.peek()?;
self.i += 1;
}
_ => {}
}
}
}
fn seq(&mut self, close: u8) -> Option<()> {
self.i += 1; self.ws();
if self.peek()? == close {
self.i += 1;
return Some(());
}
loop {
self.value()?;
self.ws();
match self.peek()? {
b',' => self.i += 1,
b':' => self.i += 1,
c if c == close => {
self.i += 1;
return Some(());
}
_ => return None,
}
}
}
fn number(&mut self) -> Option<()> {
if self.peek() == Some(b'-') {
self.i += 1;
}
match self.peek()? {
b'0' => self.i += 1,
c if c.is_ascii_digit() => self.digits(),
_ => return None,
}
if self.peek() == Some(b'.') {
self.i += 1;
if !self.peek().is_some_and(|c| c.is_ascii_digit()) {
return None;
}
self.digits();
}
if matches!(self.peek(), Some(b'e') | Some(b'E')) {
self.i += 1;
if matches!(self.peek(), Some(b'+') | Some(b'-')) {
self.i += 1;
}
if !self.peek().is_some_and(|c| c.is_ascii_digit()) {
return None;
}
self.digits();
}
Some(())
}
fn digits(&mut self) {
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.i += 1;
}
}
}
struct Parser<'a> {
b: &'a [u8],
i: usize,
}
impl<'a> Parser<'a> {
fn ws(&mut self) {
while self.i < self.b.len() && matches!(self.b[self.i], b' ' | b'\t' | b'\n' | b'\r') {
self.i += 1;
}
}
fn peek(&self) -> Option<u8> {
self.b.get(self.i).copied()
}
fn eat(&mut self, c: u8) -> Option<()> {
if self.peek() == Some(c) {
self.i += 1;
Some(())
} else {
None
}
}
fn lit(&mut self, word: &[u8]) -> Option<()> {
if self.b[self.i..].starts_with(word) {
self.i += word.len();
Some(())
} else {
None
}
}
fn value(&mut self) -> Option<Json> {
self.ws();
match self.peek()? {
b'n' => self.lit(b"null").map(|_| Json::Null),
b't' => self.lit(b"true").map(|_| Json::Bool(true)),
b'f' => self.lit(b"false").map(|_| Json::Bool(false)),
b'"' => self.string().map(Json::Str),
b'[' => self.array(),
b'{' => self.object(),
c if c == b'-' || c.is_ascii_digit() => self.number(),
_ => None,
}
}
fn array(&mut self) -> Option<Json> {
self.eat(b'[')?;
let mut items = Vec::new();
self.ws();
if self.eat(b']').is_some() {
return Some(Json::Array(items));
}
loop {
items.push(self.value()?);
self.ws();
if self.eat(b',').is_some() {
continue;
}
self.eat(b']')?;
return Some(Json::Array(items));
}
}
fn object(&mut self) -> Option<Json> {
self.eat(b'{')?;
let mut members = Vec::new();
self.ws();
if self.eat(b'}').is_some() {
return Some(Json::Object(members));
}
loop {
self.ws();
let k = self.string()?;
self.ws();
self.eat(b':')?;
let v = self.value()?;
members.push((k, v));
self.ws();
if self.eat(b',').is_some() {
continue;
}
self.eat(b'}')?;
return Some(Json::Object(members));
}
}
fn string(&mut self) -> Option<Vec<u8>> {
self.eat(b'"')?;
let mut out = Vec::new();
loop {
let c = self.peek()?;
self.i += 1;
match c {
b'"' => return Some(out),
b'\\' => {
let e = self.peek()?;
self.i += 1;
match e {
b'"' => out.push(b'"'),
b'\\' => out.push(b'\\'),
b'/' => out.push(b'/'),
b'b' => out.push(0x08),
b'f' => out.push(0x0c),
b'n' => out.push(b'\n'),
b'r' => out.push(b'\r'),
b't' => out.push(b'\t'),
b'u' => {
let hi = self.hex4()?;
let ch = if (0xD800..0xDC00).contains(&hi) {
let save = self.i;
let lo = (|| {
self.eat(b'\\')?;
self.eat(b'u')?;
self.hex4()
})();
match lo {
Some(lo) if (0xDC00..0xE000).contains(&lo) => char::from_u32(
0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00),
),
_ => {
self.i = save;
None
}
}
} else {
char::from_u32(hi)
};
let ch = ch.unwrap_or('\u{fffd}');
let mut buf = [0u8; 4];
out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
}
_ => return None,
}
}
_ => out.push(c),
}
}
}
fn hex4(&mut self) -> Option<u32> {
if self.i + 4 > self.b.len() {
return None;
}
let s = std::str::from_utf8(&self.b[self.i..self.i + 4]).ok()?;
let v = u32::from_str_radix(s, 16).ok()?;
self.i += 4;
Some(v)
}
fn number(&mut self) -> Option<Json> {
let start = self.i;
let _ = self.eat(b'-');
match self.peek()? {
b'0' => self.i += 1,
c if c.is_ascii_digit() => {
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.i += 1;
}
let _ = c;
}
_ => return None,
}
if self.peek() == Some(b'.') {
self.i += 1;
if !self.peek().is_some_and(|c| c.is_ascii_digit()) {
return None;
}
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.i += 1;
}
}
if matches!(self.peek(), Some(b'e') | Some(b'E')) {
self.i += 1;
if matches!(self.peek(), Some(b'+') | Some(b'-')) {
self.i += 1;
}
if !self.peek().is_some_and(|c| c.is_ascii_digit()) {
return None;
}
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.i += 1;
}
}
let text = std::str::from_utf8(&self.b[start..self.i]).ok()?;
Some(Json::Number(text.to_string()))
}
}
pub(crate) fn denormal(s: &str) -> Option<&'static str> {
match s.trim().to_ascii_lowercase().as_str() {
"nan" | "__nan__" | "-nan" => Some("nan"),
"inf" | "infinity" | "__inf__" | "+inf" => Some("inf"),
"-inf" | "-infinity" | "__-inf__" => Some("-inf"),
_ => None,
}
}
pub(crate) fn num_eq(a: &str, b: &str) -> bool {
if a == b {
return true;
}
if denormal(a).is_some() || denormal(b).is_some() {
return denormal(a) == denormal(b);
}
match (Decimal::parse(a), Decimal::parse(b)) {
(Some(x), Some(y)) => x == y,
_ => false,
}
}
pub(crate) fn is_numeric(s: &str) -> bool {
!s.is_empty() && (denormal(s).is_some() || Decimal::parse(s).is_some())
}
#[derive(Debug, PartialEq, Eq)]
struct Decimal {
neg: bool,
digits: Vec<u8>,
exp: i64,
}
impl Decimal {
fn parse(s: &str) -> Option<Decimal> {
let s = s.trim();
let bytes = s.as_bytes();
if bytes.is_empty() {
return None;
}
let mut i = 0;
let mut neg = false;
match bytes[0] {
b'-' => {
neg = true;
i = 1;
}
b'+' => i = 1,
_ => {}
}
let mut digits: Vec<u8> = Vec::new();
let mut seen_digit = false;
while i < bytes.len() && bytes[i].is_ascii_digit() {
digits.push(bytes[i] - b'0');
seen_digit = true;
i += 1;
}
let mut exp: i64 = 0;
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
digits.push(bytes[i] - b'0');
seen_digit = true;
exp -= 1;
i += 1;
}
}
if !seen_digit {
return None;
}
if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
i += 1;
let mut esign = 1i64;
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
if bytes[i] == b'-' {
esign = -1;
}
i += 1;
}
let start = i;
let mut e: i64 = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
e = e
.saturating_mul(10)
.saturating_add((bytes[i] - b'0') as i64);
i += 1;
}
if i == start {
return None;
}
exp = exp.saturating_add(esign * e);
}
if i != bytes.len() {
return None; }
let first = digits.iter().position(|&d| d != 0);
match first {
None => Some(Decimal {
neg: false,
digits: Vec::new(),
exp: 0,
}),
Some(f) => {
digits.drain(..f);
while digits.last() == Some(&0) {
digits.pop();
exp += 1;
}
Some(Decimal { neg, digits, exp })
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn denormals_are_quoted_outside_strings_only() {
let doc = br#"{"stored":inf,"a":[nan,-inf],"s":"inf and nan","t":"x"}"#;
let got = quote_bare_denormals(doc);
assert_eq!(
std::str::from_utf8(&got).unwrap(),
r#"{"stored":"inf","a":["nan","-inf"],"s":"inf and nan","t":"x"}"#
);
}
#[test]
fn documents_without_denormals_are_untouched() {
let doc = br#"{"stored":0,"x":"hi"}"#;
assert!(matches!(quote_bare_denormals(doc), Cow::Borrowed(_)));
}
#[test]
fn identifiers_containing_inf_are_not_rewritten() {
let doc = br#"{"info":1,"err":"infinite loop"}"#;
let got = quote_bare_denormals(doc);
assert_eq!(
std::str::from_utf8(&got).unwrap(),
r#"{"info":1,"err":"infinite loop"}"#
);
}
#[test]
fn numbers_keep_their_exact_text() {
let v = parse_exact("18446744073709551615").unwrap();
assert_eq!(v, Json::Number("18446744073709551615".into()));
let v = parse_exact(
"-57896044618658097711785492504343953926634992332820282019728792003956564819968",
)
.unwrap();
assert!(matches!(v, Json::Number(ref n) if n.len() == 78));
}
#[test]
fn trailing_bytes_are_not_a_single_value() {
assert!(parse_exact("1 2").is_none());
assert!(parse_exact("\\N").is_none());
assert!(parse_exact("").is_none());
assert!(parse_exact("4,2").is_none());
assert!(parse_exact("1.2.3.4").is_none());
}
#[test]
fn containers_and_escapes_decode() {
let v = parse_exact(r#"{"a":[1,{"b":"xé"}],"c":null}"#).unwrap();
match v {
Json::Object(m) => {
assert_eq!(m.len(), 2);
assert_eq!(m[0].0, b"a");
}
other => panic!("expected object, got {other:?}"),
}
assert_eq!(
parse_exact(r#""😀""#).unwrap(),
Json::Str("😀".as_bytes().to_vec())
);
}
#[test]
fn exact_numeric_equality() {
assert!(num_eq("2.50", "2.5000000"));
assert!(num_eq("1", "1.000"));
assert!(num_eq("0", "-0.0"));
assert!(num_eq("1e3", "1000"));
assert!(num_eq("18446744073709551615", "1.8446744073709551615e19"));
assert!(!num_eq("18446744073709551615", "18446744073709552000"));
assert!(!num_eq("0", "256"));
assert!(num_eq("inf", "Infinity"));
assert!(!num_eq("inf", "-inf"));
assert!(!num_eq("abc", "abc2"));
}
#[test]
fn non_utf8_bytes_parse_as_a_string_value() {
let v = parse_exact_bytes(&[b'"', 0xc3, b'(', b'"']).unwrap();
assert_eq!(v, Json::Str(vec![0xc3, b'(']));
let w = parse_exact_bytes(&[b'"', 0xc4, b'(', b'"']).unwrap();
assert_ne!(v, w);
assert_eq!(parse_exact_bytes(b"256"), parse_exact("256"));
assert!(parse_exact_bytes(&[0xff, 0xfe]).is_none());
}
#[test]
fn scan_agrees_with_the_parser_about_where_a_value_ends() {
let samples: Vec<&[u8]> = vec![
b"0",
b"-1.5e-3",
b"18446744073709551615",
b"null",
b"true",
b"\"hi\"",
b"\"a\\\"b\"",
b"\"\\u0041\"",
b"[]",
b"[1,2,[3,{\"a\":\"b\"}]]",
b"{}",
b"{\"1\":1,\"1\":2}",
b" {\"a\": [1, 2], \"b\": null} ",
&[b'"', 0xc3, b'(', b'"'],
];
for s in samples {
let (start, end) = value_extent(s, 0).expect("extent");
let mut p = Parser { b: s, i: 0 };
p.ws();
let pstart = p.i;
p.value().expect("parse");
assert_eq!(
(start, end),
(pstart, p.i),
"disagreement on {:?}",
String::from_utf8_lossy(s)
);
}
assert_eq!(value_extent(b"1 2", 0), Some((0, 1)));
assert_eq!(value_extent(b"", 0), None);
}
#[test]
fn a_string_decodes_to_bytes_at_an_offset() {
let doc = br#"{"k":"v\n"}"#;
let (bytes, next) = decode_string_at(doc, 5).unwrap();
assert_eq!(bytes, b"v\n");
assert_eq!(doc[next], b'}');
}
#[test]
fn json_whitespace_is_exactly_the_four() {
assert_eq!(trim_json_ws(b" \t\n\r1 \t\n\r"), b"1");
assert_eq!(trim_json_ws("\u{a0}1".as_bytes()), "\u{a0}1".as_bytes());
}
#[test]
fn is_numeric_rejects_text() {
assert!(is_numeric("-12.5"));
assert!(is_numeric("nan"));
assert!(!is_numeric("2023-11-14 22:13:20"));
assert!(!is_numeric(""));
}
}