use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecursionLimits {
pub file_ops_depth: usize,
pub expr_depth: usize,
pub expr_fuel: usize,
}
impl Default for RecursionLimits {
fn default() -> Self {
Self {
file_ops_depth: 100,
expr_depth: 100,
expr_fuel: 1000,
}
}
}
impl RecursionLimits {
pub const MIN_FILE_OPS_DEPTH: usize = 50;
pub const MAX_FILE_OPS_DEPTH: usize = 150;
pub const ABSOLUTE_MAX_FILE_OPS_DEPTH: usize = 200;
pub const MIN_EXPR_DEPTH: usize = 10;
pub const MAX_EXPR_DEPTH: usize = 100;
pub const ABSOLUTE_MAX_EXPR_DEPTH: usize = 200;
pub const MIN_EXPR_FUEL: usize = 100;
pub const MAX_EXPR_FUEL: usize = 5_000;
pub const ABSOLUTE_MAX_EXPR_FUEL: usize = 10_000;
pub fn new(file_ops_depth: usize, expr_depth: usize, expr_fuel: usize) -> Result<Self> {
let config = Self {
file_ops_depth,
expr_depth,
expr_fuel,
};
config.validate()?;
Ok(config)
}
pub fn load_or_default() -> Result<Self> {
let mut config = Self::default();
if let Ok(file_ops_str) = env::var("SQRY_RECURSION_FILE_OPS_DEPTH") {
config.file_ops_depth =
Self::parse_env_var(&file_ops_str, "SQRY_RECURSION_FILE_OPS_DEPTH")?;
}
if let Ok(expr_depth_str) = env::var("SQRY_RECURSION_EXPR_DEPTH") {
config.expr_depth = Self::parse_env_var(&expr_depth_str, "SQRY_RECURSION_EXPR_DEPTH")?;
}
if let Ok(expr_fuel_str) = env::var("SQRY_RECURSION_EXPR_FUEL") {
config.expr_fuel = Self::parse_env_var(&expr_fuel_str, "SQRY_RECURSION_EXPR_FUEL")?;
}
config.validate()?;
Ok(config)
}
pub fn effective_file_ops_depth(&self) -> Result<usize> {
if self.file_ops_depth == 0 {
bail!("recursion.file_ops_depth cannot be 0 (unlimited not allowed for safety)");
}
if self.file_ops_depth < Self::MIN_FILE_OPS_DEPTH {
bail!(
"recursion.file_ops_depth {} is below minimum {}",
self.file_ops_depth,
Self::MIN_FILE_OPS_DEPTH
);
}
if self.file_ops_depth > Self::MAX_FILE_OPS_DEPTH {
tracing::warn!(
"recursion.file_ops_depth {} exceeds recommended maximum {}",
self.file_ops_depth,
Self::MAX_FILE_OPS_DEPTH
);
}
if self.file_ops_depth > Self::ABSOLUTE_MAX_FILE_OPS_DEPTH {
bail!(
"recursion.file_ops_depth {} exceeds absolute hard cap {}",
self.file_ops_depth,
Self::ABSOLUTE_MAX_FILE_OPS_DEPTH
);
}
Ok(self.file_ops_depth)
}
pub fn effective_expr_depth(&self) -> Result<usize> {
if self.expr_depth == 0 {
bail!("recursion.expr_depth cannot be 0 (unlimited not allowed for safety)");
}
if self.expr_depth < Self::MIN_EXPR_DEPTH {
bail!(
"recursion.expr_depth {} is below minimum {}",
self.expr_depth,
Self::MIN_EXPR_DEPTH
);
}
if self.expr_depth > Self::MAX_EXPR_DEPTH {
tracing::warn!(
"recursion.expr_depth {} exceeds recommended maximum {}",
self.expr_depth,
Self::MAX_EXPR_DEPTH
);
}
if self.expr_depth > Self::ABSOLUTE_MAX_EXPR_DEPTH {
bail!(
"recursion.expr_depth {} exceeds absolute hard cap {}",
self.expr_depth,
Self::ABSOLUTE_MAX_EXPR_DEPTH
);
}
Ok(self.expr_depth)
}
pub fn effective_expr_fuel(&self) -> Result<usize> {
if self.expr_fuel == 0 {
bail!("recursion.expr_fuel cannot be 0 (unlimited not allowed for safety)");
}
if self.expr_fuel < Self::MIN_EXPR_FUEL {
bail!(
"recursion.expr_fuel {} is below minimum {}",
self.expr_fuel,
Self::MIN_EXPR_FUEL
);
}
if self.expr_fuel > Self::MAX_EXPR_FUEL {
tracing::warn!(
"recursion.expr_fuel {} exceeds recommended maximum {}",
self.expr_fuel,
Self::MAX_EXPR_FUEL
);
}
if self.expr_fuel > Self::ABSOLUTE_MAX_EXPR_FUEL {
bail!(
"recursion.expr_fuel {} exceeds absolute hard cap {}",
self.expr_fuel,
Self::ABSOLUTE_MAX_EXPR_FUEL
);
}
Ok(self.expr_fuel)
}
fn validate(&self) -> Result<()> {
self.effective_file_ops_depth()?;
self.effective_expr_depth()?;
self.effective_expr_fuel()?;
Ok(())
}
fn parse_env_var(value: &str, var_name: &str) -> Result<usize> {
match value.parse::<usize>() {
Ok(parsed) => Ok(parsed),
Err(_) => bail!("Invalid value for {var_name}: '{value}'. Expected usize"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = RecursionLimits::default();
assert_eq!(config.file_ops_depth, 100);
assert_eq!(config.expr_depth, 100);
assert_eq!(config.expr_fuel, 1000);
assert!(config.effective_file_ops_depth().is_ok());
assert!(config.effective_expr_depth().is_ok());
assert!(config.effective_expr_fuel().is_ok());
}
#[test]
fn test_new_with_valid_values() {
let config = RecursionLimits::new(200, 50, 5000).unwrap();
assert_eq!(config.effective_file_ops_depth().unwrap(), 200);
assert_eq!(config.effective_expr_depth().unwrap(), 50);
assert_eq!(config.effective_expr_fuel().unwrap(), 5000);
}
#[test]
fn test_file_ops_depth_zero_fails() {
let result = RecursionLimits::new(0, 100, 1000);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be 0"));
}
#[test]
fn test_expr_depth_zero_fails() {
let result = RecursionLimits::new(100, 0, 1000);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be 0"));
}
#[test]
fn test_expr_fuel_zero_fails() {
let result = RecursionLimits::new(100, 100, 0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be 0"));
}
#[test]
fn test_file_ops_depth_below_minimum_fails() {
let result = RecursionLimits::new(25, 100, 1000);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("below minimum 50"));
}
#[test]
fn test_expr_depth_below_minimum_fails() {
let result = RecursionLimits::new(100, 5, 1000);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("below minimum 10"));
}
#[test]
fn test_expr_fuel_below_minimum_fails() {
let result = RecursionLimits::new(100, 100, 50);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("below minimum 100")
);
}
#[test]
fn test_file_ops_depth_at_minimum_succeeds() {
let config = RecursionLimits::new(50, 100, 1000).unwrap();
assert_eq!(config.effective_file_ops_depth().unwrap(), 50);
}
#[test]
fn test_expr_depth_at_minimum_succeeds() {
let config = RecursionLimits::new(100, 10, 1000).unwrap();
assert_eq!(config.effective_expr_depth().unwrap(), 10);
}
#[test]
fn test_expr_fuel_at_minimum_succeeds() {
let config = RecursionLimits::new(100, 100, 100).unwrap();
assert_eq!(config.effective_expr_fuel().unwrap(), 100);
}
#[test]
fn test_file_ops_depth_at_hard_cap_succeeds() {
let config = RecursionLimits::new(200, 100, 1000).unwrap();
assert_eq!(config.effective_file_ops_depth().unwrap(), 200);
}
#[test]
fn test_expr_depth_at_hard_cap_succeeds() {
let config = RecursionLimits::new(100, 200, 1000).unwrap();
assert_eq!(config.effective_expr_depth().unwrap(), 200);
}
#[test]
fn test_expr_fuel_at_hard_cap_succeeds() {
let config = RecursionLimits::new(100, 100, 10_000).unwrap();
assert_eq!(config.effective_expr_fuel().unwrap(), 10_000);
}
#[test]
fn test_file_ops_depth_above_hard_cap_fails() {
let result = RecursionLimits::new(201, 100, 1000);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("exceeds absolute hard cap")
);
}
#[test]
fn test_expr_depth_above_hard_cap_fails() {
let result = RecursionLimits::new(100, 201, 1000);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("exceeds absolute hard cap")
);
}
#[test]
fn test_expr_fuel_above_hard_cap_fails() {
let result = RecursionLimits::new(100, 100, 10_001);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("exceeds absolute hard cap")
);
}
#[test]
fn test_parse_env_var_valid() {
let result = RecursionLimits::parse_env_var("150", "TEST_VAR");
assert_eq!(result.unwrap(), 150);
}
#[test]
fn test_parse_env_var_invalid() {
let result = RecursionLimits::parse_env_var("abc", "TEST_VAR");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid value for TEST_VAR")
);
}
#[test]
fn test_parse_env_var_negative() {
let result = RecursionLimits::parse_env_var("-100", "TEST_VAR");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid value"));
}
}