use regex::RegexBuilder;
pub const MAX_PATTERN_LEN: usize = 1024;
pub const SIZE_LIMIT: usize = 1 << 20;
pub const DFA_SIZE_LIMIT: usize = 8 << 20;
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum RegexCompileError {
#[error("regex pattern is {len} bytes; limit is {} bytes", MAX_PATTERN_LEN)]
#[diagnostic(code(rtb_app::regex::too_long))]
TooLong {
len: usize,
},
#[error("regex failed to compile within memory bounds")]
#[diagnostic(
code(rtb_app::regex::compile),
help("simplify the pattern or reduce repetition counts")
)]
Compile(#[source] regex::Error),
}
pub fn compile_bounded(pattern: &str) -> Result<regex::Regex, RegexCompileError> {
if pattern.len() > MAX_PATTERN_LEN {
return Err(RegexCompileError::TooLong { len: pattern.len() });
}
RegexBuilder::new(pattern)
.size_limit(SIZE_LIMIT)
.dfa_size_limit(DFA_SIZE_LIMIT)
.build()
.map_err(RegexCompileError::Compile)
}
#[cfg(test)]
mod tests {
use super::{compile_bounded, RegexCompileError, DFA_SIZE_LIMIT, MAX_PATTERN_LEN, SIZE_LIMIT};
#[test]
fn compiles_a_valid_pattern() {
let re = compile_bounded(r"^\d{3}-\d{4}$").expect("valid pattern compiles");
assert!(re.is_match("123-4567"));
assert!(!re.is_match("nope"));
}
#[test]
fn rejects_overlong_pattern() {
let pattern = "a".repeat(MAX_PATTERN_LEN + 1);
let err = compile_bounded(&pattern).expect_err("over-long pattern is rejected");
assert!(matches!(err, RegexCompileError::TooLong { len } if len == MAX_PATTERN_LEN + 1));
}
#[test]
fn accepts_pattern_at_exact_limit() {
let pattern = "a".repeat(MAX_PATTERN_LEN);
let re = compile_bounded(&pattern).expect("at-limit pattern compiles");
assert!(re.is_match(&"a".repeat(MAX_PATTERN_LEN)));
}
#[test]
fn rejects_syntactically_invalid_pattern() {
let err = compile_bounded("(").expect_err("unbalanced group is rejected");
assert!(matches!(err, RegexCompileError::Compile(_)));
}
#[test]
fn enforces_size_limit_without_panicking() {
let err = compile_bounded(r"(?:a{10000}){10000}")
.expect_err("oversized compiled program is rejected");
assert!(matches!(err, RegexCompileError::Compile(_)));
}
#[test]
fn limit_constants_match_documented_values() {
assert_eq!(MAX_PATTERN_LEN, 1024);
assert_eq!(SIZE_LIMIT, 1 << 20);
assert_eq!(DFA_SIZE_LIMIT, 8 << 20);
}
}