lnmp_core/
limits.rs

1//! Structural limit helpers for validating LNMP records and values.
2//!
3//! These limits are intended to provide a single place to constrain untrusted
4//! inputs before they are handed off to parser/encoder layers. All counts and
5//! lengths are measured in bytes and are inclusive.
6
7use crate::{LnmpField, LnmpRecord, LnmpValue};
8
9/// Errors returned when structural limits are exceeded.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum StructuralError {
12    /// A record exceeded the configured maximum depth.
13    MaxDepthExceeded {
14        /// Maximum depth configured.
15        max_depth: usize,
16        /// Actual depth encountered.
17        seen_depth: usize,
18    },
19    /// A record exceeded the configured total field count.
20    MaxFieldsExceeded {
21        /// Maximum fields configured.
22        max_fields: usize,
23        /// Actual field count encountered.
24        seen_fields: usize,
25    },
26    /// A string value exceeded the configured maximum length.
27    MaxStringLengthExceeded {
28        /// Maximum string length configured.
29        max_len: usize,
30        /// Actual string length encountered.
31        seen_len: usize,
32    },
33    /// An array contained more items than allowed.
34    MaxArrayLengthExceeded {
35        /// Maximum array length configured.
36        max_len: usize,
37        /// Actual array length encountered.
38        seen_len: usize,
39    },
40}
41
42impl std::fmt::Display for StructuralError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            StructuralError::MaxDepthExceeded {
46                max_depth,
47                seen_depth,
48            } => {
49                write!(
50                    f,
51                    "maximum nesting depth exceeded (max={}, saw={})",
52                    max_depth, seen_depth
53                )
54            }
55            StructuralError::MaxFieldsExceeded {
56                max_fields,
57                seen_fields,
58            } => {
59                write!(
60                    f,
61                    "maximum field count exceeded (max={}, saw={})",
62                    max_fields, seen_fields
63                )
64            }
65            StructuralError::MaxStringLengthExceeded { max_len, seen_len } => {
66                write!(
67                    f,
68                    "maximum string length exceeded (max={}, saw={})",
69                    max_len, seen_len
70                )
71            }
72            StructuralError::MaxArrayLengthExceeded { max_len, seen_len } => {
73                write!(
74                    f,
75                    "maximum array length exceeded (max={}, saw={})",
76                    max_len, seen_len
77                )
78            }
79        }
80    }
81}
82
83impl std::error::Error for StructuralError {}
84
85/// Configurable structural limits checked against `LnmpRecord`/`LnmpValue`.
86#[derive(Debug, Clone)]
87pub struct StructuralLimits {
88    /// Maximum allowed nesting depth (root = 0).
89    pub max_depth: usize,
90    /// Maximum total number of fields across the entire record (including nested).
91    pub max_fields: usize,
92    /// Maximum string length (bytes) for `String` values.
93    pub max_string_len: usize,
94    /// Maximum item count for arrays (string or nested record arrays).
95    pub max_array_items: usize,
96}
97
98impl Default for StructuralLimits {
99    fn default() -> Self {
100        Self {
101            // Depth 0 = top-level primitives, 1 = single layer nested record/array.
102            max_depth: 32,
103            // Generous default; callers should tune per use-case (e.g., LLM prompt budgets).
104            max_fields: 4096,
105            // Strings are expected to be short labels/values; cap to avoid unbounded text blobs.
106            max_string_len: 16 * 1024,
107            // Reasonable default to prevent pathological arrays.
108            max_array_items: 1024,
109        }
110    }
111}
112
113impl StructuralLimits {
114    /// Validates a record against the configured limits.
115    pub fn validate_record(&self, record: &LnmpRecord) -> Result<(), StructuralError> {
116        let mut field_count = 0;
117        self.validate_fields(record.fields(), 0, &mut field_count)
118    }
119
120    fn validate_fields(
121        &self,
122        fields: &[LnmpField],
123        depth: usize,
124        field_count: &mut usize,
125    ) -> Result<(), StructuralError> {
126        if depth > self.max_depth {
127            return Err(StructuralError::MaxDepthExceeded {
128                max_depth: self.max_depth,
129                seen_depth: depth,
130            });
131        }
132
133        for field in fields {
134            *field_count += 1;
135            if *field_count > self.max_fields {
136                return Err(StructuralError::MaxFieldsExceeded {
137                    max_fields: self.max_fields,
138                    seen_fields: *field_count,
139                });
140            }
141            self.validate_value(&field.value, depth + 1, field_count)?;
142        }
143
144        Ok(())
145    }
146
147    fn validate_value(
148        &self,
149        value: &LnmpValue,
150        depth: usize,
151        field_count: &mut usize,
152    ) -> Result<(), StructuralError> {
153        match value {
154            LnmpValue::String(s) => {
155                if s.len() > self.max_string_len {
156                    return Err(StructuralError::MaxStringLengthExceeded {
157                        max_len: self.max_string_len,
158                        seen_len: s.len(),
159                    });
160                }
161                Ok(())
162            }
163            LnmpValue::StringArray(arr) => {
164                if arr.len() > self.max_array_items {
165                    return Err(StructuralError::MaxArrayLengthExceeded {
166                        max_len: self.max_array_items,
167                        seen_len: arr.len(),
168                    });
169                }
170                for s in arr {
171                    if s.len() > self.max_string_len {
172                        return Err(StructuralError::MaxStringLengthExceeded {
173                            max_len: self.max_string_len,
174                            seen_len: s.len(),
175                        });
176                    }
177                }
178                Ok(())
179            }
180            LnmpValue::NestedRecord(record) => {
181                self.validate_fields(record.fields(), depth, field_count)
182            }
183            LnmpValue::NestedArray(records) => {
184                if records.len() > self.max_array_items {
185                    return Err(StructuralError::MaxArrayLengthExceeded {
186                        max_len: self.max_array_items,
187                        seen_len: records.len(),
188                    });
189                }
190                for record in records {
191                    self.validate_fields(record.fields(), depth, field_count)?;
192                }
193                Ok(())
194            }
195            // Primitive numeric/bool types do not need extra checks.
196            LnmpValue::Int(_) | LnmpValue::Float(_) | LnmpValue::Bool(_) => Ok(()),
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    fn basic_record(string_len: usize) -> LnmpRecord {
206        let mut record = LnmpRecord::new();
207        record.add_field(LnmpField {
208            fid: 1,
209            value: LnmpValue::String("a".repeat(string_len)),
210        });
211        record
212    }
213
214    #[test]
215    fn validates_within_limits() {
216        let limits = StructuralLimits::default();
217        let record = basic_record(4);
218        assert!(limits.validate_record(&record).is_ok());
219    }
220
221    #[test]
222    fn rejects_oversized_string() {
223        let limits = StructuralLimits {
224            max_string_len: 2,
225            ..StructuralLimits::default()
226        };
227        let record = basic_record(3);
228        let err = limits.validate_record(&record).unwrap_err();
229        assert!(matches!(
230            err,
231            StructuralError::MaxStringLengthExceeded { .. }
232        ));
233    }
234
235    #[test]
236    fn rejects_excessive_depth() {
237        let mut inner = LnmpRecord::new();
238        inner.add_field(LnmpField {
239            fid: 2,
240            value: LnmpValue::Int(1),
241        });
242        let mut outer = LnmpRecord::new();
243        outer.add_field(LnmpField {
244            fid: 1,
245            value: LnmpValue::NestedRecord(Box::new(inner)),
246        });
247
248        let limits = StructuralLimits {
249            max_depth: 0,
250            ..StructuralLimits::default()
251        };
252        let err = limits.validate_record(&outer).unwrap_err();
253        assert!(matches!(err, StructuralError::MaxDepthExceeded { .. }));
254    }
255
256    #[test]
257    fn rejects_field_count_overflow() {
258        let mut record = LnmpRecord::new();
259        record.add_field(LnmpField {
260            fid: 1,
261            value: LnmpValue::Int(1),
262        });
263        record.add_field(LnmpField {
264            fid: 2,
265            value: LnmpValue::Int(2),
266        });
267
268        let limits = StructuralLimits {
269            max_fields: 1,
270            ..StructuralLimits::default()
271        };
272        let err = limits.validate_record(&record).unwrap_err();
273        assert!(matches!(err, StructuralError::MaxFieldsExceeded { .. }));
274    }
275
276    #[test]
277    fn rejects_array_length_overflow() {
278        let mut record = LnmpRecord::new();
279        record.add_field(LnmpField {
280            fid: 1,
281            value: LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()]),
282        });
283        let limits = StructuralLimits {
284            max_array_items: 1,
285            ..StructuralLimits::default()
286        };
287        let err = limits.validate_record(&record).unwrap_err();
288        assert!(matches!(
289            err,
290            StructuralError::MaxArrayLengthExceeded { .. }
291        ));
292    }
293}