use smallvec::SmallVec;
#[cfg(feature = "precompiled")]
mod precompiled;
#[cfg(feature = "precompiled")]
pub use precompiled::{
PRECOMPILED_FORMAT_VERSION, PrecompiledBytes, PrecompiledError, PrecompiledPatterns,
};
#[cfg(feature = "precompiled-eager")]
mod prepared;
#[cfg(feature = "precompiled-eager")]
pub use prepared::{PRECOMPILED_EAGER_FORMAT_VERSION, PrecompiledEager, PrecompiledEagerStats};
#[cfg(feature = "precompiled-eager")]
mod prepared_records;
#[cfg(feature = "precompiled-eager")]
pub use prepared_records::{
PRECOMPILED_EAGER_INDEX_FORMAT_VERSION, PrecompiledEagerRecord, PrecompiledEagerRecordKind,
PrecompiledEagerRecords, PrecompiledEagerRecordsStats,
};
#[cfg(feature = "scanner-group-nfa-prototype")]
#[doc(hidden)]
pub mod group_prototype;
#[cfg(feature = "scanner-load-profile")]
#[doc(hidden)]
pub mod load_profile;
use crate::AccelerationMode;
use crate::api::Syntax;
use crate::encodings::utf8::ONIG_ENCODING_UTF8;
static SCANNER_ENCODING: crate::regenc::OnigEncoding = &ONIG_ENCODING_UTF8;
use crate::error::RegexError;
use crate::oniguruma::*;
use crate::regcomp::onig_new_for_scanner;
use crate::regexec::{onig_get_global_limit_revision, onig_get_retry_limit_in_search};
use crate::regint::{ANCR_ANYCHAR_INF, RegexType};
use crate::regset::{
FallbackMemoIdentity, MAX_REGSET_ENTRIES, OnigRegSet, OnigRegSetLead, RegSetEntryEvent,
SharedAutomata, SharedSeek, onig_regset_entry_search, onig_regset_get_regex,
onig_regset_last_match_len, onig_regset_new_shared, onig_regset_number_of_regex,
onig_regset_prefilter_decides, onig_regset_search_utf8, onig_regset_swap_region,
};
use std::collections::HashMap;
use std::fmt;
use std::ops::{BitOr, BitOrAssign};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_ONIG_STRING_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct CaptureIndex {
pub start: usize,
pub end: usize,
pub length: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScannerMatch {
pub index: usize,
capture_indices: SmallVec<[CaptureIndex; 8]>,
}
impl ScannerMatch {
pub fn captures(&self) -> &[CaptureIndex] {
&self.capture_indices
}
pub fn into_captures(self) -> Vec<CaptureIndex> {
self.capture_indices.into_vec()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScannerFindOptions(u32);
impl ScannerFindOptions {
pub const NONE: Self = Self(0);
pub const NOT_BEGIN_STRING: Self = Self(1);
pub const NOT_END_STRING: Self = Self(2);
pub const NOT_BEGIN_POSITION: Self = Self(4);
pub fn from_bits(bits: u32) -> Self {
Self(bits)
}
fn to_onig_options(self) -> OnigOptionType {
let mut opts = ONIG_OPTION_NONE;
if self.0 & 1 != 0 {
opts |= ONIG_OPTION_NOT_BEGIN_STRING;
}
if self.0 & 2 != 0 {
opts |= ONIG_OPTION_NOT_END_STRING;
}
if self.0 & 4 != 0 {
opts |= ONIG_OPTION_NOT_BEGIN_POSITION;
}
opts
}
}
impl BitOr for ScannerFindOptions {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for ScannerFindOptions {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
pub type ScannerSyntax = Syntax;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ScannerConfig {
pub options: OnigOptionType,
pub syntax: ScannerSyntax,
pub prefilter: bool,
pub prefilter_warmup: u32,
pub acceleration: AccelerationMode,
}
const PREFILTER_WARMUP: u32 = 32;
impl Default for ScannerConfig {
fn default() -> Self {
ScannerConfig {
options: ONIG_OPTION_CAPTURE_GROUP,
syntax: ScannerSyntax::default(),
prefilter: true,
prefilter_warmup: PREFILTER_WARMUP,
acceleration: AccelerationMode::Auto,
}
}
}
impl ScannerConfig {
pub const fn options(mut self, options: OnigOptionType) -> Self {
self.options = options;
self
}
pub const fn syntax(mut self, syntax: ScannerSyntax) -> Self {
self.syntax = syntax;
self
}
pub const fn prefilter(mut self, prefilter: bool) -> Self {
self.prefilter = prefilter;
self.acceleration = if prefilter {
AccelerationMode::Auto
} else {
AccelerationMode::Off
};
self
}
pub const fn acceleration(mut self, mode: AccelerationMode) -> Self {
self.acceleration = mode;
self.prefilter = !matches!(mode, AccelerationMode::Off);
self
}
pub const fn prefilter_warmup(mut self, searches: u32) -> Self {
self.prefilter_warmup = searches;
self
}
}
#[derive(Default)]
pub struct ScannerPatternCache {
compiled: Vec<(PatternSettings, CompiledPatterns)>,
#[cfg(feature = "dfa-prefilter")]
automata: HashMap<Box<[u32]>, SharedAutomata>,
next_id: u32,
#[cfg(feature = "precompiled")]
precompiled: Vec<precompiled::ProgramSource>,
#[cfg(feature = "scanner-group-nfa-prototype")]
group_source: Option<group_prototype::GroupSourceRef>,
}
type CompiledPatterns = HashMap<Box<str>, CachedPattern>;
#[derive(Clone)]
struct CachedPattern {
reg: Arc<RegexType>,
seek: SharedSeek,
id: u32,
#[cfg(feature = "precompiled")]
environment: precompiled::Environment,
}
impl ScannerPatternCache {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.compiled
.iter()
.map(|(_, patterns)| patterns.len())
.sum()
}
pub fn is_empty(&self) -> bool {
self.compiled
.iter()
.all(|(_, patterns)| patterns.is_empty())
}
pub fn clear(&mut self) {
self.compiled.clear();
#[cfg(feature = "dfa-prefilter")]
self.automata.clear();
self.next_id = 0;
}
fn scanner(
&mut self,
patterns: &[&str],
settings: PatternSettings,
eager: bool,
) -> Result<Scanner, RegexError> {
#[cfg(feature = "scanner-load-profile")]
let _phase =
crate::scanner::load_profile::scope(crate::scanner::load_profile::Phase::PatternCache);
let at = match self.compiled.iter().position(|(s, _)| *s == settings) {
Some(at) => at,
None => {
self.compiled.push((settings, CompiledPatterns::new()));
self.compiled.len() - 1
}
};
let compiled = &mut self.compiled[at].1;
let mut added = Vec::new();
let mut parts = Vec::with_capacity(patterns.len());
let mut ids = Vec::with_capacity(patterns.len());
let mut failed = None;
for pattern in patterns {
let cached = match compiled.get(*pattern) {
Some(cached) => cached.clone(),
None => {
#[cfg(feature = "precompiled")]
let environment = precompiled::Environment::current();
#[cfg(feature = "precompiled")]
let result = self
.precompiled
.iter()
.find_map(|source| {
source.load(pattern, settings, &environment).ok().flatten()
})
.map(Ok)
.unwrap_or_else(|| settings.compile(pattern));
#[cfg(not(feature = "precompiled"))]
let result = settings.compile(pattern);
match result {
Ok((reg, seek)) => {
let cached = CachedPattern {
reg,
seek,
id: self.next_id,
#[cfg(feature = "precompiled")]
environment,
};
self.next_id += 1;
compiled.insert((*pattern).into(), cached.clone());
added.push(*pattern);
cached
}
Err(error) => {
failed = Some(error);
break;
}
}
}
};
ids.push(cached.id);
parts.push((cached.reg, cached.seek));
}
let scanner = match failed {
Some(error) => Err(error),
None => {
#[cfg(feature = "dfa-prefilter")]
let automata = self
.automata
.get(ids.as_slice())
.cloned()
.or_else(|| {
#[cfg(feature = "scanner-group-nfa-prototype")]
if eager {
return self
.group_source
.as_ref()
.and_then(|source| source.load(patterns, settings));
}
None
})
.unwrap_or_default();
#[cfg(feature = "dfa-prefilter")]
let scanner = Scanner::assemble(patterns, settings, parts, automata.clone(), eager);
#[cfg(not(feature = "dfa-prefilter"))]
let scanner = Scanner::assemble(patterns, settings, parts, (), eager);
#[cfg(feature = "dfa-prefilter")]
if scanner.is_ok() {
self.automata
.entry(ids.into_boxed_slice())
.or_insert(automata);
}
scanner
}
};
if scanner.is_err() {
for pattern in added {
compiled.remove(pattern);
}
}
if compiled.is_empty() {
self.compiled.remove(at);
}
scanner
}
}
impl fmt::Debug for ScannerPatternCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScannerPatternCache")
.field("len", &self.len())
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct PatternSettings {
options: OnigOptionType,
syntax: ScannerSyntax,
optimize_backtracking: bool,
prefilter: bool,
prefilter_warmup: u32,
}
impl PatternSettings {
fn of(config: &ScannerConfig, optimize_backtracking: bool) -> Self {
PatternSettings {
options: config.options,
syntax: config.syntax,
optimize_backtracking,
prefilter: config.prefilter && config.acceleration != AccelerationMode::Off,
prefilter_warmup: if config.acceleration == AccelerationMode::Eager {
0
} else {
config.prefilter_warmup
},
}
}
fn compile(self, pattern: &str) -> Result<(Arc<RegexType>, SharedSeek), RegexError> {
#[cfg(all(test, feature = "precompiled"))]
precompiled::COMPILES.with(|n| n.set(n.get() + 1));
let (reg, seek) = onig_new_for_scanner(
pattern.as_bytes(),
self.options,
SCANNER_ENCODING,
self.syntax.as_onig_syntax(),
self.optimize_backtracking,
self.prefilter,
)?;
#[cfg(feature = "dfa-prefilter")]
let seek = seek.map(Arc::new);
Ok((Arc::new(reg), seek))
}
}
pub struct OnigString {
cache_id: u64,
content: String,
is_ascii: bool,
utf16_len: usize,
utf16_to_utf8: Vec<u32>,
utf8_to_utf16: Vec<u32>,
}
impl OnigString {
pub fn new(content: &str) -> Self {
let cache_id = NEXT_ONIG_STRING_ID.fetch_add(1, Ordering::Relaxed);
if content.is_ascii() {
let len = content.len();
return OnigString {
cache_id,
content: content.to_string(),
is_ascii: true,
utf16_len: len,
utf16_to_utf8: Vec::new(),
utf8_to_utf16: Vec::new(),
};
}
assert!(
u32::try_from(content.len()).is_ok(),
"OnigString: non-ASCII text longer than u32::MAX bytes is not supported"
);
let utf8_len = content.len();
let utf16_len: usize = content.chars().map(|c| c.len_utf16()).sum();
let mut utf16_to_utf8: Vec<u32> = Vec::with_capacity(utf16_len + 1);
let mut utf8_to_utf16 = vec![0u32; utf8_len + 1];
let mut utf8_pos = 0;
for ch in content.chars() {
let u8_len = ch.len_utf8();
let u16_len = ch.len_utf16();
utf16_to_utf8.push(utf8_pos as u32);
let utf16_pos = (utf16_to_utf8.len() - 1) as u32;
for b in 0..u8_len {
utf8_to_utf16[utf8_pos + b] = utf16_pos;
}
if u16_len == 2 {
utf16_to_utf8.push((utf8_pos + u8_len) as u32);
}
utf8_pos += u8_len;
}
utf16_to_utf8.push(utf8_pos as u32);
utf8_to_utf16[utf8_pos] = utf16_len as u32;
OnigString {
cache_id,
content: content.to_string(),
is_ascii: false,
utf16_len,
utf16_to_utf8,
utf8_to_utf16,
}
}
pub fn content(&self) -> &str {
&self.content
}
#[inline]
fn is_ascii(&self) -> bool {
self.is_ascii
}
pub fn utf16_len(&self) -> usize {
self.utf16_len
}
fn utf16_offset_to_utf8(&self, utf16_offset: usize) -> usize {
if self.is_ascii {
utf16_offset.min(self.content.len())
} else if utf16_offset >= self.utf16_to_utf8.len() {
self.content.len()
} else {
self.utf16_to_utf8[utf16_offset] as usize
}
}
fn utf8_offset_to_utf16(&self, utf8_offset: usize) -> usize {
if self.is_ascii {
utf8_offset.min(self.content.len())
} else if utf8_offset >= self.utf8_to_utf16.len() {
self.utf16_len
} else {
self.utf8_to_utf16[utf8_offset] as usize
}
}
}
impl fmt::Debug for OnigString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OnigString")
.field("content", &self.content)
.finish_non_exhaustive()
}
}
struct CacheEntry {
has_g_anchor: bool,
after_newline_only: bool,
last_str_id: u64,
last_position: usize,
last_options: u32,
last_limit_revision: u64,
last_matched: bool,
last_result: i32,
last_region: Option<OnigRegion>,
}
impl CacheEntry {
fn new(pattern: &str, anchor: i32) -> Self {
CacheEntry {
has_g_anchor: pattern.contains("\\G"),
after_newline_only: (anchor & ANCR_ANYCHAR_INF) != 0,
last_str_id: 0,
last_position: 0,
last_options: u32::MAX, last_limit_revision: 0,
last_matched: false,
last_result: ONIG_MISMATCH,
last_region: None,
}
}
fn remember(
&mut self,
str_id: u64,
start: usize,
options: u32,
limit_revision: u64,
found: Option<usize>,
) {
self.last_str_id = str_id;
self.last_position = start;
self.last_options = options;
self.last_limit_revision = limit_revision;
self.last_matched = found.is_some();
self.last_result = found.map_or(ONIG_MISMATCH, |position| position as i32);
}
fn forget(&mut self) {
self.last_str_id = 0;
self.last_position = 0;
self.last_options = u32::MAX;
self.last_matched = false;
self.last_result = ONIG_MISMATCH;
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ScannerStats {
pub route_regset_calls: u64,
pub route_per_regex_calls: u64,
pub route_cache_regset_calls: u64,
pub route_cache_per_regex_calls: u64,
pub route_cache_probe_calls: u64,
pub route_switch_to_regset: u64,
pub route_switch_to_per_regex: u64,
pub cache_checks: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub vm_search_calls: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PrefilterStats {
pub built: bool,
pub covered: usize,
pub own: usize,
pub nfa_states: usize,
pub memory_usage: usize,
pub cache_memory_usage: usize,
pub cache_clears: usize,
pub retired: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum CacheRoute {
#[default]
RegSet,
PerRegex,
}
#[derive(Debug, Clone, Copy, Default)]
struct CacheRouteState {
str_id: u64,
options: u32,
route: CacheRoute,
calls_since_probe: u32,
last_start: usize,
same_start_streak: u16,
poor_per_regex_streak: u8,
good_per_regex_streak: u8,
}
#[derive(Debug, Clone, Copy, Default)]
struct PerRegexCallStats {
cache_checks: u32,
cache_hits: u32,
vm_calls: u32,
}
impl PerRegexCallStats {
#[inline]
fn cache_misses(self) -> u32 {
self.cache_checks.saturating_sub(self.cache_hits)
}
}
const ROUTE_PROBE_EVERY: u32 = 8;
const ROUTE_MIN_SAME_START_FOR_PROBE: u16 = 8;
const ROUTE_POOR_STREAK_TO_REGSET: u8 = 3;
const ROUTE_GOOD_STREAK_TO_PER_REGEX: u8 = 2;
const SCANNER_STATS_ENABLED: bool = cfg!(any(test, debug_assertions));
pub struct Scanner {
caches: Vec<CacheEntry>,
regset: Box<OnigRegSet>,
stats: ScannerStats,
cache_route: CacheRouteState,
limit_revision: Option<u64>,
search_budget: bool,
warnings: Vec<Vec<crate::backtrack_lint::BacktrackWarning>>,
rewrites: Vec<Vec<crate::backtrack_rewrite::BacktrackingRewrite>>,
settings: PatternSettings,
}
const _: () = {
const fn send_sync<T: Send + Sync>() {}
send_sync::<RegexType>();
send_sync::<OnigRegSet>();
send_sync::<Scanner>();
send_sync::<ScannerPatternCache>();
};
impl fmt::Debug for Scanner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Scanner")
.field("patterns", &self.caches.len())
.field("options", &self.settings.options)
.field("syntax", &self.settings.syntax)
.field(
"optimize_backtracking",
&self.settings.optimize_backtracking,
)
.finish_non_exhaustive()
}
}
impl Scanner {
pub fn new(patterns: &[&str]) -> Result<Scanner, RegexError> {
Self::with_config(patterns, &ScannerConfig::default())
}
pub fn with_config(patterns: &[&str], config: &ScannerConfig) -> Result<Scanner, RegexError> {
let settings = PatternSettings::of(config, false);
Self::compile(
patterns,
settings,
config.acceleration == AccelerationMode::Eager,
|pattern| settings.compile(pattern),
)
}
pub fn with_backtracking_optimization(
patterns: &[&str],
config: &ScannerConfig,
) -> Result<Scanner, RegexError> {
let settings = PatternSettings::of(config, true);
Self::compile(
patterns,
settings,
config.acceleration == AccelerationMode::Eager,
|pattern| settings.compile(pattern),
)
}
pub fn with_pattern_cache(
patterns: &[&str],
config: &ScannerConfig,
cache: &mut ScannerPatternCache,
) -> Result<Scanner, RegexError> {
cache.scanner(
patterns,
PatternSettings::of(config, false),
config.acceleration == AccelerationMode::Eager,
)
}
pub fn with_backtracking_optimization_and_pattern_cache(
patterns: &[&str],
config: &ScannerConfig,
cache: &mut ScannerPatternCache,
) -> Result<Scanner, RegexError> {
cache.scanner(
patterns,
PatternSettings::of(config, true),
config.acceleration == AccelerationMode::Eager,
)
}
fn compile<'p>(
patterns: &[&'p str],
settings: PatternSettings,
eager: bool,
mut regex: impl FnMut(&'p str) -> Result<(Arc<RegexType>, SharedSeek), RegexError>,
) -> Result<Scanner, RegexError> {
if patterns.len() > MAX_REGSET_ENTRIES {
return Err(ONIGERR_INVALID_ARGUMENT.into());
}
let parts = patterns
.iter()
.map(|pattern| regex(pattern))
.collect::<Result<Vec<_>, _>>()?;
#[cfg(feature = "dfa-prefilter")]
let automata = SharedAutomata::default();
#[cfg(not(feature = "dfa-prefilter"))]
let automata = ();
Self::assemble(patterns, settings, parts, automata, eager)
}
fn assemble(
patterns: &[&str],
settings: PatternSettings,
parts: Vec<(Arc<RegexType>, SharedSeek)>,
automata: SharedAutomata,
eager: bool,
) -> Result<Scanner, RegexError> {
#[cfg(feature = "scanner-load-profile")]
let _phase = crate::scanner::load_profile::scope(
crate::scanner::load_profile::Phase::ScannerAssembly,
);
let mut caches = Vec::with_capacity(patterns.len());
let mut regset_regs = Vec::with_capacity(patterns.len());
let mut seeks = Vec::with_capacity(patterns.len());
let mut warnings = Vec::with_capacity(patterns.len());
let mut rewrites = Vec::with_capacity(patterns.len());
for (pattern, (reg, seek)) in patterns.iter().zip(parts) {
if eager {
#[cfg(feature = "scanner-load-profile")]
let _phase = crate::scanner::load_profile::scope(
crate::scanner::load_profile::Phase::EagerHelpers,
);
crate::acceleration::prepare_program(®);
}
rewrites.push(reg.backtrack_rewrites.clone());
warnings.push(reg.backtrack_warnings.clone());
caches.push(CacheEntry::new(pattern, reg.anchor));
regset_regs.push(reg);
seeks.push(seek);
}
let (regset, r) =
onig_regset_new_shared(regset_regs, seeks, automata, settings.prefilter_warmup);
if r != ONIG_NORMAL {
return Err(r.into());
}
#[allow(unused_mut)]
let mut regset = regset.unwrap();
#[cfg(feature = "dfa-prefilter")]
if eager {
crate::regset::onig_regset_prepare_prefilter(&mut regset);
}
#[cfg(feature = "prefilter-self-check")]
crate::regset::onig_regset_set_self_check_patterns(
&mut regset,
patterns
.iter()
.map(|pattern| (*pattern).to_owned())
.collect(),
);
Ok(Scanner {
caches,
regset,
stats: ScannerStats::default(),
cache_route: CacheRouteState::default(),
limit_revision: None,
search_budget: false,
warnings,
rewrites,
settings,
})
}
pub fn warnings(&self) -> &[Vec<crate::backtrack_lint::BacktrackWarning>] {
&self.warnings
}
pub fn backtracking_rewrites(&self) -> &[Vec<crate::backtrack_rewrite::BacktrackingRewrite>] {
&self.rewrites
}
pub fn stats(&self) -> ScannerStats {
self.stats
}
pub fn prefilter_stats(&self) -> PrefilterStats {
#[cfg(feature = "dfa-prefilter")]
{
if let Some(prefilter) = crate::regset::onig_regset_prefilter(&self.regset) {
return PrefilterStats {
built: true,
covered: prefilter.covered(),
own: prefilter.own().len(),
nfa_states: prefilter.nfa_states(),
memory_usage: prefilter.memory_usage(),
cache_memory_usage: prefilter.cache_memory_usage(),
cache_clears: prefilter.cache_clears(),
retired: false,
};
}
if let Some(pending) = crate::regset::onig_regset_prefilter_pending(&self.regset) {
let covered = pending.covered();
return PrefilterStats {
covered,
own: onig_regset_number_of_regex(&self.regset) as usize - covered,
..PrefilterStats::default()
};
}
}
PrefilterStats {
own: onig_regset_number_of_regex(&self.regset) as usize,
#[cfg(feature = "dfa-prefilter")]
retired: crate::regset::onig_regset_prefilter_retired(&self.regset),
..PrefilterStats::default()
}
}
pub fn reset_stats(&mut self) {
self.stats = ScannerStats::default();
}
pub fn find_next_match(
&mut self,
text: &str,
start_position: usize,
options: ScannerFindOptions,
) -> Option<ScannerMatch> {
self.find_next_match_inner(text, 0, start_position, options, false, None)
}
pub fn find_next_match_with_id(
&mut self,
text: &str,
str_id: u64,
start_position: usize,
options: ScannerFindOptions,
) -> Option<ScannerMatch> {
self.find_next_match_inner(
text,
str_id,
start_position,
options,
true,
Some(FallbackMemoIdentity::Caller(str_id)),
)
}
pub fn find_next_match_utf16(
&mut self,
string: &OnigString,
start_position: usize,
options: ScannerFindOptions,
) -> Option<ScannerMatch> {
if string.is_ascii() {
let start = start_position.min(string.content().len());
return self.find_next_match_inner(
string.content(),
0,
start,
options,
false,
Some(FallbackMemoIdentity::OnigString(string.cache_id)),
);
}
let utf8_start = string.utf16_offset_to_utf8(start_position);
let m = self.find_next_match_inner(
string.content(),
0,
utf8_start,
options,
false,
Some(FallbackMemoIdentity::OnigString(string.cache_id)),
)?;
Some(convert_match_to_utf16(string, m))
}
pub fn find_next_match_utf16_with_id(
&mut self,
string: &OnigString,
str_id: u64,
start_position: usize,
options: ScannerFindOptions,
) -> Option<ScannerMatch> {
if string.is_ascii() {
let start = start_position.min(string.content().len());
return self.find_next_match_inner(
string.content(),
str_id,
start,
options,
true,
Some(FallbackMemoIdentity::Caller(str_id)),
);
}
let utf8_start = string.utf16_offset_to_utf8(start_position);
let m = self.find_next_match_inner(
string.content(),
str_id,
utf8_start,
options,
true,
Some(FallbackMemoIdentity::Caller(str_id)),
)?;
Some(convert_match_to_utf16(string, m))
}
fn find_next_match_inner(
&mut self,
text: &str,
str_id: u64,
start_position: usize,
options: ScannerFindOptions,
use_cache: bool,
fallback_memo_id: Option<FallbackMemoIdentity>,
) -> Option<ScannerMatch> {
let str_data = text.as_bytes();
let end = str_data.len();
if start_position > end {
return None;
}
let onig_opts = options.to_onig_options();
if !use_cache {
if SCANNER_STATS_ENABLED {
self.stats.route_regset_calls += 1;
}
return self.search_regset(str_data, end, start_position, onig_opts, fallback_memo_id);
}
let use_regset =
onig_regset_prefilter_decides(&mut self.regset, onig_opts, str_data, start_position)
|| self.should_use_regset_for_cache(str_id, options.0, start_position);
let limit_revision = if use_regset {
0
} else {
let revision = onig_get_global_limit_revision();
if self.limit_revision != Some(revision) {
self.limit_revision = Some(revision);
self.search_budget = onig_get_retry_limit_in_search() != 0;
}
revision
};
if use_regset || self.search_budget {
if SCANNER_STATS_ENABLED {
self.stats.route_regset_calls += 1;
self.stats.route_cache_regset_calls += 1;
}
self.search_regset(str_data, end, start_position, onig_opts, fallback_memo_id)
} else {
if SCANNER_STATS_ENABLED {
self.stats.route_per_regex_calls += 1;
self.stats.route_cache_per_regex_calls += 1;
if self.cache_route.route == CacheRoute::RegSet {
self.stats.route_cache_probe_calls += 1;
}
}
let (m, run_stats) = self.search_per_regex(
str_data,
end,
start_position,
str_id,
options.0,
onig_opts,
use_cache,
limit_revision,
);
self.observe_per_regex_outcome(run_stats);
m
}
}
#[inline]
fn should_use_regset_for_cache(
&mut self,
str_id: u64,
options_raw: u32,
start_position: usize,
) -> bool {
if self.cache_route.str_id != str_id || self.cache_route.options != options_raw {
self.cache_route.options = options_raw;
self.cache_route.str_id = str_id;
self.cache_route.route = CacheRoute::RegSet;
self.cache_route.calls_since_probe = 0;
self.cache_route.last_start = start_position;
self.cache_route.same_start_streak = 1;
self.cache_route.good_per_regex_streak = 0;
self.cache_route.poor_per_regex_streak = 0;
return true;
}
if start_position == self.cache_route.last_start {
self.cache_route.same_start_streak =
self.cache_route.same_start_streak.saturating_add(1);
} else {
self.cache_route.same_start_streak = 1;
self.cache_route.last_start = start_position;
if self.cache_route.route == CacheRoute::RegSet {
return true;
}
}
self.cache_route.last_start = start_position;
match self.cache_route.route {
CacheRoute::PerRegex => false,
CacheRoute::RegSet => {
if self.cache_route.same_start_streak < ROUTE_MIN_SAME_START_FOR_PROBE {
return true;
}
self.cache_route.calls_since_probe =
self.cache_route.calls_since_probe.saturating_add(1);
if self.cache_route.calls_since_probe >= ROUTE_PROBE_EVERY {
self.cache_route.calls_since_probe = 0;
false
} else {
true
}
}
}
}
#[inline]
fn observe_per_regex_outcome(&mut self, run_stats: PerRegexCallStats) {
if SCANNER_STATS_ENABLED {
self.stats.cache_checks += run_stats.cache_checks as u64;
self.stats.cache_hits += run_stats.cache_hits as u64;
self.stats.cache_misses += run_stats.cache_misses() as u64;
self.stats.vm_search_calls += run_stats.vm_calls as u64;
}
let denom = run_stats.cache_hits as u64 + run_stats.vm_calls as u64;
let reuse_permille = (run_stats.cache_hits as u64 * 1000)
.checked_div(denom)
.unwrap_or(0);
let poor = run_stats.vm_calls >= 4 && reuse_permille < 350;
let good = run_stats.cache_hits >= 4 && run_stats.vm_calls <= 2 && reuse_permille >= 700;
if poor {
self.cache_route.poor_per_regex_streak =
self.cache_route.poor_per_regex_streak.saturating_add(1);
self.cache_route.good_per_regex_streak = 0;
} else if good {
self.cache_route.good_per_regex_streak =
self.cache_route.good_per_regex_streak.saturating_add(1);
self.cache_route.poor_per_regex_streak = 0;
}
if self.cache_route.route == CacheRoute::PerRegex
&& self.cache_route.poor_per_regex_streak >= ROUTE_POOR_STREAK_TO_REGSET
{
self.cache_route.route = CacheRoute::RegSet;
self.cache_route.calls_since_probe = 0;
self.cache_route.poor_per_regex_streak = 0;
self.cache_route.good_per_regex_streak = 0;
if SCANNER_STATS_ENABLED {
self.stats.route_switch_to_regset += 1;
}
} else if self.cache_route.route == CacheRoute::RegSet
&& self.cache_route.good_per_regex_streak >= ROUTE_GOOD_STREAK_TO_PER_REGEX
{
self.cache_route.route = CacheRoute::PerRegex;
self.cache_route.calls_since_probe = 0;
self.cache_route.poor_per_regex_streak = 0;
self.cache_route.good_per_regex_streak = 0;
if SCANNER_STATS_ENABLED {
self.stats.route_switch_to_per_regex += 1;
}
}
}
fn search_regset(
&mut self,
str_data: &[u8],
end: usize,
start: usize,
option: OnigOptionType,
fallback_memo_id: Option<FallbackMemoIdentity>,
) -> Option<ScannerMatch> {
let (idx, pos) = onig_regset_search_utf8(
&mut self.regset,
str_data,
end,
start,
end,
OnigRegSetLead::PositionLead,
option,
fallback_memo_id,
);
if idx < 0 {
return None;
}
let regex_idx = idx as usize;
let match_start = if pos >= 0 { pos as usize } else { start };
if let Some(reg) = onig_regset_get_regex(&self.regset, regex_idx) {
if reg.num_mem == 0 && !reg.keep_moves_match_start {
let len = onig_regset_last_match_len(&self.regset);
if len < 0 {
return None;
}
let match_end = match_start.saturating_add(len as usize).min(end);
let mut capture_indices = SmallVec::with_capacity(1);
capture_indices.push(CaptureIndex {
start: match_start,
end: match_end,
length: match_end.saturating_sub(match_start),
});
return Some(ScannerMatch {
index: regex_idx,
capture_indices,
});
}
}
let region = crate::regset::onig_regset_get_region(&self.regset, regex_idx)?;
Some(build_scanner_match(regex_idx, region))
}
#[allow(clippy::too_many_arguments)]
fn search_per_regex(
&mut self,
str_data: &[u8],
end: usize,
start: usize,
str_id: u64,
options_raw: u32,
onig_opts: OnigOptionType,
use_cache: bool,
limit_revision: u64,
) -> (Option<ScannerMatch>, PerRegexCallStats) {
let mut best: Option<(usize, usize, bool)> = None;
let mut run_stats = PerRegexCallStats::default();
let mid_line = start > 0 && str_data[start - 1] != b'\n';
let regset = &mut self.regset;
let caches = &mut self.caches;
let n = onig_regset_number_of_regex(regset) as usize;
for (i, cache) in caches.iter_mut().enumerate().take(n) {
let stop = match best {
None => end,
Some((position, _, _)) if position <= start => break,
Some((position, _, _)) => {
let mut before = position - 1;
while before > start && (str_data[before] & 0xC0) == 0x80 {
before -= 1;
}
before
}
};
if use_cache
&& !cache.has_g_anchor
&& cache.last_str_id == str_id
&& cache.last_options == options_raw
&& cache.last_limit_revision == limit_revision
&& cache.last_position <= start
&& !(cache.after_newline_only && mid_line && cache.last_position != start)
{
run_stats.cache_checks += 1;
if !cache.last_matched {
run_stats.cache_hits += 1;
continue;
}
if cache.last_result >= 0 && (cache.last_result as usize) >= start {
run_stats.cache_hits += 1;
let position = cache.last_result as usize;
if best.is_none_or(|(best_position, _, _)| position < best_position) {
best = Some((position, i, true));
}
continue;
}
}
run_stats.vm_calls += 1;
let event =
onig_regset_entry_search(regset, i, str_data, end, start, stop, onig_opts, true);
match event {
RegSetEntryEvent::Match { position } => {
onig_regset_swap_region(regset, i, &mut cache.last_region);
cache.remember(str_id, start, options_raw, limit_revision, Some(position));
if best.is_none_or(|(best_position, _, _)| position < best_position) {
best = Some((position, i, true));
}
}
RegSetEntryEvent::Error { position, .. } => {
cache.forget();
if best.is_none_or(|(best_position, _, _)| position < best_position) {
best = Some((position, i, false));
}
}
RegSetEntryEvent::None if stop == end => {
cache.remember(str_id, start, options_raw, limit_revision, None);
}
RegSetEntryEvent::None => cache.forget(),
}
}
let out = match best {
Some((_, index, true)) => self.caches[index]
.last_region
.as_ref()
.map(|region| build_scanner_match(index, region)),
_ => None,
};
(out, run_stats)
}
}
fn build_scanner_match(index: usize, region: &OnigRegion) -> ScannerMatch {
let num_regs = region.num_regs as usize;
let mut capture_indices = SmallVec::with_capacity(num_regs);
for i in 0..num_regs {
let beg = region.beg[i];
let end = region.end[i];
if beg >= 0 && end >= beg {
let start = beg as usize;
let end = end as usize;
capture_indices.push(CaptureIndex {
start,
end,
length: end - start,
});
} else {
capture_indices.push(CaptureIndex {
start: 0,
end: 0,
length: 0,
});
}
}
ScannerMatch {
index,
capture_indices,
}
}
fn convert_match_to_utf16(string: &OnigString, m: ScannerMatch) -> ScannerMatch {
ScannerMatch {
index: m.index,
capture_indices: m
.capture_indices
.into_iter()
.map(|ci| {
let start = string.utf8_offset_to_utf16(ci.start);
let end = string.utf8_offset_to_utf16(ci.end);
CaptureIndex {
start,
end,
length: end - start,
}
})
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use smallvec::smallvec;
#[test]
fn scanner_rejects_more_patterns_than_regset_indices_can_address() {
let patterns = vec!["a"; MAX_REGSET_ENTRIES + 1];
assert!(matches!(
Scanner::new(&patterns),
Err(RegexError::InvalidArgument)
));
}
#[test]
fn inverted_capture_reads_as_unmatched() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&[r"((?=(a|ab))a?){2}"]).unwrap();
let m = scanner
.find_next_match("a", 0, ScannerFindOptions::NONE)
.unwrap();
let spans: Vec<_> = m
.capture_indices
.iter()
.map(|c| (c.start, c.end, c.length))
.collect();
assert_eq!(spans, vec![(0, 0, 0), (0, 0, 0), (1, 1, 0)]);
}
#[test]
fn cache_miss_with_truncated_range_is_not_reused() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&[";", "}"]).unwrap();
let s = "a;b}";
assert_eq!(
scanner.find_next_match_with_id(s, 1, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 1,
end: 2,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_with_id(s, 1, 2, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 3,
end: 4,
length: 1
}],
})
);
}
#[test]
fn stats_and_reset_work() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["a"]).unwrap();
assert_eq!(scanner.stats(), ScannerStats::default());
let _ = scanner.find_next_match("ba", 0, ScannerFindOptions::NONE);
let _ = scanner.find_next_match_with_id("ba", 1, 0, ScannerFindOptions::NONE);
let stats = scanner.stats();
assert!(stats.route_regset_calls >= 2);
assert_eq!(stats.route_per_regex_calls, 0);
scanner.reset_stats();
assert_eq!(scanner.stats(), ScannerStats::default());
}
#[test]
fn cache_mode_regset_probe_is_counted() {
let _limits = crate::regexec::shared_limits();
let config = ScannerConfig::default().prefilter(false);
let mut scanner = Scanner::with_config(&["a"], &config).unwrap();
for _ in 0..20 {
let _ = scanner.find_next_match_with_id("ba", 1, 0, ScannerFindOptions::NONE);
}
let stats = scanner.stats();
assert!(stats.route_cache_regset_calls > 0);
assert!(stats.route_cache_probe_calls > 0);
assert!(stats.route_per_regex_calls > 0);
}
#[test]
fn optional_prefix_match_agrees_after_cache_route_switches() {
let _limits = crate::regexec::shared_limits();
let config = ScannerConfig::default().prefilter(false);
let mut scanner = Scanner::with_config(&["a?bc", "q"], &config).unwrap();
for _ in 0..25 {
let matched = scanner
.find_next_match_with_id("qabc", 55, 1, ScannerFindOptions::NONE)
.expect("match");
assert_eq!(matched.index, 0);
assert_eq!(matched.capture_indices[0].start, 1);
assert_eq!(matched.capture_indices[0].end, 4);
}
let stats = scanner.stats();
assert!(stats.route_cache_regset_calls > 0, "{stats:?}");
assert!(stats.route_cache_per_regex_calls > 0, "{stats:?}");
}
#[test]
fn vscode_simple1() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["ell", "wo"]).unwrap();
let s = "Hello world!";
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 1,
end: 4,
length: 3
}],
})
);
assert_eq!(
scanner.find_next_match(s, 2, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 6,
end: 8,
length: 2
}],
})
);
}
#[test]
fn vscode_simple2() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["a", "b", "c"]).unwrap();
assert_eq!(
scanner.find_next_match("x", 0, ScannerFindOptions::NONE),
None
);
assert_eq!(
scanner.find_next_match("xxaxxbxxc", 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 2,
end: 3,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match("xxaxxbxxc", 4, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 5,
end: 6,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match("xxaxxbxxc", 7, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 2,
capture_indices: smallvec![CaptureIndex {
start: 8,
end: 9,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match("xxaxxbxxc", 9, ScannerFindOptions::NONE),
None
);
}
#[test]
fn vscode_unicode1() {
let _limits = crate::regexec::shared_limits();
let mut scanner1 = Scanner::new(&["1", "2"]).unwrap();
assert_eq!(
scanner1.find_next_match("ab\u{2026}cde21", 7, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 8,
end: 9,
length: 1
}],
})
);
let mut scanner2 = Scanner::new(&["\""]).unwrap();
assert_eq!(
scanner2.find_next_match("{\"\\u{2026}\": 1}", 1, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 1,
end: 2,
length: 1
}],
})
);
}
#[test]
fn vscode_unicode2() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["Y", "X"]).unwrap();
let s = "a\u{1F4BB}bYX";
assert_eq!(s.len(), 8);
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 6,
end: 7,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match(s, 5, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 6,
end: 7,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match(s, 6, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 6,
end: 7,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match(s, 7, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 7,
end: 8,
length: 1
}],
})
);
}
#[test]
fn vscode_unicode3() {
let _limits = crate::regexec::shared_limits();
let mut scanner =
Scanner::new(&["\u{0412}\u{043E}\u{0437}\u{0432}\u{0440}\u{0430}\u{0442}"]).unwrap();
let s = "\u{0412}\u{043E}\u{0437}\u{0432}\u{0440}\u{0430}\u{0442} long_var_name;";
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 0,
end: 14,
length: 14
}],
})
);
}
#[test]
fn vscode_out_of_bounds() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["X"]).unwrap();
let s = "X\u{1F4BB}X"; assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 0,
end: 1,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match(s, 1000, ScannerFindOptions::NONE),
None
);
}
#[test]
fn vscode_g_anchor() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["\\G-and"]).unwrap();
let s = "first-and-second";
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
None
);
assert_eq!(
scanner.find_next_match(s, 5, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 5,
end: 9,
length: 4
}],
})
);
}
#[test]
fn vscode_oniguruma_issue_192() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&[
"(?x)\n (?<!\\+\\+|--)(?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\*\\/|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?<!\\.|-)(:))?((?:[a-z][a-z0-9]*|([_$[:alpha:]][-_$[:alnum:].]*))(?<!\\.|-))(?=((<\\s*)|(\\s+))(?!\\?)|\\/?>))",
]).unwrap();
let s = " while (i < len && f(array[i]))";
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
None
);
}
#[test]
fn vscode_find_option_not_begin_string() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["\\Afirst"]).unwrap();
let s = "first-and-first";
assert_eq!(
scanner.find_next_match(s, 10, ScannerFindOptions::NONE),
None
);
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 0,
end: 5,
length: 5
}],
})
);
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NOT_BEGIN_STRING),
None
);
}
#[test]
fn vscode_find_option_not_end_string() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["first\\z"]).unwrap();
let s = "first-and-first";
assert_eq!(
scanner.find_next_match(s, 10, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 10,
end: 15,
length: 5
}],
})
);
assert_eq!(
scanner.find_next_match(s, 10, ScannerFindOptions::NOT_END_STRING),
None
);
}
#[test]
fn vscode_find_option_not_begin_position() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["\\G-and"]).unwrap();
let s = "first-and-second";
assert_eq!(
scanner.find_next_match(s, 5, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 5,
end: 9,
length: 4
}],
})
);
assert_eq!(
scanner.find_next_match(s, 5, ScannerFindOptions::NOT_BEGIN_POSITION),
None
);
}
#[test]
fn vscode_configure_scanner() {
let _limits = crate::regexec::shared_limits();
let config = ScannerConfig {
options: OnigOptionType::IGNORECASE,
..Default::default()
};
let mut scanner = Scanner::with_config(&["^[a-z]*$"], &config).unwrap();
let s = "ABCD";
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 0,
end: 4,
length: 4
}],
})
);
}
#[test]
fn vscode_configure_syntax() {
let _limits = crate::regexec::shared_limits();
let config = ScannerConfig {
syntax: ScannerSyntax::Python,
..Default::default()
};
let mut scanner = Scanner::with_config(&["^(?P<name>.*)$"], &config).unwrap();
let s = "first-and-first";
assert_eq!(
scanner.find_next_match(s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![
CaptureIndex {
start: 0,
end: 15,
length: 15
},
CaptureIndex {
start: 0,
end: 15,
length: 15
},
],
})
);
}
#[test]
fn vscode_invalid_pattern_error() {
let result = Scanner::new(&["(?P<name>a*)"]);
assert!(result.is_err());
}
#[test]
fn vscode_utf16_simple1() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["ell", "wo"]).unwrap();
let s = OnigString::new("Hello world!");
assert_eq!(
scanner.find_next_match_utf16(&s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 1,
end: 4,
length: 3
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 2, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 6,
end: 8,
length: 2
}],
})
);
}
#[test]
fn vscode_utf16_simple2() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["a", "b", "c"]).unwrap();
let x = OnigString::new("x");
assert_eq!(
scanner.find_next_match_utf16(&x, 0, ScannerFindOptions::NONE),
None
);
let abc = OnigString::new("xxaxxbxxc");
assert_eq!(
scanner.find_next_match_utf16(&abc, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 2,
end: 3,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&abc, 4, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 5,
end: 6,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&abc, 7, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 2,
capture_indices: smallvec![CaptureIndex {
start: 8,
end: 9,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&abc, 9, ScannerFindOptions::NONE),
None
);
}
#[test]
fn vscode_utf16_unicode1() {
let _limits = crate::regexec::shared_limits();
let mut scanner1 = Scanner::new(&["1", "2"]).unwrap();
let s1 = OnigString::new("ab\u{2026}cde21"); assert_eq!(
scanner1.find_next_match_utf16(&s1, 5, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 6,
end: 7,
length: 1
}],
})
);
let mut scanner2 = Scanner::new(&["\""]).unwrap();
let s2 = OnigString::new("{\"\\u{2026}\": 1}");
assert_eq!(
scanner2.find_next_match_utf16(&s2, 1, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 1,
end: 2,
length: 1
}],
})
);
}
#[test]
fn vscode_utf16_unicode2() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["Y", "X"]).unwrap();
let s = OnigString::new("a\u{1F4BB}bYX");
assert_eq!(s.utf16_len(), 6);
assert_eq!(
scanner.find_next_match_utf16(&s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 4,
end: 5,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 1, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 4,
end: 5,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 3, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 4,
end: 5,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 4, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 4,
end: 5,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 5, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 1,
capture_indices: smallvec![CaptureIndex {
start: 5,
end: 6,
length: 1
}],
})
);
}
#[test]
fn vscode_utf16_unicode3() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["Возврат"]).unwrap();
let s = OnigString::new("Возврат long_var_name;");
assert_eq!(
scanner.find_next_match_utf16(&s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 0,
end: 7,
length: 7
}],
})
);
}
#[test]
fn vscode_utf16_out_of_bounds() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["X"]).unwrap();
let s = OnigString::new("X\u{1F4BB}X"); assert_eq!(
scanner.find_next_match_utf16(&s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 0,
end: 1,
length: 1
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 1000, ScannerFindOptions::NONE),
None
);
}
#[test]
fn vscode_utf16_g_anchor() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["\\G-and"]).unwrap();
let s = OnigString::new("first-and-second");
assert_eq!(
scanner.find_next_match_utf16(&s, 0, ScannerFindOptions::NONE),
None
);
assert_eq!(
scanner.find_next_match_utf16(&s, 5, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 5,
end: 9,
length: 4
}],
})
);
}
#[test]
fn vscode_utf16_oniguruma_issue_192() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&[
"(?x)\n (?<!\\+\\+|--)(?<=[({\\[,?=>:*]|&&|\\|\\||\\?|\\*\\/|^await|[^\\._$[:alnum:]]await|^return|[^\\._$[:alnum:]]return|^default|[^\\._$[:alnum:]]default|^yield|[^\\._$[:alnum:]]yield|^)\\s*\n (?!<\\s*[_$[:alpha:]][_$[:alnum:]]*((\\s+extends\\s+[^=>])|,)) # look ahead is not type parameter of arrow\n (?=(<)\\s*(?:([_$[:alpha:]][-_$[:alnum:].]*)(?<!\\.|-)(:))?((?:[a-z][a-z0-9]*|([_$[:alpha:]][-_$[:alnum:].]*))(?<!\\.|-))(?=((<\\s*)|(\\s+))(?!\\?)|\\/?>))",
]).unwrap();
let s = OnigString::new(" while (i < len && f(array[i]))");
assert_eq!(
scanner.find_next_match_utf16(&s, 0, ScannerFindOptions::NONE),
None
);
}
#[test]
fn vscode_utf16_find_option_not_begin_string() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["\\Afirst"]).unwrap();
let s = OnigString::new("first-and-first");
assert_eq!(
scanner.find_next_match_utf16(&s, 10, ScannerFindOptions::NONE),
None
);
assert_eq!(
scanner.find_next_match_utf16(&s, 0, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 0,
end: 5,
length: 5
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 0, ScannerFindOptions::NOT_BEGIN_STRING),
None
);
}
#[test]
fn vscode_utf16_find_option_not_end_string() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["first\\z"]).unwrap();
let s = OnigString::new("first-and-first");
assert_eq!(
scanner.find_next_match_utf16(&s, 10, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 10,
end: 15,
length: 5
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 10, ScannerFindOptions::NOT_END_STRING),
None
);
}
#[test]
fn vscode_utf16_find_option_not_begin_position() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["\\G-and"]).unwrap();
let s = OnigString::new("first-and-second");
assert_eq!(
scanner.find_next_match_utf16(&s, 5, ScannerFindOptions::NONE),
Some(ScannerMatch {
index: 0,
capture_indices: smallvec![CaptureIndex {
start: 5,
end: 9,
length: 4
}],
})
);
assert_eq!(
scanner.find_next_match_utf16(&s, 5, ScannerFindOptions::NOT_BEGIN_POSITION),
None
);
}
#[test]
fn onig_string_ascii() {
let s = OnigString::new("hello");
assert_eq!(s.utf16_len(), 5);
assert_eq!(s.utf16_offset_to_utf8(0), 0);
assert_eq!(s.utf16_offset_to_utf8(3), 3);
assert_eq!(s.utf16_offset_to_utf8(5), 5);
assert_eq!(s.utf8_offset_to_utf16(0), 0);
assert_eq!(s.utf8_offset_to_utf16(5), 5);
}
#[test]
fn onig_string_instances_with_equal_contents_have_distinct_cache_ids() {
let _limits = crate::regexec::shared_limits();
let first = OnigString::new("unchanged");
let second = OnigString::new("unchanged");
assert_ne!(first.cache_id, second.cache_id);
let mut scanner = Scanner::new(&["a*bc"]).expect("scanner");
assert_eq!(
scanner.find_next_match_utf16(&first, 0, ScannerFindOptions::NONE),
None
);
assert_eq!(
scanner.find_next_match_utf16(&second, 0, ScannerFindOptions::NONE),
None
);
}
#[test]
fn onig_string_bmp() {
let s = OnigString::new("Возврат");
assert_eq!(s.utf16_len(), 7);
assert_eq!(s.content().len(), 14);
assert_eq!(s.utf16_offset_to_utf8(0), 0);
assert_eq!(s.utf16_offset_to_utf8(1), 2);
assert_eq!(s.utf16_offset_to_utf8(7), 14);
assert_eq!(s.utf8_offset_to_utf16(0), 0);
assert_eq!(s.utf8_offset_to_utf16(2), 1);
assert_eq!(s.utf8_offset_to_utf16(14), 7);
}
#[test]
fn onig_string_supplementary() {
let s = OnigString::new("a\u{1F4BB}b");
assert_eq!(s.utf16_len(), 4); assert_eq!(s.content().len(), 6);
assert_eq!(s.utf16_offset_to_utf8(0), 0); assert_eq!(s.utf16_offset_to_utf8(1), 1); assert_eq!(s.utf16_offset_to_utf8(2), 5); assert_eq!(s.utf16_offset_to_utf8(3), 5); assert_eq!(s.utf16_offset_to_utf8(4), 6);
assert_eq!(s.utf8_offset_to_utf16(0), 0); assert_eq!(s.utf8_offset_to_utf16(1), 1); assert_eq!(s.utf8_offset_to_utf16(2), 1); assert_eq!(s.utf8_offset_to_utf16(3), 1); assert_eq!(s.utf8_offset_to_utf16(4), 1); assert_eq!(s.utf8_offset_to_utf16(5), 3); assert_eq!(s.utf8_offset_to_utf16(6), 4); }
#[test]
fn multi_pattern_correct_index() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["\\d+", "[a-z]+"]).unwrap();
let m = scanner
.find_next_match("hello42", 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 1); assert_eq!(m.capture_indices[0].start, 0);
assert_eq!(m.capture_indices[0].end, 5);
}
#[test]
fn capture_groups() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["(\\d{4})-(\\d{2})-(\\d{2})"]).unwrap();
let m = scanner
.find_next_match("date: 2026-02-16", 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 0);
assert_eq!(m.capture_indices.len(), 4); assert_eq!(m.capture_indices[0].start, 6);
assert_eq!(m.capture_indices[0].end, 16);
assert_eq!(m.capture_indices[1].start, 6);
assert_eq!(m.capture_indices[1].end, 10);
assert_eq!(m.capture_indices[2].start, 11);
assert_eq!(m.capture_indices[2].end, 13);
assert_eq!(m.capture_indices[3].start, 14);
assert_eq!(m.capture_indices[3].end, 16);
}
#[test]
fn long_string_path() {
let _limits = crate::regexec::shared_limits();
let long = "a".repeat(1500);
let mut scanner = Scanner::new(&["aaa"]).unwrap();
let m = scanner
.find_next_match(&long, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 0);
assert_eq!(m.capture_indices[0].start, 0);
assert_eq!(m.capture_indices[0].end, 3);
}
#[test]
fn caching_with_str_id() {
let _limits = crate::regexec::shared_limits();
let long = "x".repeat(500) + "hello" + &"y".repeat(1000);
let mut scanner = Scanner::new(&["hello", "world"]).unwrap();
let m = scanner
.find_next_match_with_id(&long, 1, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 0);
assert_eq!(m.capture_indices[0].start, 500);
let m = scanner.find_next_match_with_id(&long, 1, 501, ScannerFindOptions::NONE);
assert!(m.is_none());
}
#[test]
fn g_anchor_bypasses_cache() {
let _limits = crate::regexec::shared_limits();
let long = "a".repeat(1500);
let mut scanner = Scanner::new(&["\\Ga"]).unwrap();
let m = scanner
.find_next_match_with_id(&long, 1, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.capture_indices[0].start, 0);
let m = scanner
.find_next_match_with_id(&long, 1, 1, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.capture_indices[0].start, 1);
}
#[test]
fn g_anchor_uses_original_search_start_in_position_lead() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["\\G(\\s+)", "\\s+"]).unwrap();
let s = "x> y";
let m = scanner
.find_next_match(s, 1, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 1);
assert_eq!(m.capture_indices[0].start, 2);
assert_eq!(m.capture_indices[0].end, 3);
let m = scanner
.find_next_match_with_id(s, 42, 1, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 1);
assert_eq!(m.capture_indices[0].start, 2);
assert_eq!(m.capture_indices[0].end, 3);
let _ = scanner.find_next_match_with_id(s, 42, 0, ScannerFindOptions::NONE);
let m = scanner
.find_next_match_with_id(s, 42, 1, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 1);
assert_eq!(m.capture_indices[0].start, 2);
assert_eq!(m.capture_indices[0].end, 3);
}
#[test]
fn find_options_conversion() {
let opts = ScannerFindOptions::NOT_BEGIN_STRING;
let onig = opts.to_onig_options();
assert!(onig.contains(OnigOptionType::NOT_BEGIN_STRING));
let opts = ScannerFindOptions::NOT_END_STRING;
let onig = opts.to_onig_options();
assert!(onig.contains(OnigOptionType::NOT_END_STRING));
let opts = ScannerFindOptions::NOT_BEGIN_POSITION;
let onig = opts.to_onig_options();
assert!(onig.contains(OnigOptionType::NOT_BEGIN_POSITION));
let opts = ScannerFindOptions::from_bits(3); let onig = opts.to_onig_options();
assert!(onig.contains(OnigOptionType::NOT_BEGIN_STRING));
assert!(onig.contains(OnigOptionType::NOT_END_STRING));
}
type Found = Option<(usize, usize, usize)>;
type LimitCase = (&'static [&'static str], String, Vec<(usize, Found)>);
#[test]
fn limit_errors_match_c_on_every_route_and_call_history() {
let _limits = crate::regexec::exclusive_limits();
let old_limit = crate::regexec::onig_get_retry_limit_in_match();
crate::regexec::onig_set_retry_limit_in_match(10_000);
let cases: [LimitCase; 2] = [
(
&[r"(\w+)+x", "a"],
"a".repeat(30),
vec![
(0, Some((1, 0, 1))),
(1, Some((1, 1, 2))),
(5, Some((1, 5, 6))),
(29, Some((1, 29, 30))),
(30, None),
],
),
(
&[r"(a+)+b", "c"],
format!("{}c b", "a".repeat(27)),
vec![
(0, None),
(10, None),
(15, Some((1, 27, 28))),
(20, Some((1, 27, 28))),
(27, Some((1, 27, 28))),
],
),
];
let mut failures = Vec::new();
for (patterns, text, expected) in &cases {
let onig = OnigString::new(text);
let reversed: Vec<_> = expected.iter().rev().copied().collect();
let repeated: Vec<_> = expected
.iter()
.flat_map(|&call| std::iter::repeat_n(call, 20))
.collect();
for order in [expected.clone(), reversed, repeated] {
let mut scanners: [Scanner; 4] =
std::array::from_fn(|_| Scanner::new(patterns).unwrap());
for &(start, want) in &order {
let none = ScannerFindOptions::NONE;
let found = [
scanners[0].find_next_match(text, start, none),
scanners[1].find_next_match_with_id(text, 7, start, none),
scanners[2].find_next_match_utf16(&onig, start, none),
scanners[3].find_next_match_utf16_with_id(&onig, 7, start, none),
];
for (route, found) in found.iter().enumerate() {
let found = found.as_ref().map(|m| {
let whole = &m.capture_indices[0];
(m.index, whole.start, whole.end)
});
if found != want {
failures.push((patterns.to_vec(), route, start, found, want));
}
}
}
}
}
crate::regexec::onig_set_retry_limit_in_match(old_limit);
assert!(failures.is_empty(), "{failures:?}");
}
#[test]
fn keep_matches_rank_by_their_attempt_position_on_every_route() {
let _limits = crate::regexec::shared_limits();
let patterns = [r".+\K,", r"[^\s]"];
let text = "\n!c1 1,é1";
let onig = OnigString::new(text);
let starts = [0, 1, 2, 2, 1, 0];
let mut scanners: [Scanner; 4] = std::array::from_fn(|_| Scanner::new(&patterns).unwrap());
for start in starts.into_iter().chain(std::iter::repeat_n(0, 20)) {
let none = ScannerFindOptions::NONE;
let found = [
scanners[0].find_next_match(text, start, none),
scanners[1].find_next_match_with_id(text, 3, start, none),
scanners[2].find_next_match_utf16(&onig, start, none),
scanners[3].find_next_match_utf16_with_id(&onig, 3, start, none),
];
for (route, found) in found.iter().enumerate() {
let found = found.as_ref().map(|m| {
let whole = &m.capture_indices[0];
(m.index, whole.start, whole.end)
});
assert_eq!(found, Some((0, 6, 7)), "route {route}, start {start}");
}
}
}
#[test]
fn multi_pattern_earliest_wins() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["world", "hello"]).unwrap();
let m = scanner
.find_next_match("hello world", 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 1); assert_eq!(m.capture_indices[0].start, 0);
}
#[test]
fn empty_pattern_matches() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["", "x"]).unwrap();
let m = scanner
.find_next_match("hello", 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.index, 0); }
#[test]
fn optional_capture_group() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["(a)(b)?(c)"]).unwrap();
let m = scanner
.find_next_match("ac", 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.capture_indices.len(), 4);
assert_eq!(m.capture_indices[2].start, 0);
assert_eq!(m.capture_indices[2].end, 0);
assert_eq!(m.capture_indices[2].length, 0);
}
#[test]
fn cache_hit_on_repeated_search_same_string() {
let _limits = crate::regexec::shared_limits();
let config = ScannerConfig::default().prefilter(false);
let mut scanner = Scanner::with_config(&["foo", "bar", "baz"], &config).unwrap();
let input = "xxfooxxbarxxbaz";
for _ in 0..9 {
let _ = scanner.find_next_match_with_id(input, 42, 0, ScannerFindOptions::NONE);
}
for _ in 0..20 {
let _ = scanner.find_next_match_with_id(input, 42, 0, ScannerFindOptions::NONE);
}
let stats = scanner.stats();
assert!(
stats.route_per_regex_calls > 0 || stats.cache_hits > 0,
"expected per-regex or cache activity, got {:?}",
stats
);
}
#[test]
fn cache_no_match_reused() {
let _limits = crate::regexec::shared_limits();
let config = ScannerConfig::default().prefilter(false);
let mut scanner = Scanner::with_config(&["zzz", "a"], &config).unwrap();
let input = "aaa";
for _ in 0..30 {
let m = scanner.find_next_match_with_id(input, 1, 0, ScannerFindOptions::NONE);
assert!(m.is_some());
assert_eq!(m.unwrap().index, 1); }
let stats = scanner.stats();
assert!(
stats.route_per_regex_calls > 0 || stats.cache_hits > 0,
"expected per-regex or cache activity, got {:?}",
stats
);
}
#[test]
fn cache_invalidated_on_new_string() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["x"]).unwrap();
let m1 = scanner
.find_next_match_with_id("axb", 1, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m1.capture_indices[0].start, 1);
let m2 = scanner
.find_next_match_with_id("xab", 2, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m2.capture_indices[0].start, 0);
}
#[test]
fn utf16_with_id_ascii() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["x"]).unwrap();
let s = OnigString::new("axb");
let m = scanner
.find_next_match_utf16_with_id(&s, 10, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.capture_indices[0].start, 1);
}
#[test]
fn utf16_with_id_unicode() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["x"]).unwrap();
let s = OnigString::new("💻x");
let m = scanner
.find_next_match_utf16_with_id(&s, 20, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m.capture_indices[0].start, 2);
}
#[test]
fn scanner_syntax_variants() {
let syntaxes = [
ScannerSyntax::Asis,
ScannerSyntax::PosixBasic,
ScannerSyntax::Emacs,
ScannerSyntax::Grep,
ScannerSyntax::GnuRegex,
ScannerSyntax::Java,
ScannerSyntax::Perl,
ScannerSyntax::PerlNg,
ScannerSyntax::Ruby,
ScannerSyntax::Python,
];
for syntax in syntaxes {
let config = ScannerConfig {
options: ONIG_OPTION_NONE,
syntax,
..Default::default()
};
let scanner = Scanner::with_config(&["hello"], &config);
assert!(scanner.is_ok(), "failed for {:?}", syntax);
}
}
#[test]
fn g_anchor_pattern() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&[r"\Gx", "y"]).unwrap();
let input = "xxy";
let m1 = scanner
.find_next_match_with_id(input, 1, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m1.index, 0); assert_eq!(m1.capture_indices[0].start, 0);
let m2 = scanner
.find_next_match_with_id(input, 1, 2, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(m2.index, 1); }
#[test]
fn zero_width_matches_at_end_are_reported() {
let _limits = crate::regexec::shared_limits();
for pattern in ["$", r"\z", "a*"] {
let mut scanner = Scanner::new(&[pattern]).unwrap();
let found = scanner
.find_next_match("abc", 3, ScannerFindOptions::NONE)
.unwrap_or_else(|| panic!("missing end match for {pattern:?}"));
assert_eq!(found.index, 0);
assert_eq!(found.capture_indices[0].start, 3);
assert_eq!(found.capture_indices[0].end, 3);
assert_eq!(found.capture_indices[0].length, 0);
}
}
#[test]
fn zero_width_match_on_empty_input_is_reported() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["$"]).unwrap();
let found = scanner
.find_next_match("", 0, ScannerFindOptions::NONE)
.expect("empty input should match the end anchor");
assert_eq!(found.index, 0);
assert_eq!(found.capture_indices[0].start, 0);
assert_eq!(found.capture_indices[0].end, 0);
assert_eq!(found.capture_indices[0].length, 0);
}
#[test]
fn repeated_end_anchor_search_agrees_across_adaptive_routes() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&[r"\z", "q"]).unwrap();
for call in 0..25 {
let found = scanner
.find_next_match_with_id("abcd", 7, 1, ScannerFindOptions::NONE)
.unwrap_or_else(|| panic!("missing end match on call {call}"));
assert_eq!(found.index, 0, "call {call}");
assert_eq!(found.capture_indices[0].start, 4, "call {call}");
assert_eq!(found.capture_indices[0].end, 4, "call {call}");
}
}
#[test]
fn many_searches_trigger_route_switching() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["a+", "b+", "c+"]).unwrap();
let input = "aabbcc";
let mut pos = 0;
let mut matches = Vec::new();
for _ in 0..20 {
let Some(m) = scanner.find_next_match_with_id(input, 99, pos, ScannerFindOptions::NONE)
else {
break;
};
pos = m.capture_indices[0].end;
matches.push(m.index);
}
assert_eq!(matches, vec![0, 1, 2]); let stats = scanner.stats();
assert!(stats.route_regset_calls > 0);
}
#[test]
fn same_start_streak_triggers_per_regex() {
let _limits = crate::regexec::shared_limits();
let mut scanner = Scanner::new(&["x", "y"]).unwrap();
let input = "xy";
for _ in 0..12 {
let _ = scanner.find_next_match_with_id(input, 1, 0, ScannerFindOptions::NONE);
}
let stats = scanner.stats();
assert!(stats.route_regset_calls > 0 || stats.route_per_regex_calls > 0);
}
const CACHED_PATTERN_SETS: [&[&str]; 3] = [
&[
r"\G(\s+)",
r"(?<word>\w+)",
r#""(?:[^"\\]|\\.)*""#,
r"\Afirst",
r"(?<=\.)\w+",
r"(a+)+b",
],
&[
r"(\w)\1",
r".+\K,",
r"(?<word>\w+)",
r"\s*$",
r"[a-z]*(?:foo|bar|baz)",
r"(?:[ab]|(*MAX{2}).)*c",
],
&[
r"\b(?:fn|let|first)\b",
r"\p{Greek}+",
r"first\z",
r".*;",
r"(?<word>\w+)",
r"(a+)+b",
r"([0-9]+(_?))+(\.)([0-9]+)",
],
];
const CACHED_PATTERN_SUBJECTS: [&str; 6] = [
"first-and-first",
"let x = \"a\\\"b\"; // done",
"\n!c1 1,é1 zfoo",
"αβγ fn foo.bar 12_34.56",
"abcbaaccaaa abc",
"aaaaaaaaaaaaaaaaaaaa\u{1F4BB}c b",
];
const KEYWORD_SET: &[&str] = &[
r"\b(?:abstract|boolean|break|byte|case|catch|char|class|const|continue)\b",
r"\b(?:debugger|default|delete|do|double|else|enum|export|extends|final)\b",
r"\b(?:finally|float|for|function|goto|if|implements|import|in|instanceof)\b",
r"[A-Za-z_]\w*",
r"\d+",
r"//.*$",
];
const TINY_JSON_SET: &[&str] = &[
"}",
"\"",
r"/\*\*(?!/)",
r"/\*",
r"(//).*$\n?",
":",
r"[^}\s]",
];
#[test]
fn acceleration_modes_prepare_scanners_before_search() {
let _limits = crate::regexec::shared_limits();
let eager = ScannerConfig::default()
.prefilter(false)
.acceleration(AccelerationMode::Eager)
.prefilter_warmup(u32::MAX);
let auto = eager.clone().prefilter(true).prefilter_warmup(0);
let off = eager.clone().prefilter(false);
assert_eq!(auto.acceleration, AccelerationMode::Auto);
assert_eq!(off.acceleration, AccelerationMode::Off);
let mut cache = ScannerPatternCache::new();
let mut lazy = Scanner::with_pattern_cache(TINY_JSON_SET, &auto, &mut cache).unwrap();
assert!(!lazy.prefilter_stats().built);
let mut ready = Scanner::with_pattern_cache(TINY_JSON_SET, &eager, &mut cache).unwrap();
assert_eq!(
ready.prefilter_stats().built,
cfg!(feature = "dfa-prefilter")
);
assert_eq!(compiled_addresses(&lazy), compiled_addresses(&ready));
assert_eq!(cache.len(), TINY_JSON_SET.len());
let again = Scanner::with_pattern_cache(TINY_JSON_SET, &eager, &mut cache).unwrap();
assert_eq!(
again.prefilter_stats().built,
cfg!(feature = "dfa-prefilter")
);
#[cfg(feature = "dfa-prefilter")]
assert_eq!(cache.automata.len(), 1);
let mut plain = Scanner::with_config(TINY_JSON_SET, &off).unwrap();
for text in ["}", " \"name\": 42", "// comment\n", "é", ""] {
for at in text.char_indices().map(|(at, _)| at).chain([text.len()]) {
let expected = plain.find_next_match(text, at, ScannerFindOptions::NONE);
assert_eq!(
ready.find_next_match(text, at, ScannerFindOptions::NONE),
expected
);
assert_eq!(
lazy.find_next_match(text, at, ScannerFindOptions::NONE),
expected
);
}
}
assert!(!plain.prefilter_stats().built);
let disabled = ScannerConfig {
prefilter: false,
..eager
};
assert!(
!Scanner::with_config(TINY_JSON_SET, &disabled)
.unwrap()
.prefilter_stats()
.built
);
}
#[test]
fn prefilter_builds_after_the_warmup() {
let _limits = crate::regexec::shared_limits();
let text = "let x = class Foo extends Bar { const y = 1; } // done\n";
let config = ScannerConfig::default().prefilter_warmup(3);
let mut scanner = Scanner::with_config(KEYWORD_SET, &config).unwrap();
let mut plain =
Scanner::with_config(KEYWORD_SET, &config.clone().prefilter(false)).unwrap();
for (i, at) in (0..8).enumerate() {
let want = plain.find_next_match(text, at, ScannerFindOptions::NONE);
let got = scanner.find_next_match(text, at, ScannerFindOptions::NONE);
assert_eq!(got, want, "search {i}");
let stats = scanner.prefilter_stats();
assert_eq!(
stats.built,
cfg!(feature = "dfa-prefilter") && i >= 3,
"search {i}: {stats:?}"
);
}
#[cfg(feature = "dfa-prefilter")]
{
let stats = scanner.prefilter_stats();
assert!(
stats.nfa_states > crate::dfa_prefilter::TINY_SET_NFA_STATES,
"{stats:?}"
);
}
assert!(!plain.prefilter_stats().built);
}
#[test]
fn tiny_sets_build_no_automata_unless_eager() {
let _limits = crate::regexec::shared_limits();
let lines = [
"{\n",
" \"service\": \"München <team> & café\",\n",
" \"version\": 1,\n",
" \"limits\": { \"requests\": 1000, \"timeout\": 2.5e1 }\n",
"}\n",
];
let lazy = ScannerConfig::default().prefilter_warmup(1);
let eager = ScannerConfig::default().prefilter_warmup(0);
let mut lazy_scanner = Scanner::with_config(TINY_JSON_SET, &lazy).unwrap();
let mut eager_scanner = Scanner::with_config(TINY_JSON_SET, &eager).unwrap();
let mut plain =
Scanner::with_config(TINY_JSON_SET, &lazy.clone().prefilter(false)).unwrap();
for line in lines {
let string = OnigString::new(line);
let mut at = 0;
loop {
let want = plain.find_next_match_utf16(&string, at, ScannerFindOptions::NONE);
assert_eq!(
lazy_scanner.find_next_match_utf16(&string, at, ScannerFindOptions::NONE),
want
);
assert_eq!(
eager_scanner.find_next_match_utf16(&string, at, ScannerFindOptions::NONE),
want
);
match want {
Some(m) if m.captures()[0].end > at => at = m.captures()[0].end,
_ => break,
}
}
}
let stats = lazy_scanner.prefilter_stats();
assert!(!stats.built, "{stats:?}");
assert_eq!(stats.own, TINY_JSON_SET.len(), "{stats:?}");
let stats = eager_scanner.prefilter_stats();
assert_eq!(stats.built, cfg!(feature = "dfa-prefilter"), "{stats:?}");
#[cfg(feature = "dfa-prefilter")]
assert!(
stats.covered <= crate::dfa_prefilter::TINY_SET_ENTRIES
&& stats.nfa_states <= crate::dfa_prefilter::TINY_SET_NFA_STATES,
"{stats:?}"
);
}
fn compiled_addresses(scanner: &Scanner) -> Vec<*const RegexType> {
(0..onig_regset_number_of_regex(&scanner.regset) as usize)
.map(|i| onig_regset_get_regex(&scanner.regset, i).unwrap() as *const RegexType)
.collect()
}
fn route_scanners(mut build: impl FnMut() -> Scanner) -> [Scanner; 4] {
std::array::from_fn(|_| build())
}
fn every_route(
scanners: &mut [Scanner; 4],
onig: &OnigString,
id: u64,
start: usize,
options: ScannerFindOptions,
) -> [Option<ScannerMatch>; 4] {
let text = onig.content();
let utf16_start = onig.utf8_offset_to_utf16(start);
[
scanners[0].find_next_match(text, start, options),
scanners[1].find_next_match_with_id(text, id, start, options),
scanners[2].find_next_match_utf16(onig, utf16_start, options),
scanners[3].find_next_match_utf16_with_id(onig, id, utf16_start, options),
]
}
fn cached_and_uncached_differences(
cached: &mut [[Scanner; 4]],
uncached: &mut [[Scanner; 4]],
subjects: &[&str],
options: &[ScannerFindOptions],
) -> Vec<String> {
let mut differences = Vec::new();
for (set, (cached, uncached)) in cached.iter_mut().zip(uncached.iter_mut()).enumerate() {
for &options in options {
for (id, subject) in (1..).zip(subjects) {
let onig = OnigString::new(subject);
let starts: Vec<usize> = (0..=subject.len())
.filter(|&at| subject.is_char_boundary(at))
.collect();
let calls = starts
.iter()
.copied()
.chain(starts.iter().flat_map(|&at| std::iter::repeat_n(at, 11)));
for start in calls {
let want = every_route(uncached, &onig, id, start, options);
let got = every_route(cached, &onig, id, start, options);
if got != want {
differences.push(format!(
"set {set}, {options:?}, {subject:?} from {start}: \
{got:?} != {want:?}"
));
}
}
}
}
}
differences
}
const PREFILTER_PATTERN_SETS: [&[&str]; 9] = [
&[
r"(?<=\))\s*\(",
r"(?<![-\w])(?:accent-color|align-content|align-items|all)(?![-\w])",
r"(?!\s*\[)[A-Za-z_]\w*",
r"\G\s+",
r"^\s*#\s*(include)\b",
r"\b(?:fn|let|first|loop)\b",
r"(?=\s*:)\w+",
r"\s+(?=use\b)",
r"\A\s*$",
r"\n?$",
r".*;",
r"x\Z",
],
&[
r"(?i)(?:kelvin|straße|fiat|xyz)",
r"(?i)[k]+",
r"\p{Greek}+",
r"[^\x00-\x7F]+",
r"\w+ing\b",
r"[é-ü]",
r"(?i)résumé",
r"\bσ\w*",
r"(?i)(?<![-\w])(?:a|abbr|acronym|address|applet)(?![-\w])",
r"\d+",
],
&[
r"(\w)\1",
r"<(\w+)[^>]*>.*?</\1>",
r"(?<n>\()(?:[^()]|\g<n>)*\)",
r"(a)?(?(1)b|c)",
r"(?~\*/)\*/",
r".+\K,",
r"(?>a+)b",
r"a++b",
r"(?i)(ab|cd|ef|gh)\1",
r"\s*(;)",
],
&[
r"(?:(?:ab)+)?c",
r"(?:a*)+b",
r"(?:x(?=a))?y",
r"(?m:.)x",
r".x",
r"\h+",
r"(?:(?=a)|(?<=b))c",
r"(?<!\w)(?:all|small|x)(?!\w)",
r"(?<![-\w])all(?![-\w])",
r"..x",
r"[^a-z]x",
],
&[
r"(?:\s*(?:/\*(?:[^*]|\*+(?!/))*\*/))+|\s+|(?<=\W)|(?=\W)|^|\n?$|\A|\Z",
r"()",
r"\s*$",
r"^",
],
&[
r"[0-9]+(?:\.[0-9]+)?",
r"[A-Za-z_][A-Za-z0-9_]*",
r"\s+",
r"//.*",
r"/\*",
r"[{}()\[\]]",
r"==|!=|<=|>=|[<>=]",
r"\$\{[^}]*\}",
r"'(?:[^'\\]|\\.)*'",
r"\\u[0-9A-Fa-f]{4}",
],
&[r".x", r"\s*$"],
&[r"\b", r"\B", r"x"],
&[
r"\W",
r"(?i)(?:kelvin|street|fiat|xyz)!",
r"(?i)(?:kelvin|street|fiat|xyz)$",
r"(?i)(?:kelvin|street|fiat|xyz)\b",
r"(?(a)b|c)",
r"((?(a)b|c))(\1)",
r"(a)?(?(1)b|c)",
r"a",
],
];
const PREFILTER_SUBJECTS: [&str; 13] = [
"",
"aéx bêx fix small all-x",
"d日Σ\\x ü日a 日",
"😀a \u{212A}elvin! \u{212A}elvin kelvin! STRASSE",
"ab abab cb a",
"int main() { return 0; } // done\n",
" #include <stdio.h>",
"let x = \"a\\\"b\"; use std::io; use",
"<a href=\"x\">t</a> ((a)(b)) /* c */ x",
"\n!c1 1,é1 zfoo ſtraße KELVIN Fiat fiat\n",
"αβγ fn foo.bar 12_34.56 σίσυφος",
"aaaaaaaaaaaaaaaaaaaa\u{1F4BB}c b, ab; abab abababc",
"accent-color: all; Abbr.address xyz\n\n",
];
#[test]
fn prefilter_matches_the_position_lead_search_on_every_route() {
let _limits = crate::regexec::shared_limits();
let with = ScannerConfig::default().prefilter_warmup(0);
let without = ScannerConfig::default().prefilter(false);
let every_option: Vec<_> = (0..8).map(ScannerFindOptions::from_bits).collect();
let sets: Vec<&[&str]> = (PREFILTER_PATTERN_SETS.iter().copied())
.chain(CACHED_PATTERN_SETS.iter().copied())
.collect();
let subjects: Vec<&str> = (PREFILTER_SUBJECTS.iter().copied())
.chain(CACHED_PATTERN_SUBJECTS.iter().copied())
.collect();
let mut filtered: Vec<[Scanner; 4]> = sets
.iter()
.map(|patterns| route_scanners(|| Scanner::with_config(patterns, &with).unwrap()))
.collect();
let mut plain: Vec<[Scanner; 4]> = sets
.iter()
.map(|patterns| route_scanners(|| Scanner::with_config(patterns, &without).unwrap()))
.collect();
for (patterns, filtered) in sets.iter().zip(&filtered) {
let stats = filtered[0].prefilter_stats();
assert!(!stats.built, "{patterns:?}: {stats:?}");
assert_eq!(stats.covered + stats.own, patterns.len());
}
let differences =
cached_and_uncached_differences(&mut filtered, &mut plain, &subjects, &every_option);
assert!(differences.is_empty(), "{}", differences.join("\n"));
for (patterns, (filtered, plain)) in sets.iter().zip(filtered.iter().zip(&plain)) {
let stats = filtered[0].prefilter_stats();
let callouts = patterns.iter().any(|pattern| pattern.contains("(*MAX"));
let built = cfg!(feature = "dfa-prefilter") && !callouts;
assert_eq!(stats.built, built, "{patterns:?}: {stats:?}");
assert!(!plain[0].prefilter_stats().built);
assert_eq!(stats.covered + stats.own, patterns.len());
}
}
#[test]
fn own_entries_cost_linear_work_over_a_tokenizing_loop() {
let _limits = crate::regexec::shared_limits();
let word = "a".repeat(2_000);
let pairs = "ba".repeat(1_000);
let gaps = format!("{}ba", "x".repeat(100)).repeat(20);
let variants: [(&[&str], &str, u64); 11] = [
(&["a", r"(?<=z)"], &word, 0),
(&[r"(?<=a)", "b"], &word, 0),
(&[r"(?<=z)", "b"], &word, 8),
(&[r"\G ?", "a"], &word, 0),
(&[r"(?<=\))(?!\w)", "a"], &word, 0),
(&[r"(?<=\.)\w+", r"\w+:", r"\d+", "a"], &word, 0),
(&[r"(?<=/)[^/]+", r"[^/]+/", "a"], &word, 0),
(&["a", r".*(?<=z)"], &word, 0),
(&[r"(?<=b)a?", r"a[ab]{2}a"], &pairs, 0),
(&[r"(?<=b)a?", r"a[ab]{2}a"], &gaps, 0),
(&[r"(?<=z)a[ab]*a", "a"], &pairs, 0),
];
#[derive(Clone, Copy, Debug)]
enum Api {
Id,
Plain,
Utf16,
}
#[cfg(feature = "dfa-prefilter")]
fn automata_work() -> [u64; 3] {
[
crate::dfa_prefilter::EARLIEST_CALLS.with(|c| c.get()),
crate::dfa_prefilter::META_BYTES.with(|c| c.get()),
crate::dfa_prefilter::SCAN_STEPS.with(|c| c.get()),
]
}
#[cfg(not(feature = "dfa-prefilter"))]
fn automata_work() -> [u64; 3] {
[0; 3]
}
let work = || -> [u64; 7] {
let automata = automata_work();
[
crate::regexec::VM_ATTEMPTS.with(|c| c.get()),
crate::regexec::VM_BYTES.with(|c| c.get()),
crate::regexec::FORWARD_SEARCH_BYTES.with(|c| c.get()),
automata[0],
automata[1],
automata[2],
crate::regset::TABLE_SCAN_BYTES.with(|c| c.get()),
]
};
for (patterns, text, max_earliest) in variants {
for api in [Api::Id, Api::Plain, Api::Utf16] {
let run = |prefilter: bool| {
let config = ScannerConfig::default()
.prefilter(prefilter)
.prefilter_warmup(0);
let mut scanner = Scanner::with_config(patterns, &config).unwrap();
let string = OnigString::new(text);
let before = work();
let results: Vec<_> = (0..text.len())
.map(|at| {
let found = match api {
Api::Id => scanner.find_next_match_with_id(
text,
7,
at,
ScannerFindOptions::NONE,
),
Api::Plain => {
scanner.find_next_match(text, at, ScannerFindOptions::NONE)
}
Api::Utf16 => scanner.find_next_match_utf16_with_id(
&string,
7,
at,
ScannerFindOptions::NONE,
),
};
found.map(|m| (m.index, m.captures()[0].start, m.captures()[0].end))
})
.collect();
let after = work();
let spent: [u64; 7] = std::array::from_fn(|i| after[i] - before[i]);
(spent, results)
};
let n = text.len() as u64;
let (without, plain) = run(false);
let (with, filtered) = run(true);
assert_eq!(filtered, plain, "{patterns:?} {api:?}");
assert!(
with[0] <= 2 * without[0] + n,
"{patterns:?} {api:?}: {} VM attempts with the pre-filter, {} without",
with[0],
without[0]
);
assert!(
with[1] <= 2 * without[1] + 8 * n,
"{patterns:?} {api:?}: {} VM bytes with the pre-filter, {} without",
with[1],
without[1]
);
assert!(
with[2] <= 2 * without[2] + 8 * n,
"{patterns:?} {api:?}: {} forward-search bytes with the pre-filter, {} without",
with[2],
without[2]
);
let read_without = without[2] + without[6];
assert!(
with[5] <= 2 * read_without + 8 * n,
"{patterns:?} {api:?}: {} candidate walk bytes, {read_without} read without the pre-filter",
with[5]
);
if matches!(api, Api::Plain) {
assert!(
with[3] <= 4 * n,
"{patterns:?} {api:?}: {} meta regex searches over {n} calls",
with[3]
);
assert!(
with[4] <= 4 * read_without + 8 * n,
"{patterns:?} {api:?}: {} meta regex bytes, {read_without} read without the pre-filter",
with[4]
);
} else {
assert!(
with[3] <= max_earliest,
"{patterns:?} {api:?}: {} meta regex searches, at most {max_earliest}",
with[3]
);
assert!(
with[4] <= 8 * n,
"{patterns:?} {api:?}: {} meta regex bytes over {n} bytes of text",
with[4]
);
}
}
}
}
#[cfg(feature = "dfa-prefilter")]
#[test]
fn candidate_scans_stop_once_every_admissible_entry_is_a_candidate() {
let _limits = crate::regexec::shared_limits();
let text = "a".repeat(2_000);
let calls = text.len() as u64;
let sets: [&[&str]; 3] = [
&[r"(?<=\.)\w+", "a"],
&[r"(?<=\.)\w+", r"\w+:", r"\d+", "a"],
&[r"(?<=/)[^/]+", r"[^/]+/", "a"],
];
for patterns in sets {
let run = |prefilter: bool| {
let config = ScannerConfig::default()
.prefilter(prefilter)
.prefilter_warmup(0);
let mut scanner = Scanner::with_config(patterns, &config).unwrap();
let steps_before = crate::dfa_prefilter::SCAN_STEPS.with(|c| c.get());
let attempts_before = crate::regexec::VM_ATTEMPTS.with(|c| c.get());
let results: Vec<_> = (0..text.len())
.map(|at| {
scanner
.find_next_match_with_id(&text, 7, at, ScannerFindOptions::NONE)
.map(|m| (m.index, m.captures()[0].start, m.captures()[0].end))
})
.collect();
let steps = crate::dfa_prefilter::SCAN_STEPS.with(|c| c.get()) - steps_before;
let attempts = crate::regexec::VM_ATTEMPTS.with(|c| c.get()) - attempts_before;
(steps, attempts, results)
};
let (_, without, plain) = run(false);
let (steps, with, filtered) = run(true);
assert_eq!(filtered, plain, "{patterns:?}");
assert!(
with <= 2 * without + calls,
"{patterns:?}: {with} VM attempts with the pre-filter, {without} without"
);
assert!(
steps <= 3 * calls,
"{patterns:?}: {steps} bytes read by the candidate scans over {calls} calls"
);
}
}
#[test]
fn pattern_cache_scanners_match_uncached_scanners() {
let _limits = crate::regexec::exclusive_limits();
let config = ScannerConfig::default().prefilter_warmup(0);
let every_option: Vec<_> = (0..8).map(ScannerFindOptions::from_bits).collect();
let distinct: std::collections::HashSet<&str> = CACHED_PATTERN_SETS
.iter()
.flat_map(|set| set.iter().copied())
.collect();
let mut cache = ScannerPatternCache::new();
let mut failures = Vec::new();
for optimize_backtracking in [false, true] {
let uncached_scanner = |patterns: &[&str]| {
if optimize_backtracking {
Scanner::with_backtracking_optimization(patterns, &config).unwrap()
} else {
Scanner::with_config(patterns, &config).unwrap()
}
};
let mut cached: Vec<[Scanner; 4]> = CACHED_PATTERN_SETS
.iter()
.map(|patterns| {
route_scanners(|| {
if optimize_backtracking {
Scanner::with_backtracking_optimization_and_pattern_cache(
patterns, &config, &mut cache,
)
} else {
Scanner::with_pattern_cache(patterns, &config, &mut cache)
}
.unwrap()
})
})
.collect();
let mut uncached: Vec<[Scanner; 4]> = CACHED_PATTERN_SETS
.iter()
.map(|patterns| route_scanners(|| uncached_scanner(patterns)))
.collect();
let settings = usize::from(optimize_backtracking) + 1;
assert_eq!(cache.len(), settings * distinct.len());
let mut addresses = HashMap::new();
for (patterns, (cached, uncached)) in
CACHED_PATTERN_SETS.iter().zip(cached.iter().zip(&uncached))
{
for scanner in cached {
assert_eq!(scanner.warnings(), uncached[0].warnings());
assert_eq!(
scanner.backtracking_rewrites(),
uncached[0].backtracking_rewrites()
);
for (pattern, address) in patterns.iter().zip(compiled_addresses(scanner)) {
assert_eq!(*addresses.entry(*pattern).or_insert(address), address);
}
}
for (pattern, address) in patterns.iter().zip(compiled_addresses(&uncached[0])) {
assert_ne!(addresses[pattern], address);
}
}
let rewritten = uncached[2][0].backtracking_rewrites()[6].len();
assert_eq!(rewritten, usize::from(optimize_backtracking));
assert_eq!(uncached[0][0].warnings()[5].len(), 1);
failures.extend(cached_and_uncached_differences(
&mut cached,
&mut uncached,
&CACHED_PATTERN_SUBJECTS,
&every_option,
));
let per_regex_calls: u64 = (cached.iter())
.flat_map(|routes| [&routes[1], &routes[3]])
.map(|scanner| scanner.stats().route_per_regex_calls)
.sum();
assert!(per_regex_calls > 0);
}
let limit_case = format!("{}c b", "a".repeat(12));
let limit_sets: Vec<&[&str]> = (CACHED_PATTERN_SETS.iter().copied())
.chain([&[r"(a+)+b", "c"][..]])
.collect();
let limit_subjects: Vec<&str> = (CACHED_PATTERN_SUBJECTS.iter().copied())
.chain([limit_case.as_str()])
.collect();
let limits = [
("retry limit in match", 0, 0),
("retry limit in search", 20_000, 0),
("match stack limit", 0, 64),
];
let saved = (
crate::regexec::onig_get_retry_limit_in_match(),
crate::regexec::onig_get_retry_limit_in_search(),
crate::regexec::onig_get_match_stack_limit(),
);
for (name, in_search, stack) in limits {
crate::regexec::onig_set_retry_limit_in_match(1_000);
crate::regexec::onig_set_retry_limit_in_search(in_search);
crate::regexec::onig_set_match_stack_limit(stack);
let mut cached: Vec<[Scanner; 4]> = (limit_sets.iter())
.map(|patterns| {
route_scanners(|| {
Scanner::with_pattern_cache(patterns, &config, &mut cache).unwrap()
})
})
.collect();
let mut uncached: Vec<[Scanner; 4]> = (limit_sets.iter())
.map(|patterns| route_scanners(|| Scanner::with_config(patterns, &config).unwrap()))
.collect();
if in_search == 0 && stack == 0 {
let limited = &mut cached[3][0];
let none = ScannerFindOptions::NONE;
assert_eq!(limited.find_next_match(&limit_case, 0, none), None);
let after = limited.find_next_match(&limit_case, 5, none).unwrap();
assert_eq!((after.index, after.capture_indices[0].start), (1, 12));
}
failures.extend(
cached_and_uncached_differences(
&mut cached,
&mut uncached,
&limit_subjects,
&[ScannerFindOptions::NONE],
)
.into_iter()
.map(|difference| format!("{name}: {difference}")),
);
}
crate::regexec::onig_set_retry_limit_in_match(saved.0);
crate::regexec::onig_set_retry_limit_in_search(saved.1);
crate::regexec::onig_set_match_stack_limit(saved.2);
assert_eq!(cache.len(), 2 * distinct.len() + 1);
assert!(failures.is_empty(), "{failures:#?}");
}
#[test]
fn pattern_cache_keeps_settings_apart() {
let _limits = crate::regexec::shared_limits();
let plain = ScannerConfig::default().prefilter_warmup(0);
let ignore_case = ScannerConfig {
options: plain.options | ONIG_OPTION_IGNORECASE,
..plain.clone()
};
let asis = ScannerConfig {
syntax: ScannerSyntax::Asis,
..plain.clone()
};
let decimal = r"([0-9]+(_?))+(\.)([0-9]+)";
let pattern = "a+b";
let mut cache = ScannerPatternCache::new();
let optimized = Scanner::with_backtracking_optimization_and_pattern_cache(
&[pattern, decimal],
&plain,
&mut cache,
)
.unwrap();
let mut scanners = [&plain, &ignore_case, &asis].map(|config| {
Scanner::with_pattern_cache(&[pattern, decimal], config, &mut cache).unwrap()
});
assert_eq!(cache.len(), 8);
assert_eq!(cache.compiled.len(), 4);
let mut addresses: Vec<_> = scanners.iter().flat_map(compiled_addresses).collect();
addresses.extend(compiled_addresses(&optimized));
let all = addresses.len();
addresses.sort();
addresses.dedup();
assert_eq!(addresses.len(), all);
assert!(scanners[0].backtracking_rewrites()[1].is_empty());
assert_eq!(optimized.backtracking_rewrites()[1].len(), 1);
let text = "AAB a+b aab";
let found = scanners.each_mut().map(|scanner| {
let m = scanner.find_next_match(text, 0, ScannerFindOptions::NONE)?;
Some((m.capture_indices[0].start, m.capture_indices[0].end))
});
assert_eq!(found, [Some((8, 11)), Some((0, 3)), Some((4, 7))]);
let again =
Scanner::with_pattern_cache(&[decimal, pattern], &ignore_case, &mut cache).unwrap();
assert_eq!(cache.len(), 8);
let before = compiled_addresses(&scanners[1]);
assert_eq!(compiled_addresses(&again), [before[1], before[0]]);
}
#[test]
fn failed_construction_leaves_the_pattern_cache_unchanged() {
let config = ScannerConfig::default().prefilter_warmup(0);
let mut cache = ScannerPatternCache::new();
let kept = Scanner::with_pattern_cache(&["a", "b"], &config, &mut cache).unwrap();
let snapshot = |cache: &ScannerPatternCache| {
let mut entries: Vec<_> = cache
.compiled
.iter()
.flat_map(|(_, patterns)| patterns.iter())
.map(|(pattern, cached)| (pattern.to_string(), Arc::as_ptr(&cached.reg)))
.collect();
entries.sort();
(cache.compiled.len(), entries)
};
let before = snapshot(&cache);
let failing = ["a", "c", "(", "d"];
let error = Scanner::with_pattern_cache(&failing, &config, &mut cache).err();
assert!(error.is_some());
assert_eq!(error, Scanner::with_config(&failing, &config).err());
assert_eq!(snapshot(&cache), before);
let longest = ScannerConfig {
options: config.options | ONIG_OPTION_FIND_LONGEST,
..config.clone()
};
let error = Scanner::with_pattern_cache(&["e", "f"], &longest, &mut cache).err();
assert!(error.is_some());
assert_eq!(error, Scanner::with_config(&["e", "f"], &longest).err());
assert_eq!(snapshot(&cache), before);
let ruby = ScannerConfig {
syntax: ScannerSyntax::Ruby,
..config.clone()
};
let mut empty = Scanner::with_pattern_cache(&[], &ruby, &mut cache).unwrap();
assert_eq!(
empty.find_next_match("a", 0, ScannerFindOptions::NONE),
None
);
assert_eq!(snapshot(&cache), before);
let next = Scanner::with_pattern_cache(&["b", "c", "b"], &config, &mut cache).unwrap();
let kept_addresses = compiled_addresses(&kept);
let next_addresses = compiled_addresses(&next);
assert_eq!(next_addresses[0], kept_addresses[1]);
assert_eq!(next_addresses[2], kept_addresses[1]);
assert_eq!(cache.len(), 3);
}
#[test]
fn pattern_cache_and_scanners_drop_in_any_order() {
let _limits = crate::regexec::shared_limits();
let config = ScannerConfig::default().prefilter_warmup(0);
let mut cache = ScannerPatternCache::new();
assert!(cache.is_empty());
let mut first = Scanner::with_pattern_cache(&["x(y)", "z"], &config, &mut cache).unwrap();
let second = Scanner::with_pattern_cache(&["z"], &config, &mut cache).unwrap();
let references = |cache: &ScannerPatternCache, pattern: &str| {
Arc::strong_count(&cache.compiled[0].1[pattern].reg)
};
assert_eq!(
(references(&cache, "x(y)"), references(&cache, "z")),
(2, 3)
);
drop(second);
assert_eq!(references(&cache, "z"), 2);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
let whole = |m: Option<ScannerMatch>| {
m.map(|m| {
let spans: Vec<_> = m.capture_indices.iter().map(|c| (c.start, c.end)).collect();
(m.index, spans)
})
};
let found = whole(first.find_next_match("axyz", 0, ScannerFindOptions::NONE));
assert_eq!(found, Some((0, vec![(1, 3), (2, 3)])));
let third = Scanner::with_pattern_cache(&["z"], &config, &mut cache).unwrap();
assert_ne!(compiled_addresses(&third)[0], compiled_addresses(&first)[1]);
drop(first);
let mut fourth = Scanner::with_pattern_cache(&["z"], &config, &mut cache).unwrap();
drop(third);
assert_eq!(references(&cache, "z"), 2);
drop(cache);
let found = whole(fourth.find_next_match("axyz", 0, ScannerFindOptions::NONE));
assert_eq!(found, Some((0, vec![(3, 4)])));
}
#[test]
fn scanners_sharing_a_pattern_cache_search_on_several_threads() {
let _limits = crate::regexec::shared_limits();
let patterns: &[&str] = &[
r"[a-z]*(?:foo|bar|baz)",
r"\w*end\b",
r"(?i)(?:error|warn|fatal|panic)",
r"(?<word>\w+)@(?<host>[a-z]+)",
r"\s+",
];
let subjects = [
"a weekend of error reports from foo@bar",
"WARN: the legend ends here, zbaz",
"\u{1F4BB} fatal\tpanic friend",
];
let config = ScannerConfig::default().prefilter_warmup(0);
let mut reference = Scanner::with_config(patterns, &config).unwrap();
let expected: Vec<Vec<Option<ScannerMatch>>> = subjects
.iter()
.map(|subject| {
(0..=subject.len())
.filter(|&at| subject.is_char_boundary(at))
.map(|at| reference.find_next_match(subject, at, ScannerFindOptions::NONE))
.collect()
})
.collect();
let mut cache = ScannerPatternCache::new();
let scanners: Vec<Scanner> = (0..4)
.map(|_| Scanner::with_pattern_cache(patterns, &config, &mut cache).unwrap())
.collect();
std::thread::scope(|scope| {
for mut scanner in scanners {
let expected = &expected;
scope.spawn(move || {
for _ in 0..20 {
for (subject, expected) in subjects.iter().zip(expected) {
let found: Vec<_> = (0..=subject.len())
.filter(|&at| subject.is_char_boundary(at))
.map(|at| {
scanner.find_next_match(subject, at, ScannerFindOptions::NONE)
})
.collect();
assert_eq!(&found, expected);
}
}
});
}
});
}
#[test]
fn pattern_cache_replays_the_captured_scanner_traces() {
let _limits = crate::regexec::shared_limits();
type Expected = Option<(usize, Vec<(usize, usize)>)>;
let index = |value: &serde_json::Value| value.as_u64().unwrap() as usize;
for (name, distinct_patterns) in [("cpp", 250), ("java", 115), ("scss", 104)] {
let path = format!(
"{}/benches/{name}_scanner/trace.json",
env!("CARGO_MANIFEST_DIR")
);
let trace: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
let pattern_sets: Vec<Vec<&str>> = trace["scanners"]
.as_array()
.unwrap()
.iter()
.map(|set| {
let set = set.as_array().unwrap();
set.iter().map(|p| p.as_str().unwrap()).collect()
})
.collect();
let config = ScannerConfig::default().prefilter_warmup(0);
let mut cache = ScannerPatternCache::new();
let mut scanners: Vec<Scanner> = pattern_sets
.iter()
.map(|patterns| Scanner::with_pattern_cache(patterns, &config, &mut cache).unwrap())
.collect();
assert_eq!(cache.len(), distinct_patterns, "{name}");
let mut fresh_warnings = HashMap::new();
let mut addresses = HashMap::new();
for (patterns, scanner) in pattern_sets.iter().zip(&scanners) {
let compiled = patterns.iter().zip(compiled_addresses(scanner));
for (j, (pattern, address)) in compiled.enumerate() {
assert_eq!(*addresses.entry(*pattern).or_insert(address), address);
let fresh = fresh_warnings
.entry(*pattern)
.or_insert_with(|| Scanner::new(&[pattern]).unwrap().warnings()[0].clone());
assert_eq!(&scanner.warnings()[j], fresh, "{name}: {pattern}");
}
}
assert_eq!(addresses.len(), distinct_patterns, "{name}");
let strings: Vec<OnigString> = trace["subjects"]
.as_array()
.unwrap()
.iter()
.map(|s| OnigString::new(s.as_str().unwrap()))
.collect();
for (i, row) in trace["calls"].as_array().unwrap().iter().enumerate() {
let expected: Expected = (!row[4].is_null()).then(|| {
let captures = row[5].as_array().unwrap();
let captures = captures.iter().map(|c| (index(&c[0]), index(&c[1])));
(index(&row[4]), captures.collect())
});
let found = scanners[index(&row[0])].find_next_match_utf16(
&strings[index(&row[1])],
index(&row[2]),
ScannerFindOptions::from_bits(index(&row[3]) as u32),
);
let found: Expected = found.map(|m| {
let captures = m.capture_indices.iter().map(|c| (c.start, c.end));
(m.index, captures.collect())
});
assert_eq!(found, expected, "{name}: call {i}");
}
}
}
}