Skip to main content

rskit_schema/
limits.rs

1//! Structural JSON size and depth limits.
2
3use rskit_errors::{AppError, AppResult, ErrorCode};
4use serde_json::Value;
5
6/// Limits applied before compiling or validating untrusted JSON schema values.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct ValidationLimits {
9    /// Maximum structural nesting depth.
10    pub max_depth: usize,
11    /// Maximum number of JSON nodes.
12    pub max_nodes: usize,
13    /// Maximum UTF-8 byte length for a single JSON string value.
14    pub max_string_bytes: usize,
15    /// Maximum UTF-8 byte length for a single JSON object key.
16    pub max_key_bytes: usize,
17    /// Maximum cumulative UTF-8 bytes across all object keys and string values.
18    pub max_total_string_bytes: usize,
19}
20
21impl Default for ValidationLimits {
22    fn default() -> Self {
23        Self {
24            max_depth: 128,
25            max_nodes: 100_000,
26            max_string_bytes: 1_048_576,
27            max_key_bytes: 16_384,
28            max_total_string_bytes: 16_777_216,
29        }
30    }
31}
32
33impl ValidationLimits {
34    /// Create custom structural limits.
35    #[must_use]
36    pub const fn new(max_depth: usize, max_nodes: usize) -> Self {
37        Self {
38            max_depth,
39            max_nodes,
40            max_string_bytes: 1_048_576,
41            max_key_bytes: 16_384,
42            max_total_string_bytes: 16_777_216,
43        }
44    }
45
46    /// Override the maximum UTF-8 byte length for a single JSON string value.
47    #[must_use]
48    pub const fn with_max_string_bytes(mut self, max_string_bytes: usize) -> Self {
49        self.max_string_bytes = max_string_bytes;
50        self
51    }
52
53    /// Override the maximum UTF-8 byte length for a single JSON object key.
54    #[must_use]
55    pub const fn with_max_key_bytes(mut self, max_key_bytes: usize) -> Self {
56        self.max_key_bytes = max_key_bytes;
57        self
58    }
59
60    /// Override the maximum cumulative UTF-8 bytes across keys and strings.
61    #[must_use]
62    pub const fn with_max_total_string_bytes(mut self, max_total_string_bytes: usize) -> Self {
63        self.max_total_string_bytes = max_total_string_bytes;
64        self
65    }
66}
67
68pub(crate) fn check_json_limits(
69    name: &str,
70    value: &Value,
71    limits: ValidationLimits,
72) -> AppResult<()> {
73    let mut node_count = 0usize;
74    let mut total_string_bytes = 0usize;
75    let mut stack = vec![(value, 1usize)];
76
77    while let Some((node, depth)) = stack.pop() {
78        node_count = node_count.saturating_add(1);
79        if node_count > limits.max_nodes {
80            return Err(AppError::new(
81                ErrorCode::InvalidInput,
82                format!(
83                    "{name} exceeds maximum JSON node count {}",
84                    limits.max_nodes
85                ),
86            ));
87        }
88        if depth > limits.max_depth {
89            return Err(AppError::new(
90                ErrorCode::InvalidInput,
91                format!("{name} exceeds maximum JSON depth {}", limits.max_depth),
92            ));
93        }
94
95        match node {
96            Value::Array(values) => {
97                stack.extend(values.iter().map(|value| (value, depth + 1)));
98            }
99            Value::Object(values) => {
100                for key in values.keys() {
101                    check_string_bytes(name, "object key", key.len(), limits)?;
102                    total_string_bytes = add_string_bytes(
103                        name,
104                        total_string_bytes,
105                        key.len(),
106                        limits.max_total_string_bytes,
107                    )?;
108                }
109                stack.extend(values.values().map(|value| (value, depth + 1)));
110            }
111            Value::String(value) => {
112                check_string_bytes(name, "string", value.len(), limits)?;
113                total_string_bytes = add_string_bytes(
114                    name,
115                    total_string_bytes,
116                    value.len(),
117                    limits.max_total_string_bytes,
118                )?;
119            }
120            Value::Null | Value::Bool(_) | Value::Number(_) => {}
121        }
122    }
123
124    Ok(())
125}
126
127fn check_string_bytes(
128    name: &str,
129    kind: &str,
130    len: usize,
131    limits: ValidationLimits,
132) -> AppResult<()> {
133    let max = if kind == "object key" {
134        limits.max_key_bytes
135    } else {
136        limits.max_string_bytes
137    };
138
139    if len > max {
140        return Err(AppError::new(
141            ErrorCode::InvalidInput,
142            format!("{name} {kind} exceeds maximum UTF-8 byte length {max}"),
143        ));
144    }
145    Ok(())
146}
147
148fn add_string_bytes(
149    name: &str,
150    current: usize,
151    additional: usize,
152    max_total: usize,
153) -> AppResult<usize> {
154    let total = current.saturating_add(additional);
155    if total > max_total {
156        return Err(AppError::new(
157            ErrorCode::InvalidInput,
158            format!("{name} exceeds maximum cumulative string byte length {max_total}"),
159        ));
160    }
161    Ok(total)
162}