Skip to content

Commit b4711de

Browse files
committed
automata: make the PikeVM optional in the meta engine
1 parent 2b52759 commit b4711de

4 files changed

Lines changed: 575 additions & 179 deletions

File tree

‎regex-automata/src/meta/error.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ impl RetryFailError {
205205
pub(crate) fn from_offset(offset: usize) -> RetryFailError {
206206
RetryFailError { offset }
207207
}
208+
209+
pub(crate) fn offset(&self) -> usize {
210+
self.offset
211+
}
208212
}
209213

210214
#[cfg(feature = "std")]

‎regex-automata/src/meta/regex.rs‎

Lines changed: 244 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ use crate::{
2424
pool::{Pool, PoolGuard},
2525
prefilter::Prefilter,
2626
primitives::{NonMaxUsize, PatternID},
27-
search::{HalfMatch, Input, Match, MatchKind, PatternSet, Span},
27+
search::{
28+
HalfMatch, Input, Match, MatchError, MatchKind, PatternSet, Span,
29+
},
2830
},
2931
};
3032

@@ -540,6 +542,63 @@ impl Regex {
540542
result
541543
}
542544

545+
/// Executes a leftmost is-match search, returning an error instead of
546+
/// falling back to the PikeVM.
547+
///
548+
/// This is like [`Regex::is_match`], but for a `Regex` built with
549+
/// [`Config::pikevm`] disabled. The meta regex engine's fast engines (the
550+
/// DFAs) can give up on a search, either because a lazy DFA's cache is
551+
/// being used ineffectively or because a Unicode word boundary was seen
552+
/// alongside a non-ASCII byte. The engines with bounded runtime (the
553+
/// one-pass DFA and the bounded backtracker) still pick those up, and with
554+
/// the PikeVM disabled the backtracker is given every search it can serve
555+
/// rather than only the short haystacks it is normally reserved for. Only
556+
/// once nothing is left to run does this routine report a [`MatchError`],
557+
/// which is to say only when the haystack outgrows the backtracker's
558+
/// visited bitset (see [`Config::backtrack_visited_capacity`]).
559+
///
560+
/// A `Regex` built with the PikeVM enabled (the default) never returns an
561+
/// error here.
562+
///
563+
/// This is a `topk-io/regex` fork addition.
564+
///
565+
/// # Example
566+
///
567+
/// ```
568+
/// use regex_automata::meta::Regex;
569+
///
570+
/// let re = Regex::builder()
571+
/// .configure(Regex::config().pikevm(false))
572+
/// .build(r"\b\w+\b")?;
573+
/// let mut cache = re.create_cache();
574+
///
575+
/// assert_eq!(Ok(true), re.try_is_match_with(&mut cache, "quux"));
576+
/// // A Unicode word boundary against a non-ASCII haystack makes the lazy
577+
/// // DFA quit, but the backtracker still answers it.
578+
/// let haystack = "☃".repeat(100);
579+
/// assert_eq!(Ok(false), re.try_is_match_with(&mut cache, &haystack));
580+
/// // Past the backtracker's bound there is no engine left to run.
581+
/// let haystack = "☃".repeat(10_000);
582+
/// assert!(re.try_is_match_with(&mut cache, &haystack).is_err());
583+
///
584+
/// # Ok::<(), Box<dyn std::error::Error>>(())
585+
/// ```
586+
#[inline]
587+
pub fn try_is_match_with<'h, I: Into<Input<'h>>>(
588+
&self,
589+
cache: &mut Cache,
590+
input: I,
591+
) -> Result<bool, MatchError> {
592+
let input = input.into().earliest(true);
593+
if self.imp.info.is_impossible(&input) {
594+
return Ok(false);
595+
}
596+
self.imp
597+
.strat
598+
.try_is_match(cache, &input)
599+
.map_err(|err| MatchError::gave_up(err.offset()))
600+
}
601+
543602
/// Executes a leftmost search and returns the first match that is found,
544603
/// if one exists.
545604
///
@@ -1257,6 +1316,32 @@ impl Regex {
12571316
self.imp.strat.search(cache, input)
12581317
}
12591318

1319+
/// This is like [`Regex::search_with`], but returns an error instead of
1320+
/// falling back to the PikeVM.
1321+
///
1322+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1323+
/// once the DFAs and the bounded engines have all given up on a search.
1324+
/// This routine reports that as a [`MatchError`], where the infallible
1325+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1326+
/// never returns an error here.
1327+
///
1328+
/// This is a `topk-io/regex` fork addition.
1329+
pub fn try_search_with(
1330+
&self,
1331+
cache: &mut Cache,
1332+
input: &Input<'_>,
1333+
) -> Result<Option<Match>, MatchError> {
1334+
if self.imp.info.captures_disabled()
1335+
|| self.imp.info.is_impossible(input)
1336+
{
1337+
return Ok(None);
1338+
}
1339+
self.imp
1340+
.strat
1341+
.try_search(cache, input)
1342+
.map_err(|err| MatchError::gave_up(err.offset()))
1343+
}
1344+
12601345
/// This is like [`Regex::search_half`], but requires the caller to
12611346
/// explicitly pass a [`Cache`].
12621347
///
@@ -1301,6 +1386,32 @@ impl Regex {
13011386
self.imp.strat.search_half(cache, input)
13021387
}
13031388

1389+
/// This is like [`Regex::search_half_with`], but returns an error instead of
1390+
/// falling back to the PikeVM.
1391+
///
1392+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1393+
/// once the DFAs and the bounded engines have all given up on a search.
1394+
/// This routine reports that as a [`MatchError`], where the infallible
1395+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1396+
/// never returns an error here.
1397+
///
1398+
/// This is a `topk-io/regex` fork addition.
1399+
pub fn try_search_half_with(
1400+
&self,
1401+
cache: &mut Cache,
1402+
input: &Input<'_>,
1403+
) -> Result<Option<HalfMatch>, MatchError> {
1404+
if self.imp.info.captures_disabled()
1405+
|| self.imp.info.is_impossible(input)
1406+
{
1407+
return Ok(None);
1408+
}
1409+
self.imp
1410+
.strat
1411+
.try_search_half(cache, input)
1412+
.map_err(|err| MatchError::gave_up(err.offset()))
1413+
}
1414+
13041415
/// This is like [`Regex::search_captures`], but requires the caller to
13051416
/// explicitly pass a [`Cache`].
13061417
///
@@ -1394,6 +1505,29 @@ impl Regex {
13941505
caps.set_pattern(pid);
13951506
}
13961507

1508+
/// This is like [`Regex::search_captures_with`], but returns an error instead of
1509+
/// falling back to the PikeVM.
1510+
///
1511+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1512+
/// once the DFAs and the bounded engines have all given up on a search.
1513+
/// This routine reports that as a [`MatchError`], where the infallible
1514+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1515+
/// never returns an error here.
1516+
///
1517+
/// This is a `topk-io/regex` fork addition.
1518+
pub fn try_search_captures_with(
1519+
&self,
1520+
cache: &mut Cache,
1521+
input: &Input<'_>,
1522+
caps: &mut Captures,
1523+
) -> Result<(), MatchError> {
1524+
caps.set_pattern(None);
1525+
let pid =
1526+
self.try_search_slots_with(cache, input, caps.slots_mut())?;
1527+
caps.set_pattern(pid);
1528+
Ok(())
1529+
}
1530+
13971531
/// This is like [`Regex::search_slots`], but requires the caller to
13981532
/// explicitly pass a [`Cache`].
13991533
///
@@ -1456,6 +1590,33 @@ impl Regex {
14561590
self.imp.strat.search_slots(cache, input, slots)
14571591
}
14581592

1593+
/// This is like [`Regex::search_slots_with`], but returns an error instead of
1594+
/// falling back to the PikeVM.
1595+
///
1596+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1597+
/// once the DFAs and the bounded engines have all given up on a search.
1598+
/// This routine reports that as a [`MatchError`], where the infallible
1599+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1600+
/// never returns an error here.
1601+
///
1602+
/// This is a `topk-io/regex` fork addition.
1603+
pub fn try_search_slots_with(
1604+
&self,
1605+
cache: &mut Cache,
1606+
input: &Input<'_>,
1607+
slots: &mut [Option<NonMaxUsize>],
1608+
) -> Result<Option<PatternID>, MatchError> {
1609+
if self.imp.info.captures_disabled()
1610+
|| self.imp.info.is_impossible(input)
1611+
{
1612+
return Ok(None);
1613+
}
1614+
self.imp
1615+
.strat
1616+
.try_search_slots(cache, input, slots)
1617+
.map_err(|err| MatchError::gave_up(err.offset()))
1618+
}
1619+
14591620
/// This is like [`Regex::which_overlapping_matches`], but requires the
14601621
/// caller to explicitly pass a [`Cache`].
14611622
///
@@ -1504,6 +1665,31 @@ impl Regex {
15041665
}
15051666
self.imp.strat.which_overlapping_matches(cache, input, patset)
15061667
}
1668+
1669+
/// This is like [`Regex::which_overlapping_matches_with`], but returns an error instead of
1670+
/// falling back to the PikeVM.
1671+
///
1672+
/// A `Regex` built with [`Config::pikevm`] disabled has no engine left
1673+
/// once the DFAs and the bounded engines have all given up on a search.
1674+
/// This routine reports that as a [`MatchError`], where the infallible
1675+
/// variants panic. A `Regex` built with the PikeVM enabled (the default)
1676+
/// never returns an error here.
1677+
///
1678+
/// This is a `topk-io/regex` fork addition.
1679+
pub fn try_which_overlapping_matches_with(
1680+
&self,
1681+
cache: &mut Cache,
1682+
input: &Input<'_>,
1683+
patset: &mut PatternSet,
1684+
) -> Result<(), MatchError> {
1685+
if self.imp.info.is_impossible(input) {
1686+
return Ok(());
1687+
}
1688+
self.imp
1689+
.strat
1690+
.try_which_overlapping_matches(cache, input, patset)
1691+
.map_err(|err| MatchError::gave_up(err.offset()))
1692+
}
15071693
}
15081694

15091695
/// Various non-search routines for querying properties of a `Regex` and
@@ -2481,6 +2667,8 @@ pub struct Config {
24812667
dfa_state_limit: Option<Option<usize>>,
24822668
onepass: Option<bool>,
24832669
backtrack: Option<bool>,
2670+
backtrack_visited_capacity: Option<usize>,
2671+
pikevm: Option<bool>,
24842672
byte_classes: Option<bool>,
24852673
line_terminator: Option<u8>,
24862674
}
@@ -3097,6 +3285,41 @@ impl Config {
30973285
Config { backtrack: Some(yes), ..self }
30983286
}
30993287

3288+
/// Sets the visited capacity, in bytes, used by the bounded backtracker.
3289+
///
3290+
/// The backtracker refuses to run when the haystack is longer than its
3291+
/// visited bitset can track, which is roughly this capacity divided by the
3292+
/// number of NFA states. Raising it lets the backtracker serve longer
3293+
/// haystacks, which matters when [`Config::pikevm`] is disabled and the
3294+
/// backtracker is the last engine standing.
3295+
///
3296+
/// When unset, the backtracker's own default is used.
3297+
///
3298+
/// This is a `topk-io/regex` fork addition.
3299+
pub fn backtrack_visited_capacity(self, capacity: usize) -> Config {
3300+
Config { backtrack_visited_capacity: Some(capacity), ..self }
3301+
}
3302+
3303+
/// Whether to permit the use of the PikeVM.
3304+
///
3305+
/// The PikeVM is the meta regex engine's engine of last resort: it can
3306+
/// handle any regex against any haystack, but its NFA simulation can be
3307+
/// orders of magnitude slower than the DFAs. Disabling it means a search
3308+
/// that the faster engines give up on has nothing left to fall back to.
3309+
/// Only [`Regex::try_is_match_with`] can report that condition, by
3310+
/// returning a [`MatchError`]; the other search routines require the
3311+
/// PikeVM and will panic if it is disabled.
3312+
///
3313+
/// This is useful for rejecting a pathological regex outright rather than
3314+
/// paying for it.
3315+
///
3316+
/// This is enabled by default.
3317+
///
3318+
/// This is a `topk-io/regex` fork addition.
3319+
pub fn pikevm(self, yes: bool) -> Config {
3320+
Config { pikevm: Some(yes), ..self }
3321+
}
3322+
31003323
/// Returns the match kind on this configuration, as set by
31013324
/// [`Config::match_kind`].
31023325
///
@@ -3270,6 +3493,22 @@ impl Config {
32703493
}
32713494
}
32723495

3496+
/// Returns the backtracker's visited capacity, as set by
3497+
/// [`Config::backtrack_visited_capacity`]. `None` means the backtracker's
3498+
/// own default is used.
3499+
///
3500+
/// This is a `topk-io/regex` fork addition.
3501+
pub fn get_backtrack_visited_capacity(&self) -> Option<usize> {
3502+
self.backtrack_visited_capacity
3503+
}
3504+
3505+
/// Returns whether the PikeVM may be used, as set by [`Config::pikevm`].
3506+
///
3507+
/// This is a `topk-io/regex` fork addition.
3508+
pub fn get_pikevm(&self) -> bool {
3509+
self.pikevm.unwrap_or(true)
3510+
}
3511+
32733512
/// Returns a "baseline" Thompson configuration for constructing NFAs based
32743513
/// on this configuration.
32753514
///
@@ -3315,6 +3554,10 @@ impl Config {
33153554
dfa_state_limit: o.dfa_state_limit.or(self.dfa_state_limit),
33163555
onepass: o.onepass.or(self.onepass),
33173556
backtrack: o.backtrack.or(self.backtrack),
3557+
backtrack_visited_capacity: o
3558+
.backtrack_visited_capacity
3559+
.or(self.backtrack_visited_capacity),
3560+
pikevm: o.pikevm.or(self.pikevm),
33183561
byte_classes: o.byte_classes.or(self.byte_classes),
33193562
line_terminator: o.line_terminator.or(self.line_terminator),
33203563
}

0 commit comments

Comments
 (0)