use crate::error::TalaDbError;
pub const MAX_JSON_DEPTH: usize = 64;
pub fn check_json_depth(value: &serde_json::Value) -> Result<(), TalaDbError> {
let mut stack: Vec<(&serde_json::Value, usize)> = vec![(value, 1)];
while let Some((node, depth)) = stack.pop() {
if depth > MAX_JSON_DEPTH {
return Err(TalaDbError::InvalidFilter(format!(
"JSON nested deeper than {MAX_JSON_DEPTH} levels"
)));
}
match node {
serde_json::Value::Array(items) => {
for item in items {
stack.push((item, depth + 1));
}
}
serde_json::Value::Object(map) => {
for (_, v) in map {
stack.push((v, depth + 1));
}
}
_ => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn nest_arrays(depth: usize) -> serde_json::Value {
let mut v = serde_json::Value::Null;
for _ in 1..depth {
v = serde_json::Value::Array(vec![v]);
}
v
}
fn nest_objects(depth: usize) -> serde_json::Value {
let mut v = serde_json::Value::Null;
for _ in 1..depth {
let mut map = serde_json::Map::new();
map.insert("k".to_string(), v);
v = serde_json::Value::Object(map);
}
v
}
#[test]
fn accepts_ordinary_shapes() {
for v in [
serde_json::json!(null),
serde_json::json!(1),
serde_json::json!("s"),
serde_json::json!([]),
serde_json::json!({}),
serde_json::json!({ "status": "active", "tags": ["a", "b"] }),
serde_json::json!({ "$or": [{ "a": 1 }, { "$not": { "b": 2 } }] }),
] {
assert!(check_json_depth(&v).is_ok(), "rejected {v}");
}
}
#[test]
fn the_boundary_is_exact() {
assert!(check_json_depth(&nest_arrays(MAX_JSON_DEPTH)).is_ok());
assert!(check_json_depth(&nest_arrays(MAX_JSON_DEPTH + 1)).is_err());
assert!(check_json_depth(&nest_objects(MAX_JSON_DEPTH)).is_ok());
assert!(check_json_depth(&nest_objects(MAX_JSON_DEPTH + 1)).is_err());
}
#[test]
fn does_not_consume_stack_proportional_to_depth() {
let deep = nest_arrays(10_000);
let checked = std::thread::Builder::new()
.stack_size(256 * 1024)
.spawn(move || {
let rejected = check_json_depth(&deep).is_err();
let mut node = deep;
while let serde_json::Value::Array(mut items) = node {
node = items.pop().unwrap_or(serde_json::Value::Null);
}
rejected
})
.expect("spawn")
.join();
match checked {
Ok(rejected) => assert!(rejected, "a 10,000-deep value must be rejected"),
Err(_) => panic!(
"the depth check overflowed a 256 KiB stack — it is recursing over \
the input instead of walking an explicit stack"
),
}
}
#[test]
fn depth_is_measured_down_a_branch_not_across_siblings() {
let wide = serde_json::Value::Array((0..10_000).map(serde_json::Value::from).collect());
assert!(
check_json_depth(&wide).is_ok(),
"a wide-but-shallow value must be accepted"
);
}
#[test]
fn the_error_names_the_limit() {
let err = check_json_depth(&nest_arrays(MAX_JSON_DEPTH + 1)).expect_err("must reject");
assert!(
err.to_string().contains(&MAX_JSON_DEPTH.to_string()),
"the message should say what the limit is: {err}"
);
}
}