Skip to main content

structfs_serde_store/
limits.rs

1//! Shared finite limits for wire codecs and typed conversion.
2use std::fmt;
3use structfs_core_store::{CodecErrorKind as Kind, CodecOperation, Error, Format, Value};
4
5/// Inclusive bounds. Work counts traversed nodes and bytes, plus collection sorting
6/// estimates. Allocation counts conservative reservations, not allocator metadata.
7#[derive(Debug, Clone)]
8pub struct Limits {
9    pub max_input_bytes: usize,
10    pub max_output_bytes: usize,
11    pub max_depth: usize,
12    pub max_nodes: usize,
13    pub max_collection_entries: usize,
14    pub max_string_bytes: usize,
15    pub max_blob_bytes: usize,
16    pub max_payload_bytes: usize,
17    pub max_allocation_bytes: usize,
18    pub max_work: usize,
19    /// Output diagnostic byte cap. Custom Serde messages are captured with a
20    /// hard 1024-byte ceiling and locations with a 256-byte ceiling. Truncation
21    /// preserves UTF-8; locations use at most a quarter of this output budget.
22    pub max_diagnostic_bytes: usize,
23}
24impl Default for Limits {
25    fn default() -> Self {
26        Self {
27            max_input_bytes: 16 << 20,
28            max_output_bytes: 16 << 20,
29            max_depth: 64,
30            max_nodes: 262144,
31            max_collection_entries: 65536,
32            max_string_bytes: 4 << 20,
33            max_blob_bytes: 8 << 20,
34            max_payload_bytes: 16 << 20,
35            max_allocation_bytes: 64 << 20,
36            max_work: 128 << 20,
37            max_diagnostic_bytes: 256,
38        }
39    }
40}
41
42// Serde's Error::custom has no access to caller limits. Capture at most this
43// hard ceiling during formatting, then apply the caller's smaller output cap.
44const DIAGNOSTIC_CAPTURE_BYTES: usize = 1024;
45
46#[derive(Debug)]
47pub(crate) struct Failure {
48    kind: Kind,
49    message: String,
50    location: String,
51}
52pub(crate) type Result<T> = std::result::Result<T, Failure>;
53
54fn bounded(value: impl fmt::Display, max: usize) -> String {
55    use fmt::Write;
56    struct Output {
57        text: String,
58        max: usize,
59    }
60    impl fmt::Write for Output {
61        fn write_str(&mut self, text: &str) -> fmt::Result {
62            let mut n = text.len().min(self.max - self.text.len());
63            while !text.is_char_boundary(n) {
64                n -= 1;
65            }
66            self.text.push_str(&text[..n]);
67            if n < text.len() {
68                Err(fmt::Error)
69            } else {
70                Ok(())
71            }
72        }
73    }
74    let mut output = Output {
75        text: String::new(),
76        max,
77    };
78    let _ = write!(output, "{value}");
79    output.text
80}
81impl fmt::Display for Failure {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        if !self.location.is_empty() {
84            write!(f, "{}: ", self.location)?;
85        }
86        if self.message.is_empty() {
87            write!(f, "{:?}", self.kind)
88        } else {
89            f.write_str(&self.message)
90        }
91    }
92}
93impl std::error::Error for Failure {}
94impl serde::ser::Error for Failure {
95    fn custom<T: fmt::Display>(message: T) -> Self {
96        Self::diagnostic(message)
97    }
98}
99impl serde::de::Error for Failure {
100    fn custom<T: fmt::Display>(message: T) -> Self {
101        Self::diagnostic(message)
102    }
103}
104impl Failure {
105    pub(crate) fn new(kind: Kind) -> Self {
106        Self {
107            kind,
108            message: String::new(),
109            location: String::new(),
110        }
111    }
112    fn diagnostic(message: impl fmt::Display) -> Self {
113        Self {
114            kind: Kind::TypeMismatch,
115            message: bounded(message, DIAGNOSTIC_CAPTURE_BYTES),
116            location: String::new(),
117        }
118    }
119    pub(crate) fn at(mut self, component: impl fmt::Display) -> Self {
120        self.location = bounded(format_args!("{component}{}", self.location), 256);
121        self
122    }
123    pub(crate) fn core(
124        mut self,
125        format: &Format,
126        operation: CodecOperation,
127        limits: &Limits,
128    ) -> Error {
129        // Reserve room for the actual diagnostic even with a very long location.
130        let location = bounded(&self.location, limits.max_diagnostic_bytes / 4);
131        self.location.clear();
132        let message = if location.is_empty() {
133            bounded(format_args!("{}", self), limits.max_diagnostic_bytes)
134        } else {
135            bounded(
136                format_args!("{location}: {}", self),
137                limits.max_diagnostic_bytes,
138            )
139        };
140        Error::Codec {
141            kind: self.kind,
142            operation,
143            format: format.clone(),
144            message,
145        }
146    }
147}
148pub(crate) fn ensure(ok: bool, kind: Kind) -> Result<()> {
149    if ok {
150        Ok(())
151    } else {
152        Err(Failure::new(kind))
153    }
154}
155pub(crate) struct Budget<'a> {
156    pub limits: &'a Limits,
157    nodes: usize,
158    payload: usize,
159    allocation: usize,
160    work: usize,
161}
162fn add(counter: &mut usize, n: usize, max: usize) -> Result<()> {
163    *counter = counter
164        .checked_add(n)
165        .ok_or(Failure::new(Kind::ResourceLimit))?;
166    ensure(*counter <= max, Kind::ResourceLimit)
167}
168impl<'a> Budget<'a> {
169    pub fn new(limits: &'a Limits) -> Self {
170        Self {
171            limits,
172            nodes: 0,
173            payload: 0,
174            allocation: 0,
175            work: 0,
176        }
177    }
178    pub fn work(&mut self, n: usize) -> Result<()> {
179        add(&mut self.work, n, self.limits.max_work)
180    }
181    pub fn allocate(&mut self, n: usize) -> Result<()> {
182        add(&mut self.allocation, n, self.limits.max_allocation_bytes)
183    }
184    pub fn node(&mut self, depth: usize) -> Result<()> {
185        ensure(depth <= self.limits.max_depth.min(256), Kind::ResourceLimit)?;
186        add(&mut self.nodes, 1, self.limits.max_nodes)?;
187        self.allocate(128)?;
188        self.work(1)
189    }
190    pub fn entries(&mut self, n: usize) -> Result<()> {
191        ensure(n <= self.limits.max_collection_entries, Kind::ResourceLimit)?;
192        self.work(1)
193    }
194    pub fn payload(&mut self, n: usize, blob: bool) -> Result<()> {
195        ensure(
196            n <= if blob {
197                self.limits.max_blob_bytes
198            } else {
199                self.limits.max_string_bytes
200            },
201            Kind::ResourceLimit,
202        )?;
203        add(&mut self.payload, n, self.limits.max_payload_bytes)?;
204        self.allocate(n.saturating_mul(2))?;
205        self.work(n)
206    }
207    pub fn key_work(&mut self, bytes: usize, entries: usize) -> Result<()> {
208        // Conservative comparison estimate for BTreeMap lookup/insertion and
209        // builder sorting. Long shared prefixes must not be free work.
210        let comparisons = (usize::BITS - entries.max(1).leading_zeros()) as usize;
211        self.work(bytes.saturating_mul(comparisons).saturating_mul(16))
212    }
213    pub fn tree(&mut self, v: &Value, depth: usize) -> Result<()> {
214        self.node(depth)?;
215        match v {
216            Value::String(s) => self.payload(s.len(), false)?,
217            Value::Bytes(b) => self.payload(b.len(), true)?,
218            Value::Array(a) => {
219                self.entries(a.len())?;
220                for v in a {
221                    self.tree(v, depth + 1)?;
222                }
223            }
224            Value::Map(m) => {
225                self.entries(m.len())?;
226                for (k, v) in m {
227                    self.payload(k.len(), false)?;
228                    self.key_work(k.len(), m.len())?;
229                    self.tree(v, depth + 1)?;
230                }
231            }
232            _ => {}
233        }
234        Ok(())
235    }
236}
237
238/// Validate an existing value before traversing or cloning it at a trust boundary.
239pub fn validate_value(value: &Value, limits: &Limits) -> std::result::Result<(), Error> {
240    Budget::new(limits)
241        .tree(value, 0)
242        .map_err(|e| e.core(&Format::VALUE, CodecOperation::Encode, limits))
243}