Skip to main content

ferrin_schema/
json.rs

1//! JSON parsing with resource limits.
2
3use ferrin_spec::error::JsonParseError;
4use serde_json::Value;
5
6use crate::error::SchemaError;
7use crate::schema::Schema;
8
9/// Default maximum nesting depth.
10pub const DEFAULT_MAX_DEPTH: usize = 128;
11
12/// Default maximum input size in bytes (64 MiB).
13pub const DEFAULT_MAX_BYTES: usize = 64 * 1024 * 1024;
14
15/// Resource limits applied while parsing JSON text.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ParseLimits {
18    /// Maximum nesting depth of arrays and objects.
19    pub max_depth: usize,
20    /// Maximum input length in bytes.
21    pub max_bytes: usize,
22}
23
24impl Default for ParseLimits {
25    fn default() -> Self {
26        Self {
27            max_depth: DEFAULT_MAX_DEPTH,
28            max_bytes: DEFAULT_MAX_BYTES,
29        }
30    }
31}
32
33#[derive(Debug, thiserror::Error)]
34enum LimitError {
35    #[error("input of {len} bytes exceeds the limit of {max} bytes")]
36    TooLarge { len: usize, max: usize },
37    #[error("nesting depth exceeds the limit of {max}")]
38    TooDeep { max: usize },
39    #[error("object contains forbidden prototype property")]
40    PrototypeProperty,
41}
42
43/// Parses `text` with the default limits.
44///
45/// # Errors
46///
47/// Returns [`JsonParseError`] when the text is not valid JSON or exceeds the
48/// limits.
49pub fn parse(text: &str) -> Result<Value, JsonParseError> {
50    parse_with(text, ParseLimits::default())
51}
52
53/// Parses `text` with explicit limits.
54///
55/// # Errors
56///
57/// Returns [`JsonParseError`] when the text is not valid JSON or exceeds the
58/// limits.
59pub fn parse_with(text: &str, limits: ParseLimits) -> Result<Value, JsonParseError> {
60    if text.len() > limits.max_bytes {
61        return Err(JsonParseError::new(
62            truncate(text),
63            LimitError::TooLarge {
64                len: text.len(),
65                max: limits.max_bytes,
66            },
67        ));
68    }
69    let value: Value =
70        serde_json::from_str(text).map_err(|error| JsonParseError::new(truncate(text), error))?;
71    if depth(&value) > limits.max_depth {
72        return Err(JsonParseError::new(
73            truncate(text),
74            LimitError::TooDeep {
75                max: limits.max_depth,
76            },
77        ));
78    }
79    check_object_keys(&value).map_err(|error| JsonParseError::new(truncate(text), error))?;
80    Ok(value)
81}
82
83fn check_object_keys(value: &Value) -> Result<(), LimitError> {
84    let mut pending = vec![value];
85    while let Some(value) = pending.pop() {
86        match value {
87            Value::Object(fields) => {
88                if fields.contains_key("__proto__")
89                    || fields
90                        .get("constructor")
91                        .and_then(Value::as_object)
92                        .is_some_and(|constructor| constructor.contains_key("prototype"))
93                {
94                    return Err(LimitError::PrototypeProperty);
95                }
96                pending.extend(fields.values());
97            }
98            Value::Array(values) => pending.extend(values),
99            _ => {}
100        }
101    }
102    Ok(())
103}
104
105/// Parses `text` and validates it against `schema`.
106///
107/// # Errors
108///
109/// Returns [`SchemaError::JsonParse`] when the text is not valid JSON and
110/// [`SchemaError::TypeValidation`] when the value does not match the schema.
111pub fn parse_with_schema<T>(text: &str, schema: &Schema<T>) -> Result<T, SchemaError> {
112    let value = parse(text)?;
113    Ok(schema.validate(value)?)
114}
115
116/// Returns `true` when `text` parses as JSON within the default limits.
117#[must_use]
118pub fn is_parsable(text: &str) -> bool {
119    parse(text).is_ok()
120}
121
122/// Nesting depth of a value: scalars are 0, `[]`/`{}` are 1.
123#[must_use]
124pub fn depth(value: &Value) -> usize {
125    let mut max = 0;
126    let mut stack: Vec<(&Value, usize)> = vec![(value, 0)];
127    while let Some((current, level)) = stack.pop() {
128        match current {
129            Value::Array(items) => {
130                max = max.max(level + 1);
131                stack.extend(items.iter().map(|item| (item, level + 1)));
132            }
133            Value::Object(map) => {
134                max = max.max(level + 1);
135                stack.extend(map.values().map(|item| (item, level + 1)));
136            }
137            _ => max = max.max(level),
138        }
139    }
140    max
141}
142
143/// Keeps error payloads bounded: at most 4 KiB of the offending text.
144fn truncate(text: &str) -> String {
145    const MAX: usize = 4096;
146    if text.len() <= MAX {
147        return text.to_owned();
148    }
149    let mut end = MAX;
150    while !text.is_char_boundary(end) {
151        end -= 1;
152    }
153    format!("{}...", &text[..end])
154}