use core::num::NonZeroUsize;
use pest::error::Error;
use pest::iterators::Pairs;
use pest::{state, ParseResult, Parser, ParserState};
use std::sync::{Mutex, MutexGuard};
static CALL_LIMIT_LOCK: Mutex<()> = Mutex::new(());
struct CallLimitGuard {
_lock: MutexGuard<'static, ()>,
}
impl CallLimitGuard {
fn new(limit: Option<NonZeroUsize>) -> Self {
let lock = CALL_LIMIT_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
pest::set_call_limit(limit);
Self { _lock: lock }
}
}
impl Drop for CallLimitGuard {
fn drop(&mut self) {
pest::set_call_limit(None);
}
}
#[allow(dead_code, non_camel_case_types)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
enum Rule {
expression,
add_expr,
mul_expr,
primary,
number,
}
struct TestParser;
impl Parser<Rule> for TestParser {
fn parse(rule: Rule, input: &str) -> Result<Pairs<'_, Rule>, Error<Rule>> {
fn expression(
state: Box<ParserState<'_, Rule>>,
) -> ParseResult<Box<ParserState<'_, Rule>>> {
state.rule(Rule::expression, |s| {
s.sequence(|s| {
s.start_of_input()
.and_then(add_expr)
.and_then(|s| s.end_of_input())
})
})
}
fn add_expr(state: Box<ParserState<'_, Rule>>) -> ParseResult<Box<ParserState<'_, Rule>>> {
state.rule(Rule::add_expr, |s| {
s.sequence(|s| {
mul_expr(s).and_then(|s| {
s.repeat(|s| s.sequence(|s| s.match_string("+").and_then(mul_expr)))
})
})
})
}
fn mul_expr(state: Box<ParserState<'_, Rule>>) -> ParseResult<Box<ParserState<'_, Rule>>> {
state.rule(Rule::mul_expr, |s| {
s.sequence(|s| {
primary(s).and_then(|s| {
s.repeat(|s| s.sequence(|s| s.match_string("*").and_then(primary)))
})
})
})
}
fn primary(state: Box<ParserState<'_, Rule>>) -> ParseResult<Box<ParserState<'_, Rule>>> {
state.rule(Rule::primary, |s| {
number(s).or_else(|s| {
s.sequence(|s| {
s.match_string("(")
.and_then(add_expr)
.and_then(|s| s.match_string(")"))
})
})
})
}
fn number(state: Box<ParserState<'_, Rule>>) -> ParseResult<Box<ParserState<'_, Rule>>> {
state.rule(Rule::number, |s| {
s.match_char_by(|c| c.is_ascii_digit())
.and_then(|s| s.repeat(|s| s.match_char_by(|c| c.is_ascii_digit())))
})
}
state(input, |s| match rule {
Rule::expression => expression(s),
Rule::add_expr => add_expr(s),
Rule::mul_expr => mul_expr(s),
Rule::primary => primary(s),
Rule::number => number(s),
})
}
}
#[test]
fn test_call_limit_simple() {
let _guard = CallLimitGuard::new(NonZeroUsize::new(50));
let result = TestParser::parse(Rule::expression, "1");
assert!(result.is_ok());
}
#[test]
fn test_call_limit_small_budget() {
let _guard = CallLimitGuard::new(NonZeroUsize::new(4));
let result = TestParser::parse(Rule::expression, "1");
match result {
Ok(_) => {
panic!("Expected call limit error with very low limit");
}
Err(e) => {
let _error_msg = format!("{e}");
if let pest::error::ErrorVariant::CustomError { message } = &e.variant {
assert_eq!(
message, "call limit reached",
"Expected call limit error, got: {message}"
);
} else {
panic!(
"Expected CustomError variant with call limit, got: {:?}",
e.variant
);
}
}
}
}
#[test]
fn test_call_limit_allows_simple_parse() {
let _guard = CallLimitGuard::new(NonZeroUsize::new(50));
let result = TestParser::parse(Rule::expression, "1+2");
assert!(result.is_ok());
}
#[test]
fn test_no_call_limit() {
let _guard = CallLimitGuard::new(None);
let nested = "((((((1))))))";
let result = TestParser::parse(Rule::expression, nested);
assert!(result.is_ok());
}
#[test]
fn test_call_limit_reset() {
let _guard = CallLimitGuard::new(NonZeroUsize::new(5));
pest::set_call_limit(None);
let result = TestParser::parse(Rule::expression, "((((((1))))))");
assert!(result.is_ok());
}
#[test]
fn test_prevents_stack_overflow_from_issue() {
let _guard = CallLimitGuard::new(NonZeroUsize::new(50));
let mut deeply_nested = String::new();
let nesting_depth = 30;
for _ in 0..nesting_depth {
deeply_nested.push('(');
}
deeply_nested.push('1');
for _ in 0..nesting_depth {
deeply_nested.push(')');
}
let result = TestParser::parse(Rule::expression, &deeply_nested);
assert!(result.is_err());
if let Err(e) = result {
let error_msg = format!("{e}");
assert!(
error_msg.contains("call limit reached"),
"Expected call limit error, got: {error_msg}"
);
}
}
#[test]
fn test_call_limit_counts_flat_work() {
let _guard = CallLimitGuard::new(NonZeroUsize::new(50));
let input = format!("{}1", "1+".repeat(100));
assert_eq!(
TestParser::parse(Rule::expression, &input)
.unwrap_err()
.variant,
pest::error::ErrorVariant::CustomError {
message: "call limit reached".into()
}
);
pest::set_call_limit(None);
assert!(TestParser::parse(Rule::expression, &input).is_ok());
}
#[test]
fn test_call_limit_is_captured_per_state() {
let _guard = CallLimitGuard::new(NonZeroUsize::new(1));
let before = ParserState::<Rule>::new("x");
pest::set_call_limit(None);
let after = ParserState::<Rule>::new("x");
assert!(before
.sequence(|state| state.sequence(|state| state.match_string("x")))
.is_err());
assert!(after
.sequence(|state| state.sequence(|state| state.match_string("x")))
.is_ok());
}