use crate::error::{AlgorithmError, Result};
pub const MAX_EXPRESSION_DEPTH: usize = 64;
#[inline]
pub(crate) fn guard_depth(depth: usize, context: &'static str) -> Result<()> {
if depth > MAX_EXPRESSION_DEPTH {
return Err(AlgorithmError::NestingTooDeep {
context,
depth,
max: MAX_EXPRESSION_DEPTH,
});
}
Ok(())
}
#[cfg(feature = "dsl")]
pub(crate) fn source_nesting_depth(input: &str) -> usize {
let bytes = input.as_bytes();
let len = bytes.len();
let mut groups: alloc_vec::Vec<usize> = alloc_vec::vec![0];
let mut prefix_total: usize = 0;
let mut unary_run: usize = 0;
let mut max_depth: usize = 0;
let mut unary_position = true;
let mut index = 0usize;
while index < len {
let byte = bytes[index];
match byte {
b' ' | b'\t' | b'\r' | b'\n' => index += 1,
b'/' if index + 1 < len && bytes[index + 1] == b'/' => {
index += 2;
while index < len && bytes[index] != b'\n' {
index += 1;
}
}
b'/' if index + 1 < len && bytes[index + 1] == b'*' => {
index += 2;
while index + 1 < len && !(bytes[index] == b'*' && bytes[index + 1] == b'/') {
index += 1;
}
index = index.saturating_add(2).min(len);
}
b'(' | b'{' => {
groups.push(0);
unary_run = 0;
unary_position = true;
index += 1;
max_depth = max_depth.max(groups.len() - 1 + prefix_total);
}
b')' | b'}' => {
if groups.len() > 1 {
let closed = groups.pop().unwrap_or(0);
prefix_total = prefix_total.saturating_sub(closed);
}
unary_run = 0;
unary_position = false;
index += 1;
}
b';' => {
if let Some(top) = groups.last_mut() {
prefix_total = prefix_total.saturating_sub(*top);
*top = 0;
}
unary_run = 0;
unary_position = true;
index += 1;
}
b'+' | b'-' if unary_position => {
unary_run += 1;
index += 1;
max_depth = max_depth.max(groups.len() - 1 + prefix_total + unary_run);
}
b'A'..=b'Z' | b'a'..=b'z' | b'_' => {
let start = index;
while index < len && (bytes[index].is_ascii_alphanumeric() || bytes[index] == b'_')
{
index += 1;
}
let word = input.get(start..index).unwrap_or("");
unary_run = 0;
if word.eq_ignore_ascii_case("if") || word.eq_ignore_ascii_case("for") {
if let Some(top) = groups.last_mut() {
*top += 1;
}
prefix_total += 1;
unary_position = true;
max_depth = max_depth.max(groups.len() - 1 + prefix_total);
} else {
unary_position = matches!(
word.to_ascii_lowercase().as_str(),
"then" | "else" | "in" | "let" | "return" | "and" | "or" | "not"
);
}
}
b'0'..=b'9' => {
while index < len && bytes[index].is_ascii_digit() {
index += 1;
}
if index + 1 < len && bytes[index] == b'.' && bytes[index + 1].is_ascii_digit() {
index += 1;
while index < len && bytes[index].is_ascii_digit() {
index += 1;
}
}
if index < len && (bytes[index] | 0x20) == b'e' {
let mut lookahead = index + 1;
if lookahead < len && (bytes[lookahead] == b'+' || bytes[lookahead] == b'-') {
lookahead += 1;
}
if lookahead < len && bytes[lookahead].is_ascii_digit() {
index = lookahead;
while index < len && bytes[index].is_ascii_digit() {
index += 1;
}
}
}
unary_run = 0;
unary_position = false;
}
_ => {
unary_run = 0;
unary_position = true;
index += 1;
}
}
if max_depth > MAX_EXPRESSION_DEPTH {
return max_depth;
}
}
max_depth
}
#[cfg(feature = "dsl")]
pub(crate) fn check_source_nesting_depth(input: &str, context: &'static str) -> Result<()> {
let depth = source_nesting_depth(input);
if depth > MAX_EXPRESSION_DEPTH {
return Err(AlgorithmError::NestingTooDeep {
context,
depth,
max: MAX_EXPRESSION_DEPTH,
});
}
Ok(())
}
#[cfg(feature = "dsl")]
mod alloc_vec {
#[cfg(feature = "std")]
pub(super) use std::{vec, vec::Vec};
#[cfg(not(feature = "std"))]
pub(super) use alloc::{vec, vec::Vec};
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn guard_depth_accepts_up_to_the_limit() {
assert!(guard_depth(0, "test").is_ok());
assert!(guard_depth(MAX_EXPRESSION_DEPTH, "test").is_ok());
}
#[test]
fn guard_depth_rejects_beyond_the_limit() {
let err = guard_depth(MAX_EXPRESSION_DEPTH + 1, "test")
.expect_err("depth past the limit must be rejected");
assert!(matches!(
err,
AlgorithmError::NestingTooDeep {
context: "test",
max: MAX_EXPRESSION_DEPTH,
..
}
));
assert!(err.to_string().contains(&MAX_EXPRESSION_DEPTH.to_string()));
}
#[cfg(feature = "dsl")]
#[test]
fn scanner_counts_bracket_nesting() {
assert_eq!(source_nesting_depth("x"), 0);
assert_eq!(source_nesting_depth("(x)"), 1);
assert_eq!(source_nesting_depth("((x))"), 2);
assert_eq!(source_nesting_depth("(B1 - B2) / (B1 + B2)"), 1);
assert_eq!(source_nesting_depth("sqrt(abs(B1))"), 2);
assert_eq!(source_nesting_depth("(a) + (b) + (c)"), 1);
}
#[cfg(feature = "dsl")]
#[test]
fn scanner_counts_if_chains_without_brackets() {
assert_eq!(source_nesting_depth("if a then 1 else 0"), 1);
assert_eq!(
source_nesting_depth("if a then 1 else if b then 1 else 0"),
2
);
assert_eq!(
source_nesting_depth("if a then 1 else 0; if b then 1 else 0;"),
1
);
}
#[cfg(feature = "dsl")]
#[test]
fn scanner_counts_unary_runs_but_not_binary_operators() {
assert_eq!(source_nesting_depth("---x"), 3);
assert_eq!(source_nesting_depth("2 * -3"), 1);
let flat = (0..200)
.map(|i| i.to_string())
.collect::<std::vec::Vec<_>>()
.join(" - ");
assert_eq!(source_nesting_depth(&flat), 0);
}
#[cfg(feature = "dsl")]
#[test]
fn scanner_ignores_comments_and_exponents() {
assert_eq!(source_nesting_depth("x // ((((((\n"), 0);
assert_eq!(source_nesting_depth("x /* (((((( */ + 1"), 0);
assert_eq!(source_nesting_depth("1e-3 + 2.5e+4"), 0);
}
#[cfg(feature = "dsl")]
#[test]
fn scanner_never_underestimates_deep_input() {
let mut expr = std::string::String::from("x");
for _ in 0..1000 {
expr = std::format!("({expr} + 1)");
}
assert!(source_nesting_depth(&expr) > MAX_EXPRESSION_DEPTH);
assert!(check_source_nesting_depth(&expr, "dsl").is_err());
}
#[cfg(feature = "dsl")]
#[test]
fn check_accepts_exactly_the_limit() {
let mut expr = std::string::String::from("x");
for _ in 0..MAX_EXPRESSION_DEPTH {
expr = std::format!("({expr})");
}
assert_eq!(source_nesting_depth(&expr), MAX_EXPRESSION_DEPTH);
assert!(check_source_nesting_depth(&expr, "dsl").is_ok());
}
}