Skip to main content

pjson_rs_domain/value_objects/
depth_guard.rs

1//! Generic per-thread deserialization recursion depth guard.
2//!
3//! Recursive types deserialized directly from untrusted input (e.g.
4//! [`crate::value_objects::Schema`] here, and `SchemaDefinitionDto` in
5//! `pjson-rs`) need to bound their own nesting depth independently of any
6//! format-specific recursion limit a particular [`serde::Deserializer`] may
7//! or may not enforce (`serde_json` happens to cap structural nesting
8//! around 128 levels, but that is not a contract other formats such as
9//! MessagePack or CBOR make). This module factors the thread-local counter,
10//! RAII drop-guard, and bounds-checked entry pattern shared by every such
11//! guard into one primitive, so each recursive type only needs to declare
12//! its own counter and call [`enter_deserialize_depth`].
13
14use std::cell::Cell;
15use std::thread::LocalKey;
16
17use crate::value_objects::MAX_DESERIALIZE_DEPTH;
18
19/// RAII guard returned by [`enter_deserialize_depth`].
20///
21/// Decrements the held thread-local counter on drop — including when the
22/// guarded deserialization call returns an error or unwinds — so the
23/// counter always reflects the caller's actual current nesting depth.
24#[must_use = "the depth guard must be held for the duration of deserialization, \
25              or the depth check is silently disabled"]
26pub struct DepthGuard {
27    counter: &'static LocalKey<Cell<usize>>,
28}
29
30impl Drop for DepthGuard {
31    fn drop(&mut self) {
32        self.counter.with(|depth| depth.set(depth.get() - 1));
33    }
34}
35
36/// Enters one nesting level of recursive deserialization tracked by `counter`.
37///
38/// Returns a guard that must be held for the duration of the nested
39/// deserialization call and released (dropped) afterward. Rejects with a
40/// deserialization error naming `type_name`, rather than recursing further,
41/// once [`MAX_DESERIALIZE_DEPTH`] is reached.
42///
43/// Each recursive type declares its own
44/// `thread_local! { static COUNTER: Cell<usize> = const { Cell::new(0) }; }`
45/// — nesting depth is tracked independently per type — and passes a
46/// `&'static` reference to it here together with a `type_name` used in the
47/// rejection message, so failures from different recursive types stay
48/// distinguishable from one another.
49///
50/// # Examples
51///
52/// ```
53/// use pjson_rs_domain::value_objects::enter_deserialize_depth;
54/// use std::cell::Cell;
55///
56/// thread_local! {
57///     static DEPTH: Cell<usize> = const { Cell::new(0) };
58/// }
59///
60/// fn enter() -> Result<(), serde_json::Error> {
61///     let _guard = enter_deserialize_depth::<serde_json::Error>(&DEPTH, "Example")?;
62///     Ok(())
63/// }
64///
65/// assert!(enter().is_ok());
66///
67/// // Once `MAX_DESERIALIZE_DEPTH` guards are held at the same time, the next
68/// // entry is rejected instead of recursing further.
69/// let guards: Vec<_> = (0..pjson_rs_domain::MAX_DESERIALIZE_DEPTH)
70///     .map(|_| enter_deserialize_depth::<serde_json::Error>(&DEPTH, "Example").unwrap())
71///     .collect();
72/// assert!(enter_deserialize_depth::<serde_json::Error>(&DEPTH, "Example").is_err());
73/// drop(guards);
74/// ```
75pub fn enter_deserialize_depth<E>(
76    counter: &'static LocalKey<Cell<usize>>,
77    type_name: &str,
78) -> Result<DepthGuard, E>
79where
80    E: serde::de::Error,
81{
82    counter.with(|depth| {
83        let current = depth.get();
84        if current >= MAX_DESERIALIZE_DEPTH {
85            return Err(E::custom(format_args!(
86                "{type_name} nesting depth exceeds maximum of {MAX_DESERIALIZE_DEPTH}"
87            )));
88        }
89        depth.set(current + 1);
90        Ok(DepthGuard { counter })
91    })
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    thread_local! {
99        static TEST_DEPTH: Cell<usize> = const { Cell::new(0) };
100    }
101
102    #[test]
103    fn test_enter_up_to_max_depth_succeeds_then_next_entry_rejected() {
104        let mut guards = Vec::with_capacity(MAX_DESERIALIZE_DEPTH);
105        for _ in 0..MAX_DESERIALIZE_DEPTH {
106            guards.push(enter_deserialize_depth::<serde_json::Error>(&TEST_DEPTH, "Test").unwrap());
107        }
108
109        match enter_deserialize_depth::<serde_json::Error>(&TEST_DEPTH, "Test") {
110            Ok(_) => panic!("entry beyond MAX_DESERIALIZE_DEPTH should be rejected"),
111            Err(err) => assert!(
112                err.to_string().contains(&format!(
113                    "Test nesting depth exceeds maximum of {MAX_DESERIALIZE_DEPTH}"
114                )),
115                "{err}"
116            ),
117        }
118
119        drop(guards);
120    }
121
122    #[test]
123    fn test_guard_drop_decrements_so_next_entry_starts_fresh() {
124        {
125            let _guard = enter_deserialize_depth::<serde_json::Error>(&TEST_DEPTH, "Test").unwrap();
126        }
127        assert_eq!(TEST_DEPTH.with(Cell::get), 0);
128
129        let mut guards = Vec::with_capacity(MAX_DESERIALIZE_DEPTH);
130        for _ in 0..MAX_DESERIALIZE_DEPTH {
131            guards.push(enter_deserialize_depth::<serde_json::Error>(&TEST_DEPTH, "Test").unwrap());
132        }
133        assert!(enter_deserialize_depth::<serde_json::Error>(&TEST_DEPTH, "Test").is_err());
134
135        drop(guards);
136        assert_eq!(TEST_DEPTH.with(Cell::get), 0);
137    }
138}