All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m16s
standards workspaces build + test / aac-rs (pull_request) Successful in 1m35s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 51s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 34s
standards workspaces build + test / map-rs (pull_request) Successful in 31s
standards workspaces build + test / mesh-rs (pull_request) Successful in 30s
standards workspaces build + test / openpay (pull_request) Successful in 1m40s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 7m35s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 27s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 2m6s
wai-vector vector graphics / linux-aarch64 (pull_request) Successful in 25s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m28s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 8m4s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 12m12s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 34s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 4m9s
wai-vector vector graphics / macos-arm64 (pull_request) Successful in 31s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 6s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 5s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 5s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m15s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m6s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m35s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m15s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m16s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m22s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m19s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m26s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 4m14s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m24s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 8m43s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 10s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 31s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 54s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 2m3s
WAI section references / cited sections exist (pull_request) Successful in 10s
wai-vector vector graphics / linux-x86_64 (pull_request) Successful in 24s
wai-vector vector graphics / wasm32-wasip2 (wasmtime) (pull_request) Successful in 21s
wai-vector vector graphics / wasm32 under Node (pull_request) Successful in 19s
wai-vector vector graphics / fuzz, peak allocation, comparison count (pull_request) Successful in 14s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 16m51s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
406 lines
12 KiB
Rust
406 lines
12 KiB
Rust
//! JSON: a reader for the authoring form (§2.12) and the vector files, and the
|
|
//! writers of the files this crate commits.
|
|
//!
|
|
//! Numbers are integers only: every value the formats carry is an integer, so a
|
|
//! fraction or an exponent is refused. An integer is held as `i128`, which holds every
|
|
//! `u64` and `i64` a field or a cost can take.
|
|
|
|
use alloc::string::String;
|
|
use alloc::vec::Vec;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum Value {
|
|
Null,
|
|
Bool(bool),
|
|
Int(i128),
|
|
Str(String),
|
|
Arr(Vec<Value>),
|
|
/// Members in the order written.
|
|
Obj(Vec<(String, Value)>),
|
|
}
|
|
|
|
/// Why a JSON text did not parse: the byte offset of the first error.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct JsonError {
|
|
pub at: usize,
|
|
}
|
|
|
|
const MAX_DEPTH: usize = 64;
|
|
|
|
struct P<'a> {
|
|
b: &'a [u8],
|
|
i: usize,
|
|
}
|
|
|
|
impl P<'_> {
|
|
fn err<T>(&self) -> Result<T, JsonError> {
|
|
Err(JsonError { at: self.i })
|
|
}
|
|
fn ws(&mut self) {
|
|
while let Some(&c) = self.b.get(self.i) {
|
|
if matches!(c, b' ' | b'\t' | b'\n' | b'\r') {
|
|
self.i += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
fn lit(&mut self, s: &[u8]) -> bool {
|
|
if self.b.get(self.i..self.i + s.len()) == Some(s) {
|
|
self.i += s.len();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
fn value(&mut self, depth: usize) -> Result<Value, JsonError> {
|
|
if depth > MAX_DEPTH {
|
|
return self.err();
|
|
}
|
|
self.ws();
|
|
match self.b.get(self.i) {
|
|
None => self.err(),
|
|
Some(b'n') => if self.lit(b"null") { Ok(Value::Null) } else { self.err() },
|
|
Some(b't') => if self.lit(b"true") { Ok(Value::Bool(true)) } else { self.err() },
|
|
Some(b'f') => if self.lit(b"false") { Ok(Value::Bool(false)) } else { self.err() },
|
|
Some(b'"') => Ok(Value::Str(self.string()?)),
|
|
Some(b'[') => {
|
|
self.i += 1;
|
|
let mut v = Vec::new();
|
|
self.ws();
|
|
if self.b.get(self.i) == Some(&b']') {
|
|
self.i += 1;
|
|
return Ok(Value::Arr(v));
|
|
}
|
|
loop {
|
|
v.push(self.value(depth + 1)?);
|
|
self.ws();
|
|
match self.b.get(self.i) {
|
|
Some(b',') => self.i += 1,
|
|
Some(b']') => {
|
|
self.i += 1;
|
|
return Ok(Value::Arr(v));
|
|
}
|
|
_ => return self.err(),
|
|
}
|
|
}
|
|
}
|
|
Some(b'{') => {
|
|
self.i += 1;
|
|
let mut m: Vec<(String, Value)> = Vec::new();
|
|
self.ws();
|
|
if self.b.get(self.i) == Some(&b'}') {
|
|
self.i += 1;
|
|
return Ok(Value::Obj(m));
|
|
}
|
|
loop {
|
|
self.ws();
|
|
if self.b.get(self.i) != Some(&b'"') {
|
|
return self.err();
|
|
}
|
|
let k = self.string()?;
|
|
if m.iter().any(|(n, _)| *n == k) {
|
|
return self.err();
|
|
}
|
|
self.ws();
|
|
if self.b.get(self.i) != Some(&b':') {
|
|
return self.err();
|
|
}
|
|
self.i += 1;
|
|
let v = self.value(depth + 1)?;
|
|
m.push((k, v));
|
|
self.ws();
|
|
match self.b.get(self.i) {
|
|
Some(b',') => self.i += 1,
|
|
Some(b'}') => {
|
|
self.i += 1;
|
|
return Ok(Value::Obj(m));
|
|
}
|
|
_ => return self.err(),
|
|
}
|
|
}
|
|
}
|
|
Some(b'-' | b'0'..=b'9') => self.int(),
|
|
_ => self.err(),
|
|
}
|
|
}
|
|
fn int(&mut self) -> Result<Value, JsonError> {
|
|
let neg = self.b.get(self.i) == Some(&b'-');
|
|
if neg {
|
|
self.i += 1;
|
|
}
|
|
let start = self.i;
|
|
let mut v: i128 = 0;
|
|
while let Some(&c) = self.b.get(self.i) {
|
|
if !c.is_ascii_digit() {
|
|
break;
|
|
}
|
|
v = match v.checked_mul(10).and_then(|x| x.checked_add(i128::from(c - b'0'))) {
|
|
Some(x) => x,
|
|
None => return self.err(),
|
|
};
|
|
self.i += 1;
|
|
}
|
|
let digits = self.i - start;
|
|
// No empty number, no leading zero, no fraction and no exponent.
|
|
if digits == 0 || (digits > 1 && self.b[start] == b'0') {
|
|
return self.err();
|
|
}
|
|
if matches!(self.b.get(self.i), Some(b'.' | b'e' | b'E')) {
|
|
return self.err();
|
|
}
|
|
Ok(Value::Int(if neg { -v } else { v }))
|
|
}
|
|
fn hex4(&mut self) -> Result<u32, JsonError> {
|
|
let mut v = 0u32;
|
|
for _ in 0..4 {
|
|
let c = match self.b.get(self.i) {
|
|
Some(c) => *c,
|
|
None => return self.err(),
|
|
};
|
|
let d = match c {
|
|
b'0'..=b'9' => c - b'0',
|
|
b'a'..=b'f' => c - b'a' + 10,
|
|
b'A'..=b'F' => c - b'A' + 10,
|
|
_ => return self.err(),
|
|
};
|
|
v = v * 16 + u32::from(d);
|
|
self.i += 1;
|
|
}
|
|
Ok(v)
|
|
}
|
|
fn string(&mut self) -> Result<String, JsonError> {
|
|
self.i += 1;
|
|
let mut out: Vec<u8> = Vec::new();
|
|
loop {
|
|
let c = match self.b.get(self.i) {
|
|
Some(c) => *c,
|
|
None => return self.err(),
|
|
};
|
|
self.i += 1;
|
|
match c {
|
|
b'"' => break,
|
|
b'\\' => {
|
|
let e = match self.b.get(self.i) {
|
|
Some(e) => *e,
|
|
None => return self.err(),
|
|
};
|
|
self.i += 1;
|
|
let ch = match e {
|
|
b'"' => '"',
|
|
b'\\' => '\\',
|
|
b'/' => '/',
|
|
b'b' => '\u{8}',
|
|
b'f' => '\u{c}',
|
|
b'n' => '\n',
|
|
b'r' => '\r',
|
|
b't' => '\t',
|
|
b'u' => {
|
|
let hi = self.hex4()?;
|
|
let cp = if (0xD800..0xDC00).contains(&hi) {
|
|
if !self.lit(b"\\u") {
|
|
return self.err();
|
|
}
|
|
let lo = self.hex4()?;
|
|
if !(0xDC00..0xE000).contains(&lo) {
|
|
return self.err();
|
|
}
|
|
0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00)
|
|
} else if (0xDC00..0xE000).contains(&hi) {
|
|
return self.err();
|
|
} else {
|
|
hi
|
|
};
|
|
match char::from_u32(cp) {
|
|
Some(c) => c,
|
|
None => return self.err(),
|
|
}
|
|
}
|
|
_ => return self.err(),
|
|
};
|
|
let mut buf = [0u8; 4];
|
|
out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
|
|
}
|
|
0x00..=0x1F => return self.err(),
|
|
_ => out.push(c),
|
|
}
|
|
}
|
|
String::from_utf8(out).or_else(|_| self.err())
|
|
}
|
|
}
|
|
|
|
/// Parse one JSON text. Whitespace may surround the value; nothing else may.
|
|
pub fn parse(text: &str) -> Result<Value, JsonError> {
|
|
let mut p = P { b: text.as_bytes(), i: 0 };
|
|
let v = p.value(0)?;
|
|
p.ws();
|
|
if p.i != p.b.len() {
|
|
return p.err();
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
impl Value {
|
|
pub fn get(&self, key: &str) -> Option<&Value> {
|
|
match self {
|
|
Value::Obj(m) => m.iter().find(|(k, _)| k == key).map(|(_, v)| v),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub fn as_int(&self) -> Option<i128> {
|
|
match self {
|
|
Value::Int(v) => Some(*v),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub fn as_str(&self) -> Option<&str> {
|
|
match self {
|
|
Value::Str(s) => Some(s),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub fn as_arr(&self) -> Option<&[Value]> {
|
|
match self {
|
|
Value::Arr(a) => Some(a),
|
|
_ => None,
|
|
}
|
|
}
|
|
/// Write the value compactly, members in the order held.
|
|
pub fn write(&self, out: &mut String) {
|
|
match self {
|
|
Value::Null => out.push_str("null"),
|
|
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
|
Value::Int(v) => push_int(out, *v),
|
|
Value::Str(s) => push_str(out, s),
|
|
Value::Arr(a) => {
|
|
out.push('[');
|
|
for (i, v) in a.iter().enumerate() {
|
|
if i > 0 {
|
|
out.push(',');
|
|
}
|
|
v.write(out);
|
|
}
|
|
out.push(']');
|
|
}
|
|
Value::Obj(m) => {
|
|
out.push('{');
|
|
for (i, (k, v)) in m.iter().enumerate() {
|
|
if i > 0 {
|
|
out.push(',');
|
|
}
|
|
push_str(out, k);
|
|
out.push(':');
|
|
v.write(out);
|
|
}
|
|
out.push('}');
|
|
}
|
|
}
|
|
}
|
|
/// The compact text of the value.
|
|
pub fn text(&self) -> String {
|
|
let mut s = String::new();
|
|
self.write(&mut s);
|
|
s
|
|
}
|
|
}
|
|
|
|
/// An object from `(key, value)` pairs, in the order given.
|
|
pub fn obj(members: Vec<(&str, Value)>) -> Value {
|
|
Value::Obj(members.into_iter().map(|(k, v)| (String::from(k), v)).collect())
|
|
}
|
|
|
|
pub fn int(v: impl Into<i128>) -> Value {
|
|
Value::Int(v.into())
|
|
}
|
|
|
|
pub fn string(s: &str) -> Value {
|
|
Value::Str(String::from(s))
|
|
}
|
|
|
|
pub fn push_int(out: &mut String, v: i128) {
|
|
if v < 0 {
|
|
out.push('-');
|
|
}
|
|
let mut u = v.unsigned_abs();
|
|
let mut buf = [0u8; 40];
|
|
let mut n = 0;
|
|
loop {
|
|
buf[n] = b'0' + (u % 10) as u8;
|
|
n += 1;
|
|
u /= 10;
|
|
if u == 0 {
|
|
break;
|
|
}
|
|
}
|
|
for i in (0..n).rev() {
|
|
out.push(buf[i] as char);
|
|
}
|
|
}
|
|
|
|
/// A JSON string: `"` and `\` escaped, control characters as `\u00XX`, everything
|
|
/// else as UTF-8.
|
|
pub fn push_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"),
|
|
c if (c as u32) < 0x20 || c as u32 == 0x7F => {
|
|
out.push_str("\\u00");
|
|
let v = c as u32;
|
|
out.push(HEX[(v >> 4) as usize] as char);
|
|
out.push(HEX[(v & 15) as usize] as char);
|
|
}
|
|
c => out.push(c),
|
|
}
|
|
}
|
|
out.push('"');
|
|
}
|
|
|
|
const HEX: &[u8; 16] = b"0123456789abcdef";
|
|
|
|
/// Lowercase hexadecimal.
|
|
pub fn hex(b: &[u8]) -> String {
|
|
let mut s = String::with_capacity(2 * b.len());
|
|
for &x in b {
|
|
s.push(HEX[(x >> 4) as usize] as char);
|
|
s.push(HEX[(x & 15) as usize] as char);
|
|
}
|
|
s
|
|
}
|
|
|
|
/// Bytes from lowercase or uppercase hexadecimal of even length.
|
|
pub fn unhex(s: &str) -> Option<Vec<u8>> {
|
|
let b = s.as_bytes();
|
|
if !b.len().is_multiple_of(2) {
|
|
return None;
|
|
}
|
|
let d = |c: u8| match c {
|
|
b'0'..=b'9' => Some(c - b'0'),
|
|
b'a'..=b'f' => Some(c - b'a' + 10),
|
|
b'A'..=b'F' => Some(c - b'A' + 10),
|
|
_ => None,
|
|
};
|
|
b.as_chunks::<2>().0.iter().map(|p| Some(d(p[0])? << 4 | d(p[1])?)).collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn round_trip() {
|
|
let t = r#"{"a":[1,-2,170141183460469231731687303715884105727],"b":"x\"\\\n\u0001é😀","c":{},"d":[],"e":null,"f":true}"#;
|
|
let v = parse(t).unwrap();
|
|
assert_eq!(parse(&v.text()).unwrap(), v);
|
|
assert_eq!(v.get("b").unwrap().as_str().unwrap(), "x\"\\\n\u{1}é😀");
|
|
}
|
|
|
|
#[test]
|
|
fn refusals() {
|
|
for t in ["01", "1.5", "1e3", "{\"a\":1,\"a\":2}", "[1,]", "\"\\ud800\"", "\"\u{1}\"", "1 2", "-"] {
|
|
assert!(parse(t).is_err(), "{t}");
|
|
}
|
|
}
|
|
}
|