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