1use anyhow::{Result, bail};
23use serde::{Deserialize, Serialize};
24use std::env;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct RecursionLimits {
32 pub file_ops_depth: usize,
48
49 pub expr_depth: usize,
65
66 pub expr_fuel: usize,
80}
81
82impl Default for RecursionLimits {
83 fn default() -> Self {
84 Self {
85 file_ops_depth: 100,
86 expr_depth: 100,
87 expr_fuel: 1000,
88 }
89 }
90}
91
92impl RecursionLimits {
93 pub const MIN_FILE_OPS_DEPTH: usize = 50;
95
96 pub const MAX_FILE_OPS_DEPTH: usize = 150;
98
99 pub const ABSOLUTE_MAX_FILE_OPS_DEPTH: usize = 200;
105
106 pub const MIN_EXPR_DEPTH: usize = 10;
108
109 pub const MAX_EXPR_DEPTH: usize = 100;
111
112 pub const ABSOLUTE_MAX_EXPR_DEPTH: usize = 200;
114
115 pub const MIN_EXPR_FUEL: usize = 100;
117
118 pub const MAX_EXPR_FUEL: usize = 5_000;
120
121 pub const ABSOLUTE_MAX_EXPR_FUEL: usize = 10_000;
127
128 pub fn new(file_ops_depth: usize, expr_depth: usize, expr_fuel: usize) -> Result<Self> {
135 let config = Self {
136 file_ops_depth,
137 expr_depth,
138 expr_fuel,
139 };
140 config.validate()?;
141 Ok(config)
142 }
143
144 pub fn load_or_default() -> Result<Self> {
151 let mut config = Self::default();
152
153 if let Ok(file_ops_str) = env::var("SQRY_RECURSION_FILE_OPS_DEPTH") {
155 config.file_ops_depth =
156 Self::parse_env_var(&file_ops_str, "SQRY_RECURSION_FILE_OPS_DEPTH")?;
157 }
158
159 if let Ok(expr_depth_str) = env::var("SQRY_RECURSION_EXPR_DEPTH") {
160 config.expr_depth = Self::parse_env_var(&expr_depth_str, "SQRY_RECURSION_EXPR_DEPTH")?;
161 }
162
163 if let Ok(expr_fuel_str) = env::var("SQRY_RECURSION_EXPR_FUEL") {
164 config.expr_fuel = Self::parse_env_var(&expr_fuel_str, "SQRY_RECURSION_EXPR_FUEL")?;
165 }
166
167 config.validate()?;
168 Ok(config)
169 }
170
171 pub fn effective_file_ops_depth(&self) -> Result<usize> {
178 if self.file_ops_depth == 0 {
179 bail!("recursion.file_ops_depth cannot be 0 (unlimited not allowed for safety)");
180 }
181
182 if self.file_ops_depth < Self::MIN_FILE_OPS_DEPTH {
183 bail!(
184 "recursion.file_ops_depth {} is below minimum {}",
185 self.file_ops_depth,
186 Self::MIN_FILE_OPS_DEPTH
187 );
188 }
189
190 if self.file_ops_depth > Self::MAX_FILE_OPS_DEPTH {
191 tracing::warn!(
192 "recursion.file_ops_depth {} exceeds recommended maximum {}",
193 self.file_ops_depth,
194 Self::MAX_FILE_OPS_DEPTH
195 );
196 }
197
198 if self.file_ops_depth > Self::ABSOLUTE_MAX_FILE_OPS_DEPTH {
199 bail!(
200 "recursion.file_ops_depth {} exceeds absolute hard cap {}",
201 self.file_ops_depth,
202 Self::ABSOLUTE_MAX_FILE_OPS_DEPTH
203 );
204 }
205
206 Ok(self.file_ops_depth)
207 }
208
209 pub fn effective_expr_depth(&self) -> Result<usize> {
216 if self.expr_depth == 0 {
217 bail!("recursion.expr_depth cannot be 0 (unlimited not allowed for safety)");
218 }
219
220 if self.expr_depth < Self::MIN_EXPR_DEPTH {
221 bail!(
222 "recursion.expr_depth {} is below minimum {}",
223 self.expr_depth,
224 Self::MIN_EXPR_DEPTH
225 );
226 }
227
228 if self.expr_depth > Self::MAX_EXPR_DEPTH {
229 tracing::warn!(
230 "recursion.expr_depth {} exceeds recommended maximum {}",
231 self.expr_depth,
232 Self::MAX_EXPR_DEPTH
233 );
234 }
235
236 if self.expr_depth > Self::ABSOLUTE_MAX_EXPR_DEPTH {
237 bail!(
238 "recursion.expr_depth {} exceeds absolute hard cap {}",
239 self.expr_depth,
240 Self::ABSOLUTE_MAX_EXPR_DEPTH
241 );
242 }
243
244 Ok(self.expr_depth)
245 }
246
247 pub fn effective_expr_fuel(&self) -> Result<usize> {
254 if self.expr_fuel == 0 {
255 bail!("recursion.expr_fuel cannot be 0 (unlimited not allowed for safety)");
256 }
257
258 if self.expr_fuel < Self::MIN_EXPR_FUEL {
259 bail!(
260 "recursion.expr_fuel {} is below minimum {}",
261 self.expr_fuel,
262 Self::MIN_EXPR_FUEL
263 );
264 }
265
266 if self.expr_fuel > Self::MAX_EXPR_FUEL {
267 tracing::warn!(
268 "recursion.expr_fuel {} exceeds recommended maximum {}",
269 self.expr_fuel,
270 Self::MAX_EXPR_FUEL
271 );
272 }
273
274 if self.expr_fuel > Self::ABSOLUTE_MAX_EXPR_FUEL {
275 bail!(
276 "recursion.expr_fuel {} exceeds absolute hard cap {}",
277 self.expr_fuel,
278 Self::ABSOLUTE_MAX_EXPR_FUEL
279 );
280 }
281
282 Ok(self.expr_fuel)
283 }
284
285 fn validate(&self) -> Result<()> {
287 self.effective_file_ops_depth()?;
289 self.effective_expr_depth()?;
290 self.effective_expr_fuel()?;
291 Ok(())
292 }
293
294 fn parse_env_var(value: &str, var_name: &str) -> Result<usize> {
296 match value.parse::<usize>() {
297 Ok(parsed) => Ok(parsed),
298 Err(_) => bail!("Invalid value for {var_name}: '{value}'. Expected usize"),
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_default_config() {
309 let config = RecursionLimits::default();
310 assert_eq!(config.file_ops_depth, 100);
311 assert_eq!(config.expr_depth, 100);
312 assert_eq!(config.expr_fuel, 1000);
313 assert!(config.effective_file_ops_depth().is_ok());
314 assert!(config.effective_expr_depth().is_ok());
315 assert!(config.effective_expr_fuel().is_ok());
316 }
317
318 #[test]
319 fn test_new_with_valid_values() {
320 let config = RecursionLimits::new(200, 50, 5000).unwrap();
321 assert_eq!(config.effective_file_ops_depth().unwrap(), 200);
322 assert_eq!(config.effective_expr_depth().unwrap(), 50);
323 assert_eq!(config.effective_expr_fuel().unwrap(), 5000);
324 }
325
326 #[test]
327 fn test_file_ops_depth_zero_fails() {
328 let result = RecursionLimits::new(0, 100, 1000);
329 assert!(result.is_err());
330 assert!(result.unwrap_err().to_string().contains("cannot be 0"));
331 }
332
333 #[test]
334 fn test_expr_depth_zero_fails() {
335 let result = RecursionLimits::new(100, 0, 1000);
336 assert!(result.is_err());
337 assert!(result.unwrap_err().to_string().contains("cannot be 0"));
338 }
339
340 #[test]
341 fn test_expr_fuel_zero_fails() {
342 let result = RecursionLimits::new(100, 100, 0);
343 assert!(result.is_err());
344 assert!(result.unwrap_err().to_string().contains("cannot be 0"));
345 }
346
347 #[test]
348 fn test_file_ops_depth_below_minimum_fails() {
349 let result = RecursionLimits::new(25, 100, 1000);
350 assert!(result.is_err());
351 assert!(result.unwrap_err().to_string().contains("below minimum 50"));
352 }
353
354 #[test]
355 fn test_expr_depth_below_minimum_fails() {
356 let result = RecursionLimits::new(100, 5, 1000);
357 assert!(result.is_err());
358 assert!(result.unwrap_err().to_string().contains("below minimum 10"));
359 }
360
361 #[test]
362 fn test_expr_fuel_below_minimum_fails() {
363 let result = RecursionLimits::new(100, 100, 50);
364 assert!(result.is_err());
365 assert!(
366 result
367 .unwrap_err()
368 .to_string()
369 .contains("below minimum 100")
370 );
371 }
372
373 #[test]
374 fn test_file_ops_depth_at_minimum_succeeds() {
375 let config = RecursionLimits::new(50, 100, 1000).unwrap();
376 assert_eq!(config.effective_file_ops_depth().unwrap(), 50);
377 }
378
379 #[test]
380 fn test_expr_depth_at_minimum_succeeds() {
381 let config = RecursionLimits::new(100, 10, 1000).unwrap();
382 assert_eq!(config.effective_expr_depth().unwrap(), 10);
383 }
384
385 #[test]
386 fn test_expr_fuel_at_minimum_succeeds() {
387 let config = RecursionLimits::new(100, 100, 100).unwrap();
388 assert_eq!(config.effective_expr_fuel().unwrap(), 100);
389 }
390
391 #[test]
392 fn test_file_ops_depth_at_hard_cap_succeeds() {
393 let config = RecursionLimits::new(200, 100, 1000).unwrap();
394 assert_eq!(config.effective_file_ops_depth().unwrap(), 200);
395 }
396
397 #[test]
398 fn test_expr_depth_at_hard_cap_succeeds() {
399 let config = RecursionLimits::new(100, 200, 1000).unwrap();
400 assert_eq!(config.effective_expr_depth().unwrap(), 200);
401 }
402
403 #[test]
404 fn test_expr_fuel_at_hard_cap_succeeds() {
405 let config = RecursionLimits::new(100, 100, 10_000).unwrap();
406 assert_eq!(config.effective_expr_fuel().unwrap(), 10_000);
407 }
408
409 #[test]
410 fn test_file_ops_depth_above_hard_cap_fails() {
411 let result = RecursionLimits::new(201, 100, 1000);
412 assert!(result.is_err());
413 assert!(
414 result
415 .unwrap_err()
416 .to_string()
417 .contains("exceeds absolute hard cap")
418 );
419 }
420
421 #[test]
422 fn test_expr_depth_above_hard_cap_fails() {
423 let result = RecursionLimits::new(100, 201, 1000);
424 assert!(result.is_err());
425 assert!(
426 result
427 .unwrap_err()
428 .to_string()
429 .contains("exceeds absolute hard cap")
430 );
431 }
432
433 #[test]
434 fn test_expr_fuel_above_hard_cap_fails() {
435 let result = RecursionLimits::new(100, 100, 10_001);
436 assert!(result.is_err());
437 assert!(
438 result
439 .unwrap_err()
440 .to_string()
441 .contains("exceeds absolute hard cap")
442 );
443 }
444
445 #[test]
446 fn test_parse_env_var_valid() {
447 let result = RecursionLimits::parse_env_var("150", "TEST_VAR");
448 assert_eq!(result.unwrap(), 150);
449 }
450
451 #[test]
452 fn test_parse_env_var_invalid() {
453 let result = RecursionLimits::parse_env_var("abc", "TEST_VAR");
454 assert!(result.is_err());
455 assert!(
456 result
457 .unwrap_err()
458 .to_string()
459 .contains("Invalid value for TEST_VAR")
460 );
461 }
462
463 #[test]
464 fn test_parse_env_var_negative() {
465 let result = RecursionLimits::parse_env_var("-100", "TEST_VAR");
466 assert!(result.is_err());
467 assert!(result.unwrap_err().to_string().contains("Invalid value"));
468 }
469}