1#![forbid(unsafe_code)]
28#![warn(missing_docs)]
29
30mod arrays;
31mod ast;
32mod character_groups;
33mod character_reader;
34mod checker_reader;
35mod code_point;
36mod collect_results;
37mod downgrade_pattern;
38mod infinite_loop_tracker;
39mod is_unbounded_reader;
40mod map;
41mod node_extra;
42mod our_range;
43mod parse;
44mod quantifier;
45mod reader;
46mod result_cache;
47mod to_friendly;
48mod tree;
49
50use crate::ast::{NodeKind, RcNode};
51use crate::character_reader::level0::StackEntry;
52use crate::checker_reader::{Clock, Trail as CheckerTrail};
53use crate::collect_results::{collect_results, CollectInput};
54use crate::downgrade_pattern::is_missing_start_anchor;
55use std::collections::HashSet;
56use std::time::Duration;
57
58pub use crate::downgrade_pattern::{downgrade_pattern, DowngradedRegexPattern};
59pub use crate::to_friendly::{to_friendly, ToFriendlyConfig};
60
61pub const VERSION: &str = env!("CARGO_PKG_VERSION");
63
64pub const DEFAULT_MAX_SCORE: u64 = 200;
66pub const DEFAULT_MAX_STEPS: u64 = 20000;
68pub const DEFAULT_MULTI_LINE: bool = false;
70pub const DEFAULT_UNICODE: bool = false;
72pub const DEFAULT_CASE_INSENSITIVE: bool = false;
74pub const DEFAULT_DOT_ALL: bool = false;
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub struct NodeLocation {
80 pub offset: usize,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct Node {
87 pub end: NodeLocation,
89 pub source: String,
91 pub start: NodeLocation,
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct BackReference {
98 pub index: u64,
100 pub node: Node,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq)]
106pub struct QuantifierIteration {
107 pub iteration: u64,
109 pub node: Node,
111}
112
113#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct TrailEntrySide {
116 pub backreference_stack: Vec<BackReference>,
118 pub node: Node,
120 pub quantifier_iterations: Vec<QuantifierIteration>,
122}
123
124#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct TrailEntry {
127 pub a: TrailEntrySide,
129 pub b: TrailEntrySide,
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
135pub struct Trail {
136 pub entries: Vec<TrailEntry>,
138}
139
140#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
144#[non_exhaustive]
145pub enum AnalysisLimit {
146 HitMaxScore,
151 HitMaxSteps,
153 TimedOut,
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159pub(crate) enum CheckError {
160 HitMaxSteps,
162 TimedOut,
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
171#[non_exhaustive]
172pub enum Score {
173 Finite(u64),
175 Infinite,
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
181pub struct Report {
182 pub error: Option<AnalysisLimit>,
184 pub pattern: String,
186 pub pattern_downgraded: bool,
188 pub score: Score,
190 pub trails: Vec<Trail>,
192}
193
194impl Report {
195 pub fn is_safe(&self) -> bool {
198 self.error.is_none()
199 }
200}
201
202#[derive(Clone, Debug, PartialEq, Eq)]
211#[non_exhaustive]
212pub enum Error {
213 UnsupportedFlag(char),
215 CaseInsensitiveWithUnicode,
217 AtomicGroupOffsetsWithDowngrade,
219 InvalidTimeout,
221 InvalidMaxSteps,
223 Parse(String),
225 MissingStartAnchor,
227 UnsupportedReference,
231}
232
233impl std::fmt::Display for Error {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 match self {
236 Error::UnsupportedFlag(flag) => write!(f, "Unsupported flag: {}", flag),
237 Error::CaseInsensitiveWithUnicode => {
238 write!(f, "`caseInsensitive` cannot be used with `unicode`.")
239 }
240 Error::AtomicGroupOffsetsWithDowngrade => write!(
241 f,
242 "`atomicGroupOffsets` cannot be used with `downgradePattern: true`."
243 ),
244 Error::InvalidTimeout => write!(f, "`timeout` must be a positive number."),
245 Error::InvalidMaxSteps => write!(f, "`maxSteps` must be a positive number."),
246 Error::Parse(message) => write!(f, "{}", message),
247 Error::MissingStartAnchor => write!(
248 f,
249 "Pattern is not bounded at the start and needs downgrading. See the `downgradePattern` option."
250 ),
251 Error::UnsupportedReference => write!(
252 f,
253 "Pattern has an unsupported reference and needs downgrading. See the `downgradePattern` option."
254 ),
255 }
256 }
257}
258
259impl std::error::Error for Error {}
260
261#[derive(Clone, Debug)]
263pub struct Config {
264 pub max_score: Option<u64>,
267 pub max_steps: Option<u64>,
269 pub timeout: Option<Duration>,
271 pub case_insensitive: bool,
273 pub dot_all: bool,
275 pub multi_line: bool,
277 pub unicode: bool,
279 pub downgrade_pattern: bool,
281 pub atomic_group_offsets: Option<HashSet<usize>>,
284}
285
286impl Default for Config {
287 fn default() -> Self {
288 Config {
289 max_score: Some(DEFAULT_MAX_SCORE),
290 max_steps: Some(DEFAULT_MAX_STEPS),
291 timeout: None,
292 case_insensitive: DEFAULT_CASE_INSENSITIVE,
293 dot_all: DEFAULT_DOT_ALL,
294 multi_line: DEFAULT_MULTI_LINE,
295 unicode: DEFAULT_UNICODE,
296 downgrade_pattern: true,
297 atomic_group_offsets: None,
298 }
299 }
300}
301
302struct SystemClock;
303
304impl Clock for SystemClock {
305 fn now(&self) -> f64 {
306 use std::time::{SystemTime, UNIX_EPOCH};
307 SystemTime::now()
308 .duration_since(UNIX_EPOCH)
309 .map(|d| d.as_secs_f64() * 1000.0)
310 .unwrap_or(0.0)
311 }
312}
313
314fn parse_flags(flags: &str) -> Result<(bool, bool, bool, bool), Error> {
319 let mut unicode = false;
320 let mut case_insensitive = false;
321 let mut dot_all = false;
322 let mut multi_line = false;
323 for flag in flags.chars() {
324 match flag {
325 'u' => unicode = true,
326 'i' => case_insensitive = true,
327 's' => dot_all = true,
328 'm' => multi_line = true,
329 'g' | 'y' | 'd' => {}
330 other => return Err(Error::UnsupportedFlag(other)),
331 }
332 }
333 Ok((unicode, case_insensitive, dot_all, multi_line))
334}
335
336pub fn is_safe(source: &str, flags: &str, config: &Config) -> Result<Report, Error> {
345 let (unicode, case_insensitive, dot_all, multi_line) = parse_flags(flags)?;
346
347 let merged = Config {
348 case_insensitive,
349 dot_all,
350 multi_line,
351 unicode,
352 ..config.clone()
353 };
354 is_safe_pattern(source, &merged)
355}
356
357pub fn is_safe_pattern(input_pattern: &str, config: &Config) -> Result<Report, Error> {
361 is_safe_pattern_with_clock(input_pattern, config, SystemClock)
362}
363
364fn cap_to_f64(cap: Option<u64>) -> f64 {
367 cap.map_or(f64::INFINITY, |value| value as f64)
368}
369
370fn timeout_to_f64(timeout: Option<Duration>) -> f64 {
373 timeout.map_or(f64::INFINITY, |duration| duration.as_secs_f64() * 1000.0)
374}
375
376fn is_safe_pattern_with_clock<C: Clock>(
377 input_pattern: &str,
378 config: &Config,
379 clock: C,
380) -> Result<Report, Error> {
381 if config.case_insensitive && config.unicode {
382 return Err(Error::CaseInsensitiveWithUnicode);
383 }
384 if config.downgrade_pattern && config.atomic_group_offsets.is_some() {
385 return Err(Error::AtomicGroupOffsetsWithDowngrade);
386 }
387 if config.timeout == Some(Duration::ZERO) {
388 return Err(Error::InvalidTimeout);
389 }
390 if config.max_steps == Some(0) {
391 return Err(Error::InvalidMaxSteps);
392 }
393
394 let (pattern, atomic_group_offsets) = if config.downgrade_pattern {
395 let downgraded = downgrade_pattern(input_pattern, config.unicode)?;
396 (downgraded.pattern, downgraded.atomic_group_offsets)
397 } else {
398 (
399 input_pattern.to_string(),
400 config.atomic_group_offsets.clone().unwrap_or_default(),
401 )
402 };
403
404 let pattern_downgraded = config.downgrade_pattern && input_pattern != pattern;
405
406 let ast = parse::parse(&pattern, config.unicode).map_err(|e| Error::Parse(e.0))?;
407
408 if !config.downgrade_pattern {
409 if is_missing_start_anchor(&ast) {
410 return Err(Error::MissingStartAnchor);
411 }
412 if downgrade_pattern(input_pattern, config.unicode)?.pattern != input_pattern {
417 return Err(Error::UnsupportedReference);
418 }
419 }
420
421 let result = collect_results(
422 CollectInput {
423 atomic_group_offsets,
424 case_insensitive: config.case_insensitive,
425 dot_all: config.dot_all,
426 max_score: cap_to_f64(config.max_score),
427 max_steps: cap_to_f64(config.max_steps),
428 multi_line: config.multi_line,
429 node: ast,
430 timeout: timeout_to_f64(config.timeout),
431 },
432 clock,
433 );
434
435 Ok(build_result(result, pattern, pattern_downgraded))
436}
437
438fn build_result(
439 result: collect_results::CollectResults,
440 pattern: String,
441 pattern_downgraded: bool,
442) -> Report {
443 let trails = result.trails.iter().map(map_trail).collect::<Vec<Trail>>();
444
445 Report {
446 error: result.error,
447 pattern,
448 pattern_downgraded,
449 score: result.score,
450 trails,
451 }
452}
453
454fn map_trail(trail: &CheckerTrail) -> Trail {
455 Trail {
456 entries: trail
457 .iter()
458 .map(|entry| TrailEntry {
459 a: TrailEntrySide {
460 backreference_stack: to_backreference_stack(&entry.right.stack),
461 node: to_node(&entry.right.node),
462 quantifier_iterations: to_quantifier_iterations(&entry.right.stack),
463 },
464 b: TrailEntrySide {
465 backreference_stack: to_backreference_stack(&entry.left.stack),
466 node: to_node(&entry.left.node),
467 quantifier_iterations: to_quantifier_iterations(&entry.left.stack),
468 },
469 })
470 .collect(),
471 }
472}
473
474fn to_node(node: &RcNode) -> Node {
475 Node {
476 end: NodeLocation {
477 offset: node.range.1,
478 },
479 source: node.raw.clone(),
480 start: NodeLocation {
481 offset: node.range.0,
482 },
483 }
484}
485
486fn to_backreference_stack(stack: &[StackEntry]) -> Vec<BackReference> {
487 let mut references: Vec<RcNode> = stack
488 .iter()
489 .filter_map(|entry| match entry {
490 StackEntry::Reference { reference } => Some(reference.clone()),
491 _ => None,
492 })
493 .collect();
494 references.reverse();
495 references
496 .iter()
497 .map(|reference| {
498 let index = match &reference.kind {
499 NodeKind::Reference { match_index } => *match_index,
500 _ => 0,
501 };
502 BackReference {
503 index,
504 node: to_node(reference),
505 }
506 })
507 .collect()
508}
509
510fn to_quantifier_iterations(stack: &[StackEntry]) -> Vec<QuantifierIteration> {
511 let reversed: Vec<&StackEntry> = stack.iter().rev().collect();
512 let reference_index = reversed
513 .iter()
514 .position(|entry| matches!(entry, StackEntry::Reference { .. }));
515 let portion_len = reference_index.unwrap_or(stack.len());
516 let mut portion: Vec<&StackEntry> = reversed[..portion_len].to_vec();
517 portion.reverse();
518
519 portion
520 .iter()
521 .filter_map(|entry| match entry {
522 StackEntry::Quantifier {
523 iteration,
524 quantifier,
525 } => Some(QuantifierIteration {
526 iteration: *iteration,
527 node: to_node(quantifier),
528 }),
529 _ => None,
530 })
531 .collect()
532}
533
534#[cfg(test)]
535mod api_tests {
536 use super::*;
537 use std::cell::Cell;
538
539 fn inf_steps() -> Config {
540 Config {
541 max_score: None,
542 max_steps: Some(5000),
543 ..Config::default()
544 }
545 }
546
547 struct FakeClock {
549 time: Cell<f64>,
550 }
551
552 impl Clock for FakeClock {
553 fn now(&self) -> f64 {
554 let next = self.time.get() + 1.0;
555 self.time.set(next);
556 next
557 }
558 }
559
560 #[test]
561 fn version_is_crate_version() {
562 assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
563 }
564
565 #[test]
566 fn respects_max_score() {
567 let config = Config {
568 max_score: Some(2),
569 ..Config::default()
570 };
571 assert_eq!(is_safe("^a?a?$", "", &config).unwrap().error, None);
572 let config = Config {
573 max_score: Some(1),
574 ..Config::default()
575 };
576 assert_eq!(
577 is_safe("^a?a?$", "", &config).unwrap().error,
578 Some(AnalysisLimit::HitMaxScore)
579 );
580 }
581
582 #[test]
583 fn respects_max_steps() {
584 let config = Config {
585 max_steps: Some(20),
586 ..Config::default()
587 };
588 assert_eq!(
589 is_safe("a?a?a?", "", &config).unwrap().error,
590 Some(AnalysisLimit::HitMaxSteps)
591 );
592 }
593
594 #[test]
595 fn respects_timeout() {
596 let config = Config {
597 timeout: Some(Duration::from_millis(20)),
598 ..Config::default()
599 };
600 let clock = FakeClock {
601 time: Cell::new(0.0),
602 };
603 let result = is_safe_pattern_with_clock("a?a?a?", &config, clock).unwrap();
604 assert_eq!(result.error, Some(AnalysisLimit::TimedOut));
605 }
606
607 #[test]
608 fn rejects_zero_timeout() {
609 let config = Config {
610 timeout: Some(Duration::ZERO),
611 ..Config::default()
612 };
613 assert_eq!(is_safe("a", "", &config), Err(Error::InvalidTimeout));
614 }
615
616 #[test]
617 fn rejects_zero_max_steps() {
618 let config = Config {
619 max_steps: Some(0),
620 ..Config::default()
621 };
622 assert_eq!(is_safe("a", "", &config), Err(Error::InvalidMaxSteps));
623 }
624
625 #[test]
626 fn rejects_unsupported_flag() {
627 assert_eq!(
628 is_safe("a", "z", &Config::default()),
629 Err(Error::UnsupportedFlag('z'))
630 );
631 }
632
633 #[test]
634 fn accepts_supported_flags() {
635 for flag in ["u", "g", "s", "y", "i", "d", "m"] {
636 assert!(is_safe("a", flag, &Config::default()).is_ok());
637 }
638 }
639
640 #[test]
641 fn rejects_case_insensitive_with_unicode() {
642 let config = Config {
643 case_insensitive: true,
644 unicode: true,
645 ..Config::default()
646 };
647 assert_eq!(
648 is_safe_pattern("a", &config),
649 Err(Error::CaseInsensitiveWithUnicode)
650 );
651 }
652
653 #[test]
654 fn rejects_iterations_above_max_safe_integer() {
655 let err = is_safe_pattern("a{0,9007199254740992}", &Config::default()).unwrap_err();
656 assert!(matches!(err, Error::Parse(message)
657 if message.contains("iterations outside JS safe integer range")));
658 }
659
660 #[test]
661 fn rejects_atomic_offsets_with_downgrade() {
662 let config = Config {
663 downgrade_pattern: true,
664 atomic_group_offsets: Some(HashSet::new()),
665 ..Config::default()
666 };
667 assert_eq!(
668 is_safe_pattern("", &config),
669 Err(Error::AtomicGroupOffsetsWithDowngrade)
670 );
671 }
672
673 #[test]
674 fn supports_atomic_group_offsets() {
675 let mut offsets = HashSet::new();
676 offsets.insert(1);
677 let config = Config {
678 downgrade_pattern: false,
679 atomic_group_offsets: Some(offsets),
680 ..Config::default()
681 };
682 assert!(is_safe_pattern("^(a?)a?$", &config)
683 .unwrap()
684 .trails
685 .is_empty());
686 }
687
688 #[test]
689 fn no_options_pattern() {
690 assert_eq!(
691 is_safe_pattern("a", &Config::default()).unwrap().error,
692 None
693 );
694 }
695
696 #[test]
697 fn flags_override_config() {
698 assert!(is_safe("a", "i", &inf_steps()).is_ok());
700 }
701
702 #[test]
703 fn missing_start_anchor_without_downgrade_errors() {
704 let config = Config {
705 downgrade_pattern: false,
706 ..Config::default()
707 };
708 assert_eq!(
709 is_safe_pattern("a", &config),
710 Err(Error::MissingStartAnchor)
711 );
712 }
713}