Skip to main content

graphrefly/
json.rs

1//! Neutral JSON/encoding helpers shared by checkpoint and passive storage (D96).
2//!
3//! This module owns the strict/canonical JSON byte contract used by storage,
4//! checkpoints, and future V1 hash inputs. It is deliberately independent of
5//! graph lifecycle and storage tier types.
6
7use std::cmp::Ordering;
8use std::error::Error;
9use std::fmt;
10
11use serde_json::Value;
12
13/// `JsonValue` type alias.
14pub type JsonValue = Value;
15/// `JsonCodecResult` type alias.
16pub type JsonCodecResult<T> = Result<T, JsonCodecError>;
17
18const JS_MAX_SAFE_INTEGER_F64: f64 = 9_007_199_254_740_991.0;
19const JS_MAX_SAFE_INTEGER_I64: i64 = 9_007_199_254_740_991;
20const JS_MAX_SAFE_INTEGER_U64: u64 = 9_007_199_254_740_991;
21
22#[derive(Clone, Debug, Eq, PartialEq)]
23/// `JsonCodecError` variants.
24pub enum JsonCodecError {
25    /// `Encode` variant.
26    Encode(String),
27    /// `Decode` variant.
28    Decode(String),
29    /// `Validation` variant.
30    Validation(String),
31}
32
33impl JsonCodecError {
34    /// Creates or computes `encode`.
35    pub fn encode(message: impl Into<String>) -> Self {
36        Self::Encode(message.into())
37    }
38
39    /// Creates or computes `decode`.
40    pub fn decode(message: impl Into<String>) -> Self {
41        Self::Decode(message.into())
42    }
43
44    /// Creates or computes `validation`.
45    pub fn validation(message: impl Into<String>) -> Self {
46        Self::Validation(message.into())
47    }
48}
49
50impl fmt::Display for JsonCodecError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Encode(message) => write!(f, "json encode failed: {message}"),
54            Self::Decode(message) => write!(f, "json decode failed: {message}"),
55            Self::Validation(message) => f.write_str(message),
56        }
57    }
58}
59
60impl Error for JsonCodecError {}
61
62/// Typed byte codec for D82 storage binding helpers and D96 JSON surfaces.
63pub trait Codec<T> {
64    /// Updates or reads `encode`.
65    fn encode(&self, value: &T) -> JsonCodecResult<Vec<u8>>;
66    /// Updates or reads `decode`.
67    fn decode(&self, bytes: &[u8]) -> JsonCodecResult<T>;
68}
69
70#[derive(Clone, Copy, Debug, Default)]
71/// `JsonCodec` data container.
72pub struct JsonCodec;
73
74impl Codec<JsonValue> for JsonCodec {
75    fn encode(&self, value: &JsonValue) -> JsonCodecResult<Vec<u8>> {
76        stable_json_string(value).map(String::into_bytes)
77    }
78
79    fn decode(&self, bytes: &[u8]) -> JsonCodecResult<JsonValue> {
80        serde_json::from_slice(bytes).map_err(|err| JsonCodecError::decode(err.to_string()))
81    }
82}
83
84#[derive(Clone, Copy, Debug, Default)]
85/// `StrictJsonCodec` data container.
86pub struct StrictJsonCodec;
87
88impl Codec<JsonValue> for StrictJsonCodec {
89    fn encode(&self, value: &JsonValue) -> JsonCodecResult<Vec<u8>> {
90        strict_canonical_json_bytes(value)
91    }
92
93    fn decode(&self, bytes: &[u8]) -> JsonCodecResult<JsonValue> {
94        strict_json_decode(bytes)
95    }
96}
97
98/// Creates or computes `json_codec_for`.
99pub fn json_codec_for<T>() -> JsonCodec {
100    let _ = std::marker::PhantomData::<T>;
101    JsonCodec
102}
103
104/// Creates or computes `strict_json_codec_for`.
105pub fn strict_json_codec_for<T>() -> StrictJsonCodec {
106    let _ = std::marker::PhantomData::<T>;
107    StrictJsonCodec
108}
109
110/// Stable JSON string with object keys sorted by JavaScript UTF-16 code-unit order.
111pub fn stable_json_string(value: &JsonValue) -> JsonCodecResult<String> {
112    validate_strict_json_value(value, "$")?;
113    canonical_json_string(value)
114}
115
116/// D113 neutral helper for strict canonical JSON UTF-8 bytes.
117pub fn strict_canonical_json_bytes(value: &JsonValue) -> JsonCodecResult<Vec<u8>> {
118    stable_json_string(value).map(String::into_bytes)
119}
120
121/// Decode strict canonical JSON bytes, rejecting malformed UTF-8, duplicate keys,
122/// non-canonical byte shape, and values that fail the shared strict validator.
123pub fn strict_json_decode(bytes: &[u8]) -> JsonCodecResult<JsonValue> {
124    let text = std::str::from_utf8(bytes).map_err(|err| JsonCodecError::decode(err.to_string()))?;
125    assert_no_duplicate_json_object_keys(text)?;
126    let decoded: JsonValue =
127        serde_json::from_str(text).map_err(|err| JsonCodecError::decode(err.to_string()))?;
128    let canonical = strict_canonical_json_bytes(&decoded)?;
129    if bytes != canonical.as_slice() {
130        return Err(JsonCodecError::validation(
131            "strictJsonCodec: bytes are not canonical stable JSON",
132        ));
133    }
134    Ok(decoded)
135}
136
137/// Shared strict JSON value validator for checkpoint and storage canonical bytes.
138pub fn validate_strict_json_value(value: &JsonValue, path: &str) -> JsonCodecResult<()> {
139    validate_strict_json_value_inner(value, path, 0)
140}
141
142fn validate_strict_json_value_inner(
143    value: &JsonValue,
144    path: &str,
145    depth: u32,
146) -> JsonCodecResult<()> {
147    if depth > 128 {
148        return Err(JsonCodecError::validation(format!(
149            "strictJsonCodec: JSON value at {path} exceeds maximum depth 128"
150        )));
151    }
152    match value {
153        Value::Object(map) => {
154            for (key, value) in map {
155                validate_strict_json_value_inner(value, &format!("{path}.{key}"), depth + 1)?;
156            }
157        }
158        Value::Array(values) => {
159            for (index, value) in values.iter().enumerate() {
160                validate_strict_json_value_inner(value, &format!("{path}[{index}]"), depth + 1)?;
161            }
162        }
163        Value::Number(number) => {
164            let text = number.to_string();
165            if text == "-0.0" || text == "-0" {
166                return Err(JsonCodecError::validation(format!(
167                    "strictJsonCodec: JSON number at {path} is not strict canonical JSON compatible"
168                )));
169            }
170            if let Some(value) = number.as_i64() {
171                if !(-JS_MAX_SAFE_INTEGER_I64..=JS_MAX_SAFE_INTEGER_I64).contains(&value) {
172                    return Err(JsonCodecError::validation(format!(
173                        "strictJsonCodec: JSON integer at {path} is outside the safe integer range"
174                    )));
175                }
176            } else if let Some(value) = number.as_u64() {
177                if value > JS_MAX_SAFE_INTEGER_U64 {
178                    return Err(JsonCodecError::validation(format!(
179                        "strictJsonCodec: JSON integer at {path} is outside the safe integer range"
180                    )));
181                }
182            }
183            if let Some(float) = number.as_f64() {
184                let abs = float.abs();
185                if abs > 0.0 && abs < f64::MIN_POSITIVE {
186                    return Err(JsonCodecError::validation(format!(
187                        "strictJsonCodec: JSON number at {path} is subnormal and not strict canonical JSON compatible"
188                    )));
189                }
190                if float.fract() == 0.0 && abs > JS_MAX_SAFE_INTEGER_F64 {
191                    return Err(JsonCodecError::validation(format!(
192                        "strictJsonCodec: JSON integer at {path} is outside the safe integer range"
193                    )));
194                }
195            }
196        }
197        Value::Null | Value::Bool(_) | Value::String(_) => {}
198    }
199    Ok(())
200}
201
202fn canonical_json_string(value: &JsonValue) -> JsonCodecResult<String> {
203    match value {
204        Value::Null => Ok("null".to_owned()),
205        Value::Bool(true) => Ok("true".to_owned()),
206        Value::Bool(false) => Ok("false".to_owned()),
207        Value::Number(number) => Ok(canonical_number_string(number)),
208        Value::String(value) => {
209            serde_json::to_string(value).map_err(|err| JsonCodecError::encode(err.to_string()))
210        }
211        Value::Array(values) => {
212            let mut out = String::from("[");
213            for (index, value) in values.iter().enumerate() {
214                if index > 0 {
215                    out.push(',');
216                }
217                out.push_str(&canonical_json_string(value)?);
218            }
219            out.push(']');
220            Ok(out)
221        }
222        Value::Object(map) => {
223            let mut keys = map.keys().collect::<Vec<_>>();
224            keys.sort_by(|a, b| cmp_js_utf16(a, b));
225            let mut out = String::from("{");
226            for (index, key) in keys.iter().enumerate() {
227                if index > 0 {
228                    out.push(',');
229                }
230                out.push_str(
231                    &serde_json::to_string(key)
232                        .map_err(|err| JsonCodecError::encode(err.to_string()))?,
233                );
234                out.push(':');
235                out.push_str(&canonical_json_string(
236                    map.get(*key).expect("key came from map.keys()"),
237                )?);
238            }
239            out.push('}');
240            Ok(out)
241        }
242    }
243}
244
245fn canonical_number_string(number: &serde_json::Number) -> String {
246    if let Some(value) = number.as_i64() {
247        return value.to_string();
248    }
249    if let Some(value) = number.as_u64() {
250        return value.to_string();
251    }
252    if let Some(value) = number.as_f64() {
253        if value == 0.0 {
254            return "0".to_owned();
255        }
256        if value.is_finite() && value.fract() == 0.0 && value.abs() <= JS_MAX_SAFE_INTEGER_F64 {
257            return format!("{value:.0}");
258        }
259    }
260    number.to_string()
261}
262
263fn cmp_js_utf16(a: &str, b: &str) -> Ordering {
264    a.encode_utf16().cmp(b.encode_utf16())
265}
266
267fn assert_no_duplicate_json_object_keys(text: &str) -> JsonCodecResult<()> {
268    struct Scanner<'a> {
269        text: &'a str,
270        index: usize,
271    }
272
273    impl Scanner<'_> {
274        fn fail<T>(&self, message: impl Into<String>) -> JsonCodecResult<T> {
275            Err(JsonCodecError::validation(format!(
276                "strictJsonCodec: {}",
277                message.into()
278            )))
279        }
280
281        fn peek(&self) -> Option<u8> {
282            self.text.as_bytes().get(self.index).copied()
283        }
284
285        fn skip_whitespace(&mut self) {
286            while matches!(self.peek(), Some(b' ' | b'\n' | b'\r' | b'\t')) {
287                self.index += 1;
288            }
289        }
290
291        fn read_json_string(&mut self) -> JsonCodecResult<String> {
292            let start = self.index;
293            self.index += 1;
294            while self.index < self.text.len() {
295                match self.peek() {
296                    Some(b'"') => {
297                        self.index += 1;
298                        return serde_json::from_str(&self.text[start..self.index]).map_err(
299                            |err| {
300                                JsonCodecError::validation(format!(
301                                    "strictJsonCodec: malformed JSON string: {err}"
302                                ))
303                            },
304                        );
305                    }
306                    Some(b'\\') => {
307                        self.index += 2;
308                    }
309                    Some(_) => {
310                        self.index += 1;
311                    }
312                    None => break,
313                }
314            }
315            self.fail("unterminated JSON string")
316        }
317
318        fn consume_literal(&mut self, literal: &str) -> JsonCodecResult<()> {
319            if self.text[self.index..].starts_with(literal) {
320                self.index += literal.len();
321                Ok(())
322            } else {
323                self.fail(format!("malformed JSON near byte {}", self.index))
324            }
325        }
326
327        fn consume_number(&mut self) -> JsonCodecResult<()> {
328            let bytes = self.text.as_bytes();
329            let start = self.index;
330            if self.peek() == Some(b'-') {
331                self.index += 1;
332            }
333            match self.peek() {
334                Some(b'0') => self.index += 1,
335                Some(b'1'..=b'9') => {
336                    self.index += 1;
337                    while matches!(self.peek(), Some(b'0'..=b'9')) {
338                        self.index += 1;
339                    }
340                }
341                _ => return self.fail(format!("malformed JSON number near byte {start}")),
342            }
343            if self.peek() == Some(b'.') {
344                self.index += 1;
345                if !matches!(self.peek(), Some(b'0'..=b'9')) {
346                    return self.fail(format!("malformed JSON number near byte {start}"));
347                }
348                while matches!(self.peek(), Some(b'0'..=b'9')) {
349                    self.index += 1;
350                }
351            }
352            if matches!(self.peek(), Some(b'e' | b'E')) {
353                self.index += 1;
354                if matches!(self.peek(), Some(b'+' | b'-')) {
355                    self.index += 1;
356                }
357                if !matches!(self.peek(), Some(b'0'..=b'9')) {
358                    return self.fail(format!("malformed JSON number near byte {start}"));
359                }
360                while self.index < bytes.len() && matches!(self.peek(), Some(b'0'..=b'9')) {
361                    self.index += 1;
362                }
363            }
364            Ok(())
365        }
366
367        fn parse_value(&mut self, path: &str) -> JsonCodecResult<()> {
368            self.skip_whitespace();
369            match self.peek() {
370                Some(b'{') => self.parse_object(path),
371                Some(b'[') => self.parse_array(path),
372                Some(b'"') => self.read_json_string().map(|_| ()),
373                Some(b't') => self.consume_literal("true"),
374                Some(b'f') => self.consume_literal("false"),
375                Some(b'n') => self.consume_literal("null"),
376                Some(b'-' | b'0'..=b'9') => self.consume_number(),
377                _ => self.fail(format!("malformed JSON near byte {}", self.index)),
378            }
379        }
380
381        fn parse_object(&mut self, path: &str) -> JsonCodecResult<()> {
382            let mut keys = Vec::<String>::new();
383            self.index += 1;
384            self.skip_whitespace();
385            if self.peek() == Some(b'}') {
386                self.index += 1;
387                return Ok(());
388            }
389            while self.index < self.text.len() {
390                self.skip_whitespace();
391                if self.peek() != Some(b'"') {
392                    return self.fail(format!("expected object key near byte {}", self.index));
393                }
394                let key = self.read_json_string()?;
395                if keys.iter().any(|seen| seen == &key) {
396                    return Err(JsonCodecError::validation(format!(
397                        "strictJsonCodec: duplicate object key {:?} at {path}",
398                        key
399                    )));
400                }
401                keys.push(key.clone());
402                self.skip_whitespace();
403                if self.peek() != Some(b':') {
404                    return self.fail(format!(
405                        "expected ':' after object key near byte {}",
406                        self.index
407                    ));
408                }
409                self.index += 1;
410                self.parse_value(&format!("{path}.{key}"))?;
411                self.skip_whitespace();
412                match self.peek() {
413                    Some(b',') => self.index += 1,
414                    Some(b'}') => {
415                        self.index += 1;
416                        return Ok(());
417                    }
418                    _ => {
419                        return self.fail(format!("expected ',' or '}}' near byte {}", self.index))
420                    }
421                }
422            }
423            self.fail("unterminated JSON object")
424        }
425
426        fn parse_array(&mut self, path: &str) -> JsonCodecResult<()> {
427            self.index += 1;
428            self.skip_whitespace();
429            if self.peek() == Some(b']') {
430                self.index += 1;
431                return Ok(());
432            }
433            let mut item = 0;
434            while self.index < self.text.len() {
435                self.parse_value(&format!("{path}[{item}]"))?;
436                item += 1;
437                self.skip_whitespace();
438                match self.peek() {
439                    Some(b',') => self.index += 1,
440                    Some(b']') => {
441                        self.index += 1;
442                        return Ok(());
443                    }
444                    _ => return self.fail(format!("expected ',' or ']' near byte {}", self.index)),
445                }
446            }
447            self.fail("unterminated JSON array")
448        }
449    }
450
451    let mut scanner = Scanner { text, index: 0 };
452    scanner.parse_value("$")?;
453    scanner.skip_whitespace();
454    if scanner.index != text.len() {
455        return Err(JsonCodecError::validation(format!(
456            "strictJsonCodec: trailing JSON token near byte {}",
457            scanner.index
458        )));
459    }
460    Ok(())
461}
462
463/// `DecimalIntegerString` type alias.
464pub type DecimalIntegerString = String;
465/// `NonNegativeDecimalIntegerString` type alias.
466pub type NonNegativeDecimalIntegerString = String;
467
468/// Creates or computes `is_decimal_integer_string`.
469pub fn is_decimal_integer_string(value: &str) -> bool {
470    if value == "0" {
471        return true;
472    }
473    let rest = value.strip_prefix('-').unwrap_or(value);
474    !rest.is_empty()
475        && !rest.starts_with('0')
476        && rest.bytes().all(|byte| byte.is_ascii_digit())
477        && value != "-0"
478}
479
480/// Creates or computes `is_non_negative_decimal_integer_string`.
481pub fn is_non_negative_decimal_integer_string(value: &str) -> bool {
482    if value == "0" {
483        return true;
484    }
485    !value.is_empty()
486        && !value.starts_with('-')
487        && !value.starts_with('0')
488        && value.bytes().all(|byte| byte.is_ascii_digit())
489}
490
491/// Creates or computes `assert_decimal_integer_string`.
492pub fn assert_decimal_integer_string(
493    value: impl Into<String>,
494    label: &str,
495) -> JsonCodecResult<DecimalIntegerString> {
496    let value = value.into();
497    if is_decimal_integer_string(&value) {
498        Ok(value)
499    } else {
500        Err(JsonCodecError::validation(format!(
501            "{label} must be a canonical decimal integer string"
502        )))
503    }
504}
505
506/// Creates or computes `assert_non_negative_decimal_integer_string`.
507pub fn assert_non_negative_decimal_integer_string(
508    value: impl Into<String>,
509    label: &str,
510) -> JsonCodecResult<NonNegativeDecimalIntegerString> {
511    let value = value.into();
512    if is_non_negative_decimal_integer_string(&value) {
513        Ok(value)
514    } else {
515        Err(JsonCodecError::validation(format!(
516            "{label} must be a canonical non-negative decimal integer string"
517        )))
518    }
519}
520
521/// Creates or computes `i128_to_decimal_string`.
522pub fn i128_to_decimal_string(value: i128) -> DecimalIntegerString {
523    value.to_string()
524}
525
526/// Creates or computes `u128_to_non_negative_decimal_string`.
527pub fn u128_to_non_negative_decimal_string(value: u128) -> NonNegativeDecimalIntegerString {
528    value.to_string()
529}
530
531/// Creates or computes `decimal_string_to_i128`.
532pub fn decimal_string_to_i128(value: &str) -> JsonCodecResult<i128> {
533    assert_decimal_integer_string(value, "decimal integer")?
534        .parse::<i128>()
535        .map_err(|err| {
536            JsonCodecError::validation(format!("decimal integer is outside i128 range: {err}"))
537        })
538}
539
540/// Creates or computes `non_negative_decimal_string_to_u128`.
541pub fn non_negative_decimal_string_to_u128(value: &str) -> JsonCodecResult<u128> {
542    assert_non_negative_decimal_integer_string(value, "decimal integer")?
543        .parse::<u128>()
544        .map_err(|err| {
545            JsonCodecError::validation(format!("decimal integer is outside u128 range: {err}"))
546        })
547}