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    pub max_diagnostic_bytes: usize,
20}
21impl Default for Limits {
22    fn default() -> Self {
23        Self {
24            max_input_bytes: 16 << 20,
25            max_output_bytes: 16 << 20,
26            max_depth: 64,
27            max_nodes: 262144,
28            max_collection_entries: 65536,
29            max_string_bytes: 4 << 20,
30            max_blob_bytes: 8 << 20,
31            max_payload_bytes: 16 << 20,
32            max_allocation_bytes: 64 << 20,
33            max_work: 128 << 20,
34            max_diagnostic_bytes: 256,
35        }
36    }
37}
38
39#[derive(Debug)]
40pub(crate) struct Failure(pub Kind);
41pub(crate) type Result<T> = std::result::Result<T, Failure>;
42impl fmt::Display for Failure {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "{:?}", self.0)
45    }
46}
47impl std::error::Error for Failure {}
48impl serde::ser::Error for Failure {
49    fn custom<T: fmt::Display>(_: T) -> Self {
50        Self(Kind::TypeMismatch)
51    }
52}
53impl serde::de::Error for Failure {
54    fn custom<T: fmt::Display>(_: T) -> Self {
55        Self(Kind::TypeMismatch)
56    }
57}
58impl Failure {
59    pub(crate) fn core(self, format: &Format, operation: CodecOperation, limits: &Limits) -> Error {
60        let mut message = format!("{:?}", self.0);
61        message.truncate(limits.max_diagnostic_bytes.min(message.len()));
62        Error::Codec {
63            kind: self.0,
64            operation,
65            format: format.clone(),
66            message,
67        }
68    }
69}
70pub(crate) fn ensure(ok: bool, kind: Kind) -> Result<()> {
71    if ok {
72        Ok(())
73    } else {
74        Err(Failure(kind))
75    }
76}
77pub(crate) struct Budget<'a> {
78    pub limits: &'a Limits,
79    nodes: usize,
80    payload: usize,
81    allocation: usize,
82    work: usize,
83}
84fn add(counter: &mut usize, n: usize, max: usize) -> Result<()> {
85    *counter = counter.checked_add(n).ok_or(Failure(Kind::ResourceLimit))?;
86    ensure(*counter <= max, Kind::ResourceLimit)
87}
88impl<'a> Budget<'a> {
89    pub fn new(limits: &'a Limits) -> Self {
90        Self {
91            limits,
92            nodes: 0,
93            payload: 0,
94            allocation: 0,
95            work: 0,
96        }
97    }
98    pub fn work(&mut self, n: usize) -> Result<()> {
99        add(&mut self.work, n, self.limits.max_work)
100    }
101    pub fn allocate(&mut self, n: usize) -> Result<()> {
102        add(&mut self.allocation, n, self.limits.max_allocation_bytes)
103    }
104    pub fn node(&mut self, depth: usize) -> Result<()> {
105        ensure(depth <= self.limits.max_depth.min(256), Kind::ResourceLimit)?;
106        add(&mut self.nodes, 1, self.limits.max_nodes)?;
107        self.allocate(128)?;
108        self.work(1)
109    }
110    pub fn entries(&mut self, n: usize) -> Result<()> {
111        ensure(n <= self.limits.max_collection_entries, Kind::ResourceLimit)?;
112        self.work(1)
113    }
114    pub fn payload(&mut self, n: usize, blob: bool) -> Result<()> {
115        ensure(
116            n <= if blob {
117                self.limits.max_blob_bytes
118            } else {
119                self.limits.max_string_bytes
120            },
121            Kind::ResourceLimit,
122        )?;
123        add(&mut self.payload, n, self.limits.max_payload_bytes)?;
124        self.allocate(n.saturating_mul(2))?;
125        self.work(n)
126    }
127    pub fn key_work(&mut self, bytes: usize, entries: usize) -> Result<()> {
128        // Conservative comparison estimate for BTreeMap lookup/insertion and
129        // builder sorting. Long shared prefixes must not be free work.
130        let comparisons = (usize::BITS - entries.max(1).leading_zeros()) as usize;
131        self.work(bytes.saturating_mul(comparisons).saturating_mul(16))
132    }
133    pub fn tree(&mut self, v: &Value, depth: usize) -> Result<()> {
134        self.node(depth)?;
135        match v {
136            Value::String(s) => self.payload(s.len(), false)?,
137            Value::Bytes(b) => self.payload(b.len(), true)?,
138            Value::Array(a) => {
139                self.entries(a.len())?;
140                for v in a {
141                    self.tree(v, depth + 1)?;
142                }
143            }
144            Value::Map(m) => {
145                self.entries(m.len())?;
146                for (k, v) in m {
147                    self.payload(k.len(), false)?;
148                    self.key_work(k.len(), m.len())?;
149                    self.tree(v, depth + 1)?;
150                }
151            }
152            _ => {}
153        }
154        Ok(())
155    }
156}
157
158/// Validate an existing value before traversing or cloning it at a trust boundary.
159pub fn validate_value(value: &Value, limits: &Limits) -> std::result::Result<(), Error> {
160    Budget::new(limits)
161        .tree(value, 0)
162        .map_err(|e| e.core(&Format::VALUE, CodecOperation::Encode, limits))
163}