//! A complete hand-rolled JSON value, serializer, and parser (no external //! crates). The orchestrator's `ci_reports::json` is write-only; the protocol //! needs to *parse* untrusted client packets too, and parsing must be **total** //! — any byte sequence yields `Ok` or `Err`, never a panic. That totality is //! what lets the server treat a malformed packet as a deterministic //! `ValidationReport` rather than a crash. use std::collections::BTreeMap; use std::fmt; /// A parsed JSON value. Objects use a `BTreeMap` so key order is canonical, /// which keeps re-serialization stable and hashable. #[derive(Clone, PartialEq, Debug)] pub enum Json { Null, Bool(bool), /// All numbers are carried as `f64`; integer accessors round-trip exact /// values within the safe integer range, which is all the protocol uses. Num(f64), Str(String), Arr(Vec), Obj(BTreeMap), } impl Json { pub fn s(v: impl Into) -> Json { Json::Str(v.into()) } pub fn i(v: i64) -> Json { Json::Num(v as f64) } pub fn u(v: u64) -> Json { Json::Num(v as f64) } pub fn obj(fields: Vec<(&str, Json)>) -> Json { let mut m = BTreeMap::new(); for (k, v) in fields { m.insert(k.to_string(), v); } Json::Obj(m) } // ---- typed accessors (all fallible, none panic) ---- pub fn get(&self, key: &str) -> Option<&Json> { match self { Json::Obj(m) => m.get(key), _ => None, } } pub fn as_str(&self) -> Option<&str> { match self { Json::Str(s) => Some(s), _ => None, } } pub fn as_f64(&self) -> Option { match self { Json::Num(n) => Some(*n), _ => None, } } pub fn as_i64(&self) -> Option { match self { Json::Num(n) if n.is_finite() => Some(*n as i64), _ => None, } } pub fn as_u64(&self) -> Option { match self { Json::Num(n) if n.is_finite() && *n >= 0.0 => Some(*n as u64), _ => None, } } pub fn as_u8(&self) -> Option { self.as_u64().and_then(|v| u8::try_from(v).ok()) } pub fn as_bool(&self) -> Option { match self { Json::Bool(b) => Some(*b), _ => None, } } pub fn as_arr(&self) -> Option<&[Json]> { match self { Json::Arr(a) => Some(a), _ => None, } } /// Convenience: required field accessors that produce a descriptive error. pub fn field<'a>(&'a self, key: &str) -> Result<&'a Json, JsonError> { self.get(key).ok_or_else(|| JsonError::Field(key.to_string())) } pub fn str_field(&self, key: &str) -> Result { self.field(key)? .as_str() .map(|s| s.to_string()) .ok_or_else(|| JsonError::Type(key.to_string(), "string")) } pub fn u64_field(&self, key: &str) -> Result { self.field(key)? .as_u64() .ok_or_else(|| JsonError::Type(key.to_string(), "u64")) } pub fn i64_field(&self, key: &str) -> Result { self.field(key)? .as_i64() .ok_or_else(|| JsonError::Type(key.to_string(), "i64")) } pub fn arr_field<'a>(&'a self, key: &str) -> Result<&'a [Json], JsonError> { self.field(key)? .as_arr() .ok_or_else(|| JsonError::Type(key.to_string(), "array")) } // ---- serialization ---- /// Compact canonical serialization (no whitespace). Deterministic because /// object keys are stored sorted. pub fn to_compact(&self) -> String { let mut out = String::new(); self.write(&mut out); out } fn write(&self, out: &mut String) { match self { Json::Null => out.push_str("null"), Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }), Json::Num(n) => { if !n.is_finite() { out.push_str("null"); } else if *n == n.trunc() && n.abs() < 9_007_199_254_740_992.0 { // Exact integer: print without a decimal point. out.push_str(&(*n as i64).to_string()); } else { out.push_str(&format!("{}", n)); } } Json::Str(s) => write_str(out, s), Json::Arr(items) => { out.push('['); for (i, it) in items.iter().enumerate() { if i > 0 { out.push(','); } it.write(out); } out.push(']'); } Json::Obj(m) => { out.push('{'); for (i, (k, v)) in m.iter().enumerate() { if i > 0 { out.push(','); } write_str(out, k); out.push(':'); v.write(out); } out.push('}'); } } } } fn write_str(out: &mut String, s: &str) { 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 => out.push_str(&format!("\\u{:04x}", c as u32)), c => out.push(c), } } out.push('"'); } /// A JSON parse / shape error. Carrying a message keeps decode failures /// diagnosable without ever unwinding. #[derive(Clone, PartialEq, Eq, Debug)] pub enum JsonError { Parse(String), Field(String), Type(String, &'static str), } impl fmt::Display for JsonError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { JsonError::Parse(m) => write!(f, "json parse error: {m}"), JsonError::Field(k) => write!(f, "missing field: {k}"), JsonError::Type(k, t) => write!(f, "field {k} is not a {t}"), } } } impl std::error::Error for JsonError {} /// Parse a JSON document. Total: never panics on any input. pub fn parse(input: &str) -> Result { let bytes = input.as_bytes(); let mut p = Parser { bytes, pos: 0, depth: 0 }; p.skip_ws(); let v = p.value()?; p.skip_ws(); if p.pos != bytes.len() { return Err(JsonError::Parse("trailing characters".into())); } Ok(v) } /// Maximum nesting depth. Bounds recursion so a deeply-nested adversarial /// packet returns `Err` instead of overflowing the stack. const MAX_DEPTH: usize = 64; struct Parser<'a> { bytes: &'a [u8], pos: usize, depth: usize, } impl<'a> Parser<'a> { fn peek(&self) -> Option { self.bytes.get(self.pos).copied() } fn skip_ws(&mut self) { while let Some(b) = self.peek() { if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' { self.pos += 1; } else { break; } } } fn value(&mut self) -> Result { self.depth += 1; if self.depth > MAX_DEPTH { return Err(JsonError::Parse("max depth exceeded".into())); } let r = match self.peek() { Some(b'{') => self.object(), Some(b'[') => self.array(), Some(b'"') => Ok(Json::Str(self.string()?)), Some(b't') | Some(b'f') => self.boolean(), Some(b'n') => self.null(), Some(b'-') | Some(b'0'..=b'9') => self.number(), Some(c) => Err(JsonError::Parse(format!("unexpected byte '{}'", c as char))), None => Err(JsonError::Parse("unexpected end".into())), }; self.depth -= 1; r } fn expect(&mut self, b: u8) -> Result<(), JsonError> { if self.peek() == Some(b) { self.pos += 1; Ok(()) } else { Err(JsonError::Parse(format!("expected '{}'", b as char))) } } fn object(&mut self) -> Result { self.expect(b'{')?; let mut m = BTreeMap::new(); self.skip_ws(); if self.peek() == Some(b'}') { self.pos += 1; return Ok(Json::Obj(m)); } loop { self.skip_ws(); let key = self.string()?; self.skip_ws(); self.expect(b':')?; self.skip_ws(); let val = self.value()?; m.insert(key, val); self.skip_ws(); match self.peek() { Some(b',') => { self.pos += 1; continue; } Some(b'}') => { self.pos += 1; break; } _ => return Err(JsonError::Parse("expected ',' or '}'".into())), } } Ok(Json::Obj(m)) } fn array(&mut self) -> Result { self.expect(b'[')?; let mut a = Vec::new(); self.skip_ws(); if self.peek() == Some(b']') { self.pos += 1; return Ok(Json::Arr(a)); } loop { self.skip_ws(); a.push(self.value()?); self.skip_ws(); match self.peek() { Some(b',') => { self.pos += 1; continue; } Some(b']') => { self.pos += 1; break; } _ => return Err(JsonError::Parse("expected ',' or ']'".into())), } } Ok(Json::Arr(a)) } fn string(&mut self) -> Result { self.expect(b'"')?; let mut s = String::new(); loop { match self.peek() { None => return Err(JsonError::Parse("unterminated string".into())), Some(b'"') => { self.pos += 1; break; } Some(b'\\') => { self.pos += 1; match self.peek() { Some(b'"') => s.push('"'), Some(b'\\') => s.push('\\'), Some(b'/') => s.push('/'), Some(b'n') => s.push('\n'), Some(b'r') => s.push('\r'), Some(b't') => s.push('\t'), Some(b'b') => s.push('\u{0008}'), Some(b'f') => s.push('\u{000c}'), Some(b'u') => { let cp = self.hex4()?; // Handle surrogate pairs. if (0xD800..=0xDBFF).contains(&cp) { if self.peek() == Some(b'\\') { self.pos += 1; if self.peek() == Some(b'u') { let lo = self.hex4()?; if (0xDC00..=0xDFFF).contains(&lo) { let c = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); if let Some(ch) = char::from_u32(c) { s.push(ch); } else { s.push('\u{FFFD}'); } continue; } } } s.push('\u{FFFD}'); } else if let Some(ch) = char::from_u32(cp) { s.push(ch); } else { s.push('\u{FFFD}'); } continue; } _ => return Err(JsonError::Parse("bad escape".into())), } self.pos += 1; } Some(_) => { // Copy one UTF-8 codepoint from the source. let start = self.pos; let len = utf8_len(self.bytes[start]); if start + len > self.bytes.len() { return Err(JsonError::Parse("bad utf8".into())); } match std::str::from_utf8(&self.bytes[start..start + len]) { Ok(chunk) => s.push_str(chunk), Err(_) => return Err(JsonError::Parse("bad utf8".into())), } self.pos += len; } } } Ok(s) } fn hex4(&mut self) -> Result { // assumes the 'u' has been consumed self.pos += 1; let mut v: u32 = 0; for _ in 0..4 { let d = self .peek() .and_then(|b| (b as char).to_digit(16)) .ok_or_else(|| JsonError::Parse("bad \\u".into()))?; v = v * 16 + d; self.pos += 1; } Ok(v) } fn boolean(&mut self) -> Result { if self.bytes[self.pos..].starts_with(b"true") { self.pos += 4; Ok(Json::Bool(true)) } else if self.bytes[self.pos..].starts_with(b"false") { self.pos += 5; Ok(Json::Bool(false)) } else { Err(JsonError::Parse("bad literal".into())) } } fn null(&mut self) -> Result { if self.bytes[self.pos..].starts_with(b"null") { self.pos += 4; Ok(Json::Null) } else { Err(JsonError::Parse("bad literal".into())) } } fn number(&mut self) -> Result { let start = self.pos; if self.peek() == Some(b'-') { self.pos += 1; } while let Some(b'0'..=b'9') = self.peek() { self.pos += 1; } if self.peek() == Some(b'.') { self.pos += 1; while let Some(b'0'..=b'9') = self.peek() { self.pos += 1; } } if let Some(b'e') | Some(b'E') = self.peek() { self.pos += 1; if let Some(b'+') | Some(b'-') = self.peek() { self.pos += 1; } while let Some(b'0'..=b'9') = self.peek() { self.pos += 1; } } let slice = std::str::from_utf8(&self.bytes[start..self.pos]) .map_err(|_| JsonError::Parse("bad number".into()))?; slice .parse::() .map(Json::Num) .map_err(|_| JsonError::Parse("bad number".into())) } } fn utf8_len(b: u8) -> usize { if b < 0x80 { 1 } else if b >> 5 == 0b110 { 2 } else if b >> 4 == 0b1110 { 3 } else if b >> 3 == 0b11110 { 4 } else { 1 } } #[cfg(test)] mod tests { use super::*; #[test] fn roundtrip_basic() { let v = Json::obj(vec![ ("a", Json::i(42)), ("b", Json::Arr(vec![Json::Bool(true), Json::Null, Json::s("x")])), ("c", Json::Num(1.5)), ]); let s = v.to_compact(); let back = parse(&s).unwrap(); assert_eq!(v, back); } #[test] fn parse_never_panics_on_garbage() { let deep = "{".repeat(1000); let cases = [ "", "{", "[", "\"", "nul", "{\"a\":}", "[1,2,", "tru", "12.3.4", "{\"a\"1}", "\\", "\"\\u00\"", deep.as_str(), ]; for c in cases { // Must return without panicking; value is irrelevant. let _ = parse(c); } } #[test] fn deep_nesting_is_rejected_not_overflowed() { let deep = "[".repeat(10_000); assert!(parse(&deep).is_err()); } #[test] fn integers_roundtrip_exact() { let v = Json::i(-1234567890123); assert_eq!(parse(&v.to_compact()).unwrap().as_i64(), Some(-1234567890123)); } #[test] fn escapes_roundtrip() { let v = Json::s("line\ntab\tquote\"slash\\end"); let s = v.to_compact(); assert_eq!(parse(&s).unwrap(), v); } }