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}