#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod arrays;
mod ast;
mod character_groups;
mod character_reader;
mod checker_reader;
mod code_point;
mod collect_results;
mod downgrade_pattern;
mod infinite_loop_tracker;
mod is_unbounded_reader;
mod map;
mod node_extra;
mod our_range;
mod parse;
mod quantifier;
mod reader;
mod result_cache;
mod sets;
mod to_friendly;
mod tree;
use crate::ast::{NodeKind, RcNode};
use crate::character_reader::level0::StackEntry;
use crate::checker_reader::{Clock, Trail as CheckerTrail};
use crate::collect_results::{collect_results, CollectInput};
use crate::downgrade_pattern::is_missing_start_anchor;
use std::collections::HashSet;
use std::time::Duration;
pub use crate::downgrade_pattern::{downgrade_pattern, DowngradedRegexPattern};
pub use crate::to_friendly::{to_friendly, ToFriendlyConfig};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DEFAULT_MAX_SCORE: u64 = 200;
pub const DEFAULT_MAX_STEPS: u64 = 20000;
pub const DEFAULT_MULTI_LINE: bool = false;
pub const DEFAULT_UNICODE: bool = false;
pub const DEFAULT_CASE_INSENSITIVE: bool = false;
pub const DEFAULT_DOT_ALL: bool = false;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NodeLocation {
pub offset: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Node {
pub end: NodeLocation,
pub source: String,
pub start: NodeLocation,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BackReference {
pub index: u64,
pub node: Node,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QuantifierIteration {
pub iteration: u64,
pub node: Node,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrailEntrySide {
pub backreference_stack: Vec<BackReference>,
pub node: Node,
pub quantifier_iterations: Vec<QuantifierIteration>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrailEntry {
pub a: TrailEntrySide,
pub b: TrailEntrySide,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Trail {
pub entries: Vec<TrailEntry>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AnalysisLimit {
HitMaxScore,
HitMaxSteps,
TimedOut,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CheckError {
HitMaxSteps,
TimedOut,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Score {
Finite(u64),
Infinite,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Report {
pub error: Option<AnalysisLimit>,
pub pattern: String,
pub pattern_downgraded: bool,
pub score: Score,
pub trails: Vec<Trail>,
}
impl Report {
pub fn is_safe(&self) -> bool {
self.error.is_none()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
UnsupportedFlag(char),
CaseInsensitiveWithUnicode,
AtomicGroupOffsetsWithDowngrade,
InvalidTimeout,
InvalidMaxSteps,
Parse(String),
MissingStartAnchor,
UnsupportedReference,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::UnsupportedFlag(flag) => write!(f, "Unsupported flag: {}", flag),
Error::CaseInsensitiveWithUnicode => {
write!(f, "`caseInsensitive` cannot be used with `unicode`.")
}
Error::AtomicGroupOffsetsWithDowngrade => write!(
f,
"`atomicGroupOffsets` cannot be used with `downgradePattern: true`."
),
Error::InvalidTimeout => write!(f, "`timeout` must be a positive number."),
Error::InvalidMaxSteps => write!(f, "`maxSteps` must be a positive number."),
Error::Parse(message) => write!(f, "{}", message),
Error::MissingStartAnchor => write!(
f,
"Pattern is not bounded at the start and needs downgrading. See the `downgradePattern` option."
),
Error::UnsupportedReference => write!(
f,
"Pattern has an unsupported reference and needs downgrading. See the `downgradePattern` option."
),
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Debug)]
pub struct Config {
pub max_score: Option<u64>,
pub max_steps: Option<u64>,
pub timeout: Option<Duration>,
pub case_insensitive: bool,
pub dot_all: bool,
pub multi_line: bool,
pub unicode: bool,
pub downgrade_pattern: bool,
pub atomic_group_offsets: Option<HashSet<usize>>,
}
impl Default for Config {
fn default() -> Self {
Config {
max_score: Some(DEFAULT_MAX_SCORE),
max_steps: Some(DEFAULT_MAX_STEPS),
timeout: None,
case_insensitive: DEFAULT_CASE_INSENSITIVE,
dot_all: DEFAULT_DOT_ALL,
multi_line: DEFAULT_MULTI_LINE,
unicode: DEFAULT_UNICODE,
downgrade_pattern: true,
atomic_group_offsets: None,
}
}
}
struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> f64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64() * 1000.0)
.unwrap_or(0.0)
}
}
fn parse_flags(flags: &str) -> Result<(bool, bool, bool, bool), Error> {
let mut unicode = false;
let mut case_insensitive = false;
let mut dot_all = false;
let mut multi_line = false;
for flag in flags.chars() {
match flag {
'u' => unicode = true,
'i' => case_insensitive = true,
's' => dot_all = true,
'm' => multi_line = true,
'g' | 'y' | 'd' => {}
other => return Err(Error::UnsupportedFlag(other)),
}
}
Ok((unicode, case_insensitive, dot_all, multi_line))
}
pub fn is_safe(source: &str, flags: &str, config: &Config) -> Result<Report, Error> {
let (unicode, case_insensitive, dot_all, multi_line) = parse_flags(flags)?;
let merged = Config {
case_insensitive,
dot_all,
multi_line,
unicode,
..config.clone()
};
is_safe_pattern(source, &merged)
}
pub fn is_safe_pattern(input_pattern: &str, config: &Config) -> Result<Report, Error> {
is_safe_pattern_with_clock(input_pattern, config, SystemClock)
}
fn cap_to_f64(cap: Option<u64>) -> f64 {
cap.map_or(f64::INFINITY, |value| value as f64)
}
fn timeout_to_f64(timeout: Option<Duration>) -> f64 {
timeout.map_or(f64::INFINITY, |duration| duration.as_secs_f64() * 1000.0)
}
fn is_safe_pattern_with_clock<C: Clock>(
input_pattern: &str,
config: &Config,
clock: C,
) -> Result<Report, Error> {
if config.case_insensitive && config.unicode {
return Err(Error::CaseInsensitiveWithUnicode);
}
if config.downgrade_pattern && config.atomic_group_offsets.is_some() {
return Err(Error::AtomicGroupOffsetsWithDowngrade);
}
if config.timeout == Some(Duration::ZERO) {
return Err(Error::InvalidTimeout);
}
if config.max_steps == Some(0) {
return Err(Error::InvalidMaxSteps);
}
let (pattern, atomic_group_offsets) = if config.downgrade_pattern {
let downgraded = downgrade_pattern(input_pattern, config.unicode)?;
(downgraded.pattern, downgraded.atomic_group_offsets)
} else {
(
input_pattern.to_string(),
config.atomic_group_offsets.clone().unwrap_or_default(),
)
};
let pattern_downgraded = config.downgrade_pattern && input_pattern != pattern;
let ast = parse::parse(&pattern, config.unicode).map_err(|e| Error::Parse(e.0))?;
if !config.downgrade_pattern {
if is_missing_start_anchor(&ast) {
return Err(Error::MissingStartAnchor);
}
if downgrade_pattern(input_pattern, config.unicode)?.pattern != input_pattern {
return Err(Error::UnsupportedReference);
}
}
let result = collect_results(
CollectInput {
atomic_group_offsets,
case_insensitive: config.case_insensitive,
dot_all: config.dot_all,
max_score: cap_to_f64(config.max_score),
max_steps: cap_to_f64(config.max_steps),
multi_line: config.multi_line,
node: ast,
timeout: timeout_to_f64(config.timeout),
},
clock,
);
Ok(build_result(result, pattern, pattern_downgraded))
}
fn build_result(
result: collect_results::CollectResults,
pattern: String,
pattern_downgraded: bool,
) -> Report {
let trails = result.trails.iter().map(map_trail).collect::<Vec<Trail>>();
Report {
error: result.error,
pattern,
pattern_downgraded,
score: result.score,
trails,
}
}
fn map_trail(trail: &CheckerTrail) -> Trail {
Trail {
entries: trail
.iter()
.map(|entry| TrailEntry {
a: TrailEntrySide {
backreference_stack: to_backreference_stack(&entry.right.stack),
node: to_node(&entry.right.node),
quantifier_iterations: to_quantifier_iterations(&entry.right.stack),
},
b: TrailEntrySide {
backreference_stack: to_backreference_stack(&entry.left.stack),
node: to_node(&entry.left.node),
quantifier_iterations: to_quantifier_iterations(&entry.left.stack),
},
})
.collect(),
}
}
fn to_node(node: &RcNode) -> Node {
Node {
end: NodeLocation {
offset: node.range.1,
},
source: node.raw.clone(),
start: NodeLocation {
offset: node.range.0,
},
}
}
fn to_backreference_stack(stack: &[StackEntry]) -> Vec<BackReference> {
let mut references: Vec<RcNode> = stack
.iter()
.filter_map(|entry| match entry {
StackEntry::Reference { reference } => Some(reference.clone()),
_ => None,
})
.collect();
references.reverse();
references
.iter()
.map(|reference| {
let index = match &reference.kind {
NodeKind::Reference { match_index } => *match_index,
_ => 0,
};
BackReference {
index,
node: to_node(reference),
}
})
.collect()
}
fn to_quantifier_iterations(stack: &[StackEntry]) -> Vec<QuantifierIteration> {
let reversed: Vec<&StackEntry> = stack.iter().rev().collect();
let reference_index = reversed
.iter()
.position(|entry| matches!(entry, StackEntry::Reference { .. }));
let portion_len = reference_index.unwrap_or(stack.len());
let mut portion: Vec<&StackEntry> = reversed[..portion_len].to_vec();
portion.reverse();
portion
.iter()
.filter_map(|entry| match entry {
StackEntry::Quantifier {
iteration,
quantifier,
} => Some(QuantifierIteration {
iteration: *iteration,
node: to_node(quantifier),
}),
_ => None,
})
.collect()
}
#[cfg(test)]
mod api_tests {
use super::*;
use std::cell::Cell;
fn inf_steps() -> Config {
Config {
max_score: None,
max_steps: Some(5000),
..Config::default()
}
}
struct FakeClock {
time: Cell<f64>,
}
impl Clock for FakeClock {
fn now(&self) -> f64 {
let next = self.time.get() + 1.0;
self.time.set(next);
next
}
}
#[test]
fn version_is_crate_version() {
assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
}
#[test]
fn respects_max_score() {
let config = Config {
max_score: Some(2),
..Config::default()
};
assert_eq!(is_safe("^a?a?$", "", &config).unwrap().error, None);
let config = Config {
max_score: Some(1),
..Config::default()
};
assert_eq!(
is_safe("^a?a?$", "", &config).unwrap().error,
Some(AnalysisLimit::HitMaxScore)
);
}
#[test]
fn respects_max_steps() {
let config = Config {
max_steps: Some(20),
..Config::default()
};
assert_eq!(
is_safe("a?a?a?", "", &config).unwrap().error,
Some(AnalysisLimit::HitMaxSteps)
);
}
#[test]
fn respects_timeout() {
let config = Config {
timeout: Some(Duration::from_millis(20)),
..Config::default()
};
let clock = FakeClock {
time: Cell::new(0.0),
};
let result = is_safe_pattern_with_clock("a?a?a?", &config, clock).unwrap();
assert_eq!(result.error, Some(AnalysisLimit::TimedOut));
}
#[test]
fn rejects_zero_timeout() {
let config = Config {
timeout: Some(Duration::ZERO),
..Config::default()
};
assert_eq!(is_safe("a", "", &config), Err(Error::InvalidTimeout));
}
#[test]
fn rejects_zero_max_steps() {
let config = Config {
max_steps: Some(0),
..Config::default()
};
assert_eq!(is_safe("a", "", &config), Err(Error::InvalidMaxSteps));
}
#[test]
fn rejects_unsupported_flag() {
assert_eq!(
is_safe("a", "z", &Config::default()),
Err(Error::UnsupportedFlag('z'))
);
}
#[test]
fn accepts_supported_flags() {
for flag in ["u", "g", "s", "y", "i", "d", "m"] {
assert!(is_safe("a", flag, &Config::default()).is_ok());
}
}
#[test]
fn rejects_case_insensitive_with_unicode() {
let config = Config {
case_insensitive: true,
unicode: true,
..Config::default()
};
assert_eq!(
is_safe_pattern("a", &config),
Err(Error::CaseInsensitiveWithUnicode)
);
}
#[test]
fn rejects_iterations_above_max_safe_integer() {
let err = is_safe_pattern("a{0,9007199254740992}", &Config::default()).unwrap_err();
assert!(matches!(err, Error::Parse(message)
if message.contains("iterations outside JS safe integer range")));
}
#[test]
fn rejects_atomic_offsets_with_downgrade() {
let config = Config {
downgrade_pattern: true,
atomic_group_offsets: Some(HashSet::new()),
..Config::default()
};
assert_eq!(
is_safe_pattern("", &config),
Err(Error::AtomicGroupOffsetsWithDowngrade)
);
}
#[test]
fn supports_atomic_group_offsets() {
let mut offsets = HashSet::new();
offsets.insert(1);
let config = Config {
downgrade_pattern: false,
atomic_group_offsets: Some(offsets),
..Config::default()
};
assert!(is_safe_pattern("^(a?)a?$", &config)
.unwrap()
.trails
.is_empty());
}
#[test]
fn no_options_pattern() {
assert_eq!(
is_safe_pattern("a", &Config::default()).unwrap().error,
None
);
}
#[test]
fn flags_override_config() {
assert!(is_safe("a", "i", &inf_steps()).is_ok());
}
#[test]
fn missing_start_anchor_without_downgrade_errors() {
let config = Config {
downgrade_pattern: false,
..Config::default()
};
assert_eq!(
is_safe_pattern("a", &config),
Err(Error::MissingStartAnchor)
);
}
}