Skip to main content

lemma/
limits.rs

1use crate::error::Error;
2use crate::parsing::source::Source;
3
4pub const MAX_SPEC_NAME_LENGTH: usize = 128;
5pub const MAX_DATA_NAME_LENGTH: usize = 256;
6pub const MAX_RULE_NAME_LENGTH: usize = 256;
7
8/// Maximum character length for a text value (data/runtime input).
9pub const MAX_TEXT_VALUE_LENGTH: usize = 1024;
10
11/// Maximum magnitude of a unit exponent, both as written (`meter^3`) and in every
12/// planned measure decomposition (`meter^2 * meter^3`, compound units of compound units).
13pub const MAX_UNIT_EXPONENT: i32 = 64;
14
15/// Maximum bit length of the numerator and of the denominator of an arithmetic result
16/// (about 9,860 decimal digits). Larger results veto, which bounds the cost of every
17/// exact rational operation.
18pub const MAX_RATIONAL_BITS: u64 = 1 << 15;
19
20/// Validate that a name does not exceed the given character limit.
21/// `kind` is a human-readable noun like "spec", "data", "rule", or "type".
22pub fn check_max_length(
23    name: &str,
24    limit: usize,
25    kind: &str,
26    source: Option<Source>,
27) -> Result<(), Error> {
28    if name.len() > limit {
29        return Err(Error::resource_limit_exceeded(
30            format!("max_{kind}_name_length"),
31            format!("{limit} characters"),
32            format!("{} characters", name.len()),
33            format!("Shorten the {kind} name to at most {limit} characters"),
34            source,
35            None,
36            None,
37        ));
38    }
39    Ok(())
40}
41
42/// Limits to prevent abuse and enable predictable resource usage
43///
44/// These limits protect against malicious inputs while being generous enough
45/// for all legitimate use cases.
46#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
47pub struct ResourceLimits {
48    /// Maximum size of one loaded source text in bytes.
49    /// Real usage: ~5KB, Limit: 5MB (1000x)
50    pub max_source_size_bytes: usize,
51
52    /// Maximum expression nesting depth
53    /// Real usage: ~3 levels, Limit: 7. Deeper logic via rule composition.
54    pub max_expression_depth: usize,
55
56    /// Maximum expression nodes per source (parser-level)
57    /// Quick-reject for pathological single sources.
58    pub max_expression_count: usize,
59
60    /// Maximum size of a single data value in bytes. For a value list each item is checked;
61    /// the list itself is bounded by `max_list_rows`, `max_list_depth`, `max_list_nodes`.
62    /// Real usage: ~100 bytes, Limit: 1KB (10x)
63    /// Enables server pre-allocation for zero-allocation evaluation
64    pub max_data_value_bytes: usize,
65
66    /// Maximum total bytes to load in one batch (and/or in-memory size of loaded specs)
67    pub max_loaded_bytes: usize,
68
69    /// Maximum number of sources in one load batch (e.g. after expanding paths on disk)
70    pub max_sources: usize,
71
72    /// Maximum unique normal-form cells reachable from one rule root in the
73    /// shared graph after normalize. Rule references count as one cell: rule references are
74    /// evaluation boundaries, so this bounds only intra-rule IR size. Bounds
75    /// planning work and shipped table size. Default: 30,000.
76    pub max_normalized_expression_nodes: usize,
77
78    /// Maximum depth of the spec dependency chain (`uses` imports) from the
79    /// root spec. Bounds the depth of the walks in dependency discovery and graph building.
80    /// Real usage: ~3 levels, Limit: 32 (10x).
81    pub max_spec_dependency_depth: usize,
82
83    /// Maximum number of specs in one dependency DAG (the root spec plus all
84    /// transitive dependencies). Bounds per-plan memory and planning work.
85    pub max_dag_specs: usize,
86
87    /// Maximum rows in one list (evaluation-data bind).
88    pub max_list_rows: usize,
89    /// Maximum nested self-list depth, the spec holding the list being level 1.
90    pub max_list_depth: usize,
91    /// Maximum total rows across one evaluation-data list tree.
92    pub max_list_nodes: usize,
93}
94
95impl Default for ResourceLimits {
96    fn default() -> Self {
97        Self {
98            max_source_size_bytes: 5 * 1024 * 1024, // 5 MB
99            max_expression_depth: 7,
100            max_expression_count: 65_536,
101            max_data_value_bytes: 1024,         // 1 KB
102            max_loaded_bytes: 50 * 1024 * 1024, // 50 MB
103            max_sources: 4096,
104            max_normalized_expression_nodes: 30_000,
105            max_spec_dependency_depth: 32,
106            max_dag_specs: 4096,
107            max_list_rows: 1024,
108            max_list_depth: 32,
109            max_list_nodes: 4096,
110        }
111    }
112}
113
114impl ResourceLimits {
115    /// Apply one named limit override. Unknown keys return `Err`.
116    pub fn apply(&mut self, key: &str, value: usize) -> Result<(), String> {
117        match key {
118            "max_source_size_bytes" => self.max_source_size_bytes = value,
119            "max_expression_depth" => self.max_expression_depth = value,
120            "max_expression_count" => self.max_expression_count = value,
121            "max_data_value_bytes" => self.max_data_value_bytes = value,
122            "max_loaded_bytes" => self.max_loaded_bytes = value,
123            "max_sources" => self.max_sources = value,
124            "max_normalized_expression_nodes" => self.max_normalized_expression_nodes = value,
125            "max_spec_dependency_depth" => self.max_spec_dependency_depth = value,
126            "max_dag_specs" => self.max_dag_specs = value,
127            "max_list_rows" => self.max_list_rows = value,
128            "max_list_depth" => self.max_list_depth = value,
129            "max_list_nodes" => self.max_list_nodes = value,
130            other => return Err(format!("unknown limits key: '{other}'")),
131        }
132        Ok(())
133    }
134}
135
136/// Convert a JS/JSON number to a [`usize`] limit. Rejects non-integers, negatives,
137/// values outside the f64 safe-integer range, and values that do not fit in `usize`
138/// (e.g. large safe integers on wasm32).
139#[cfg(any(test, target_arch = "wasm32"))]
140pub(crate) fn usize_limit_from_f64(key: &str, value: f64) -> Result<usize, String> {
141    if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
142        return Err(format!(
143            "limits value for '{key}' must be a non-negative integer"
144        ));
145    }
146    let as_u64 = value as u64;
147    if value >= 2f64.powi(53) || as_u64 as f64 != value {
148        return Err(format!(
149            "limits value for '{key}' must be a non-negative integer within f64 safe range"
150        ));
151    }
152    if as_u64 > usize::MAX as u64 {
153        return Err(format!(
154            "limits value for '{key}' exceeds platform usize maximum ({})",
155            usize::MAX
156        ));
157    }
158    Ok(as_u64 as usize)
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn apply_sets_known_key() {
167        let mut limits = ResourceLimits::default();
168        limits.apply("max_sources", 7).expect("known key");
169        assert_eq!(limits.max_sources, 7);
170    }
171
172    #[test]
173    fn apply_rejects_removed_max_normal_form_depth() {
174        let mut limits = ResourceLimits::default();
175        assert!(limits.apply("max_normal_form_depth", 99).is_err());
176    }
177
178    #[test]
179    fn apply_rejects_unknown_key() {
180        let mut limits = ResourceLimits::default();
181        let err = limits.apply("not_a_limit", 1).expect_err("unknown");
182        assert!(err.contains("unknown limits key"));
183    }
184
185    #[test]
186    fn usize_limit_from_f64_accepts_integer() {
187        assert_eq!(usize_limit_from_f64("max_sources", 7.0).unwrap(), 7);
188    }
189
190    #[test]
191    fn usize_limit_from_f64_rejects_fraction() {
192        let err = usize_limit_from_f64("max_sources", 1.5).expect_err("fraction");
193        assert!(err.contains("non-negative integer"));
194    }
195
196    #[test]
197    fn usize_limit_from_f64_rejects_above_safe_integer() {
198        let err = usize_limit_from_f64("max_sources", 2f64.powi(53)).expect_err("unsafe");
199        assert!(err.contains("f64 safe range"));
200    }
201
202    #[test]
203    fn usize_limit_from_f64_rejects_above_usize_max() {
204        // On 64-bit hosts usize::MAX is outside f64 safe integers, so the safe-range
205        // check fires first. On 32-bit, a safe integer above u32::MAX must error here.
206        if (usize::MAX as u64) < (1u64 << 53) {
207            let too_big = (usize::MAX as u64).saturating_add(1) as f64;
208            let err = usize_limit_from_f64("max_loaded_bytes", too_big).expect_err("overflow");
209            assert!(err.contains("exceeds platform usize maximum"), "got: {err}");
210        }
211    }
212}