#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![warn(clippy::all)]
use std::collections::BTreeMap;
use std::fmt::Write as _;
#[derive(Debug, Clone, PartialEq)]
pub enum Json {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<Json>),
Object(BTreeMap<String, Json>),
}
impl Json {
pub fn get(&self, key: &str) -> Option<&Json> {
match self {
Json::Object(m) => m.get(key),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
Json::String(s) => Some(s),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Json::Number(n) => Some(*n as i64),
_ => None,
}
}
pub fn as_array(&self) -> Option<&[Json]> {
match self {
Json::Array(items) => Some(items),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Json::Number(n) => Some(*n),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Json::Bool(b) => Some(*b),
_ => None,
}
}
pub fn object<const N: usize>(pairs: [(&str, Json); N]) -> Json {
let mut m = BTreeMap::new();
for (k, v) in pairs {
m.insert(k.to_string(), v);
}
Json::Object(m)
}
pub fn str(s: impl Into<String>) -> Json {
Json::String(s.into())
}
pub fn num(n: impl Into<f64>) -> Json {
Json::Number(n.into())
}
fn write(&self, out: &mut String) {
match self {
Json::Null => out.push_str("null"),
Json::Bool(true) => out.push_str("true"),
Json::Bool(false) => out.push_str("false"),
Json::Number(n) => {
if n.fract() == 0.0 && n.is_finite() && n.abs() < 9e15 {
let _ = write!(out, "{}", *n as i64);
} else {
let _ = write!(out, "{n}");
}
}
Json::String(s) => write_string(s, out),
Json::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
item.write(out);
}
out.push(']');
}
Json::Object(map) => {
out.push('{');
for (i, (k, v)) in map.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_string(k, out);
out.push(':');
v.write(out);
}
out.push('}');
}
}
}
}
impl std::fmt::Display for Json {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut out = String::new();
self.write(&mut out);
f.write_str(&out)
}
}
fn write_string(s: &str, out: &mut String) {
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"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
pub const MAX_DEPTH: usize = 64;
pub fn parse(input: &str) -> Result<Json, String> {
let bytes: Vec<char> = input.chars().collect();
let mut p = Parser {
input: &bytes,
pos: 0,
depth: 0,
};
p.skip_ws();
let value = p.value()?;
p.skip_ws();
if p.pos != p.input.len() {
return Err(format!("trailing input at position {}", p.pos));
}
Ok(value)
}
struct Parser<'a> {
input: &'a [char],
pos: usize,
depth: usize,
}
impl Parser<'_> {
fn peek(&self) -> Option<char> {
self.input.get(self.pos).copied()
}
fn bump(&mut self) -> Option<char> {
let c = self.peek();
if c.is_some() {
self.pos += 1;
}
c
}
fn skip_ws(&mut self) {
while matches!(self.peek(), Some(' ' | '\t' | '\n' | '\r')) {
self.pos += 1;
}
}
fn expect(&mut self, c: char) -> Result<(), String> {
if self.bump() == Some(c) {
Ok(())
} else {
Err(format!("expected '{c}' at position {}", self.pos))
}
}
fn literal(&mut self, word: &str) -> Result<(), String> {
for c in word.chars() {
if self.bump() != Some(c) {
return Err(format!("invalid literal near position {}", self.pos));
}
}
Ok(())
}
fn value(&mut self) -> Result<Json, String> {
self.skip_ws();
match self.peek() {
Some('n') => {
self.literal("null")?;
Ok(Json::Null)
}
Some('t') => {
self.literal("true")?;
Ok(Json::Bool(true))
}
Some('f') => {
self.literal("false")?;
Ok(Json::Bool(false))
}
Some('"') => Ok(Json::String(self.string()?)),
Some('[') => self.array(),
Some('{') => self.object(),
Some(c) if c == '-' || c.is_ascii_digit() => self.number(),
Some(c) => Err(format!("unexpected '{c}' at position {}", self.pos)),
None => Err("unexpected end of input".to_string()),
}
}
fn string(&mut self) -> Result<String, String> {
self.expect('"')?;
let mut out = String::new();
loop {
match self.bump() {
None => return Err("unterminated string".to_string()),
Some('"') => return Ok(out),
Some('\\') => match self.bump() {
Some('"') => out.push('"'),
Some('\\') => out.push('\\'),
Some('/') => out.push('/'),
Some('b') => out.push('\u{8}'),
Some('f') => out.push('\u{c}'),
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some('u') => {
let mut code = 0u32;
for _ in 0..4 {
let c = self.bump().ok_or("truncated \\u escape")?;
let d = c.to_digit(16).ok_or("invalid \\u escape")?;
code = code * 16 + d;
}
if (0xD800..0xDC00).contains(&code) && self.peek() == Some('\\') {
self.pos += 1;
self.expect('u')?;
let mut low = 0u32;
for _ in 0..4 {
let c = self.bump().ok_or("truncated \\u escape")?;
let d = c.to_digit(16).ok_or("invalid \\u escape")?;
low = low * 16 + d;
}
code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
}
out.push(char::from_u32(code).unwrap_or('\u{FFFD}'));
}
_ => return Err("invalid escape".to_string()),
},
Some(c) if (c as u32) < 0x20 => {
return Err("unescaped control character in string".to_string())
}
Some(c) => out.push(c),
}
}
}
fn number(&mut self) -> Result<Json, String> {
let start = self.pos;
if self.peek() == Some('-') {
self.pos += 1;
}
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
if self.peek() == Some('.') {
self.pos += 1;
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
}
if matches!(self.peek(), Some('e' | 'E')) {
self.pos += 1;
if matches!(self.peek(), Some('+' | '-')) {
self.pos += 1;
}
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
}
let text: String = self.input[start..self.pos].iter().collect();
text.parse::<f64>()
.map(Json::Number)
.map_err(|_| format!("invalid number '{text}'"))
}
fn array(&mut self) -> Result<Json, String> {
self.depth += 1;
if self.depth > MAX_DEPTH {
return Err(format!(
"nesting deeper than {MAX_DEPTH} at position {}",
self.pos
));
}
let parsed = self.array_inner();
self.depth -= 1;
parsed
}
fn array_inner(&mut self) -> Result<Json, String> {
self.expect('[')?;
let mut items = Vec::new();
self.skip_ws();
if self.peek() == Some(']') {
self.pos += 1;
return Ok(Json::Array(items));
}
loop {
items.push(self.value()?);
self.skip_ws();
match self.bump() {
Some(',') => continue,
Some(']') => return Ok(Json::Array(items)),
_ => return Err("expected ',' or ']'".to_string()),
}
}
}
fn object(&mut self) -> Result<Json, String> {
self.depth += 1;
if self.depth > MAX_DEPTH {
return Err(format!(
"nesting deeper than {MAX_DEPTH} at position {}",
self.pos
));
}
let parsed = self.object_inner();
self.depth -= 1;
parsed
}
fn object_inner(&mut self) -> Result<Json, String> {
self.expect('{')?;
let mut map = BTreeMap::new();
self.skip_ws();
if self.peek() == Some('}') {
self.pos += 1;
return Ok(Json::Object(map));
}
loop {
self.skip_ws();
let key = self.string()?;
self.skip_ws();
self.expect(':')?;
let value = self.value()?;
map.insert(key, value);
self.skip_ws();
match self.bump() {
Some(',') => continue,
Some('}') => return Ok(Json::Object(map)),
_ => return Err("expected ',' or '}'".to_string()),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn array_and_number_accessors_report_the_right_shape() {
let v = parse(r#"{"xs":[1,2],"empty":[],"n":1.5,"s":"t"}"#).unwrap();
assert_eq!(
v.get("xs").and_then(|x| x.as_array()).map(|a| a.len()),
Some(2)
);
assert_eq!(
v.get("empty").and_then(|x| x.as_array()).map(|a| a.len()),
Some(0)
);
assert!(
v.get("s").unwrap().as_array().is_none(),
"a string is not an array"
);
assert_eq!(v.get("n").and_then(|x| x.as_f64()), Some(1.5));
assert_eq!(v.get("n").and_then(|x| x.as_i64()), Some(1));
assert!(v.get("s").unwrap().as_f64().is_none());
}
fn alloc_join(n: usize, unit: &str) -> String {
vec![unit; n].join(",")
}
fn alloc_join_with(n: usize, unit: &str) -> String {
(0..n).map(|_| unit).collect::<Vec<_>>().join(",")
}
#[test]
fn parses_scalars() {
assert_eq!(parse("null").unwrap(), Json::Null);
assert_eq!(parse("true").unwrap(), Json::Bool(true));
assert_eq!(parse("false").unwrap(), Json::Bool(false));
assert_eq!(parse("42").unwrap(), Json::Number(42.0));
assert_eq!(parse("-1.5e2").unwrap(), Json::Number(-150.0));
assert_eq!(parse(r#""hi""#).unwrap(), Json::str("hi"));
}
#[test]
fn parses_nested_structures() {
let v = parse(r#"{"a":[1,2,{"b":null}],"c":"d"}"#).unwrap();
assert_eq!(v.get("c").unwrap().as_str(), Some("d"));
let a = v.get("a").unwrap();
match a {
Json::Array(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items[2].get("b"), Some(&Json::Null));
}
_ => panic!("expected an array"),
}
}
#[test]
fn handles_escapes() {
assert_eq!(parse(r#""a\nb""#).unwrap(), Json::str("a\nb"));
assert_eq!(parse(r#""a\\b""#).unwrap(), Json::str("a\\b"));
assert_eq!(parse(r#""a\"b""#).unwrap(), Json::str("a\"b"));
assert_eq!(parse(r#""A""#).unwrap(), Json::str("A"));
assert_eq!(parse(r#""😀""#).unwrap(), Json::str("\u{1F600}"));
}
#[test]
fn deep_nesting_is_refused_rather_than_fatal() {
for depth in [MAX_DEPTH + 1, 1_000, 100_000] {
let deep = "[".repeat(depth) + &"]".repeat(depth);
let err = parse(&deep).expect_err("{depth} deep should be refused");
assert!(
err.contains("nesting"),
"the error should say what was wrong: {err}"
);
let deep = "{\"a\":".repeat(depth) + "1" + &"}".repeat(depth);
assert!(
parse(&deep).is_err(),
"{depth} deep objects should be refused"
);
}
}
#[test]
fn the_limit_is_exactly_where_it_claims() {
let at = "[".repeat(MAX_DEPTH) + &"]".repeat(MAX_DEPTH);
assert!(parse(&at).is_ok(), "{MAX_DEPTH} deep should parse");
let over = "[".repeat(MAX_DEPTH + 1) + &"]".repeat(MAX_DEPTH + 1);
assert!(parse(&over).is_err(), "{} deep should not", MAX_DEPTH + 1);
}
#[test]
fn a_wide_document_is_not_a_deep_one() {
let wide = format!("[{}]", vec!["1"; 50_000].join(","));
let parsed = parse(&wide).expect("a flat array of 50,000 items is not deep");
match parsed {
Json::Array(items) => assert_eq!(items.len(), 50_000),
other => panic!("expected an array, got {other:?}"),
}
let nested = r#"{"a":{"b":{"c":{"d":{"e":{"f":{"g":[1,2,3]}}}}}}}"#;
assert!(parse(nested).is_ok());
}
#[test]
fn depth_is_released_between_siblings() {
let siblings = alloc_join(MAX_DEPTH * 8, "[]");
let doc = format!("[{siblings}]");
let parsed = parse(&doc).expect("wide and shallow should parse");
match parsed {
Json::Array(items) => assert_eq!(items.len(), MAX_DEPTH * 8),
other => panic!("expected an array, got {other:?}"),
}
let siblings = alloc_join(MAX_DEPTH * 8, "{}");
assert!(parse(&format!("[{siblings}]")).is_ok());
let branch = "[".repeat(MAX_DEPTH - 2) + &"]".repeat(MAX_DEPTH - 2);
let many = alloc_join_with(8, &branch);
assert!(
parse(&format!("[{many}]")).is_ok(),
"deep branches side by side should parse; the counter is not falling"
);
let over = "[".repeat(MAX_DEPTH + 10) + &"]".repeat(MAX_DEPTH + 10);
assert!(parse(&over).is_err());
assert!(parse(&doc).is_ok(), "a refusal poisoned the next parse");
}
#[test]
fn rejects_malformed_input() {
for bad in [
"",
"{",
"[1,]",
r#"{"a"}"#,
r#"{"a":}"#,
"tru",
"{} {}",
"\"unterminated",
"01x",
] {
assert!(parse(bad).is_err(), "{bad:?} should not parse");
}
}
#[test]
fn roundtrips_through_serialization() {
let cases = [
r#"{"a":1,"b":[true,false,null],"c":"x"}"#,
r#"[]"#,
r#"{}"#,
r#"{"nested":{"deep":{"value":-3}}}"#,
];
for case in cases {
let v = parse(case).unwrap();
let text = v.to_string();
assert_eq!(parse(&text).unwrap(), v, "round trip of {case}");
}
}
#[test]
fn serialization_escapes_control_characters() {
let v = Json::str("tab\there\u{1}");
let text = v.to_string();
assert!(text.contains("\\t"));
assert!(text.contains("\\u0001"));
assert_eq!(parse(&text).unwrap(), v);
}
#[test]
fn integers_serialize_without_a_decimal_point() {
assert_eq!(Json::num(42.0).to_string(), "42");
assert_eq!(Json::num(-7.0).to_string(), "-7");
assert_eq!(Json::num(0.5).to_string(), "0.5");
}
#[test]
fn object_keys_are_ordered_deterministically() {
let a = Json::object([("z", Json::num(1)), ("a", Json::num(2))]);
assert_eq!(a.to_string(), r#"{"a":2,"z":1}"#);
}
#[test]
fn accessors_return_none_on_type_mismatch() {
let v = Json::str("text");
assert_eq!(v.as_i64(), None);
assert_eq!(v.as_bool(), None);
assert_eq!(v.get("key"), None);
}
}