use crate::first_bytes::derive_start_byte_map;
use crate::oniguruma::*;
use crate::regenc::{
OnigEncoding, onigenc_get_prev_char_head, onigenc_is_ascii_compatible_encoding,
};
use crate::regexec::{
FirstOpTest, MatchArg, OnigMatchParam, first_op_fails, first_op_test, forward_search,
may_skip_first_op_failures, onig_get_global_limit_revision, onig_get_match_stack_limit,
onig_get_retry_limit_in_match, onig_get_retry_limit_in_search, onig_get_time_limit, onig_match,
onig_match_with_msa_start, search_in_range,
};
use crate::regint::*;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OnigRegSetLead {
PositionLead = 0,
RegexLead = 1,
PriorityToRegexOrder = 2,
}
struct RegSetEntry {
reg: Arc<RegexType>,
region: Option<OnigRegion>,
fallback_memo_safe: bool,
required_literals_safe: bool,
start_filter: Option<Box<[u8; CHAR_MAP_SIZE]>>,
fallback: bool,
gated: bool,
has_callouts: bool,
first_op: Option<FirstOpTest>,
}
#[derive(Clone, Copy)]
enum SkipNeedle {
None,
One(u8),
Two(u8, u8),
Three(u8, u8, u8),
}
pub struct OnigRegSet {
entries: Vec<RegSetEntry>,
enc: OnigEncoding,
anchor: i32,
anc_dmin: OnigLen,
anc_dmax: OnigLen,
all_low_high: bool,
anychar_inf: bool,
first_byte_candidates: Box<[Vec<u16>; 256]>,
table_entry_count: usize,
table_entries: Vec<u16>,
fallback_search_candidates: Vec<FallbackCandidate>,
fallback_memo_key: Option<FallbackMemoKey>,
fallback_memos: Vec<Vec<FallbackMemo>>,
scratch_limits: Option<FallbackMemoLimits>,
scratch_limits_revision: Option<u64>,
scratch_table_retry_counters: Vec<u64>,
skip_needle: SkipNeedle,
scratch_msa: Option<MatchArg>,
last_match_len: i32,
table_start_bytes: Option<Vec<Option<[u64; 4]>>>,
#[cfg(test)]
table_positions_scanned: u64,
gates: Vec<EntryGate>,
gate_generation: u32,
gate_subject: Option<(FallbackMemoIdentity, usize)>,
has_gated: bool,
has_gated_callouts: bool,
}
#[derive(Clone, Copy, Debug)]
struct FallbackCandidate {
index: u16,
memo_safe: bool,
begin_position: bool,
after_newline_only: bool,
no_match_from: usize,
exact_miss: usize,
match_from: usize,
match_at: usize,
}
impl FallbackCandidate {
fn new(index: usize, entry: &RegSetEntry) -> Self {
Self {
index: index as u16,
memo_safe: entry.fallback_memo_safe,
begin_position: (entry.reg.anchor & ANCR_BEGIN_POSITION) != 0,
after_newline_only: (entry.reg.anchor & ANCR_ANYCHAR_INF) != 0,
no_match_from: usize::MAX,
exact_miss: usize::MAX,
match_from: usize::MAX,
match_at: usize::MAX,
}
}
}
#[derive(Clone, Copy)]
enum FallbackMemo {
ExactStartMiss(usize),
MatchAt {
searched_from: usize,
position: usize,
},
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct FallbackMemoKey {
identity: FallbackMemoIdentity,
end: usize,
option: OnigOptionType,
retry_limit_in_match: u64,
retry_limit_in_search: u64,
match_stack_limit: u32,
time_limit: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct FallbackMemoLimits {
retry_limit_in_match: u64,
retry_limit_in_search: u64,
match_stack_limit: u32,
time_limit: u64,
}
impl FallbackMemoLimits {
fn current() -> Self {
Self {
retry_limit_in_match: onig_get_retry_limit_in_match(),
retry_limit_in_search: onig_get_retry_limit_in_search(),
match_stack_limit: onig_get_match_stack_limit(),
time_limit: onig_get_time_limit(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FallbackMemoIdentity {
Caller(u64),
OnigString(u64),
}
const FALLBACK_MEMO_CAPACITY: usize = 8;
fn fallback_start_filter(reg: &RegexType) -> Option<Box<[u8; CHAR_MAP_SIZE]>> {
(has_variable_optimizer(reg) && reg.dist_max == INFINITE_LEN && fallback_memo_is_safe(reg))
.then(|| derive_start_byte_map(reg))
.flatten()
.map(Box::new)
}
#[inline]
fn required_literals<'a>(
entry: &'a RegSetEntry,
option: OnigOptionType,
msa: &MatchArg,
) -> Option<&'a crate::required_literals::RequiredLiterals> {
let reg = &entry.reg;
reg.required_literals.as_deref().filter(|required| {
entry.required_literals_safe
&& required.applies(msa, opton_find_longest(option | reg.options))
})
}
fn required_literals_are_safe(reg: &RegexType) -> bool {
reg.extp.as_ref().is_none_or(|ext| ext.callout_num == 0)
&& !reg.ops.iter().any(|op| op.opcode == OpCode::Move)
}
#[inline(never)]
fn next_required_literal(reg: &RegexType, str_data: &[u8], s: usize, end: usize) -> Option<usize> {
reg.required_literals.as_deref()?.find(str_data, s, end)
}
#[inline]
fn fallback_memo_is_safe(reg: &RegexType) -> bool {
reg.extp.as_ref().is_none_or(|ext| ext.callout_num == 0)
&& !reg.ops.iter().any(|op| op.opcode == OpCode::CheckPosition)
}
#[inline]
fn enclen(enc: OnigEncoding, str_data: &[u8], s: usize) -> usize {
if s >= str_data.len() {
return 1;
}
if str_data[s] < 0x80 && onigenc_is_ascii_compatible_encoding(enc) {
return 1;
}
enc.mbc_enc_len(&str_data[s..])
}
#[inline]
fn has_variable_optimizer(reg: &RegexType) -> bool {
reg.dist_max > 0
&& match reg.optimize {
OptimizeType::Map => true,
OptimizeType::Str | OptimizeType::StrFast | OptimizeType::StrFastStepForward => {
!reg.exact.is_empty()
}
_ => false,
}
}
fn table_start_map(reg: &RegexType) -> Option<[u8; CHAR_MAP_SIZE]> {
let mut start_map = derive_start_byte_map(reg)?;
if reg.start_dispatch && reg.has_first_byte_map {
for (byte, dispatch) in start_map.iter_mut().zip(®.first_byte_map) {
*byte &= *dispatch;
}
}
Some(start_map)
}
#[inline]
fn table_routes_by_start_map(reg: &RegexType) -> bool {
reg.dist_max != INFINITE_LEN || reg.start_dispatch
}
#[inline]
fn has_finite_variable_optimizer(reg: &RegexType) -> bool {
reg.dist_max != INFINITE_LEN && has_variable_optimizer(reg)
}
fn add_entry_by_start_map(
table: &mut [Vec<u16>; CHAR_MAP_SIZE],
start_map: &[u8; CHAR_MAP_SIZE],
idx: u16,
) {
for (byte, entries) in table.iter_mut().enumerate() {
if start_map[byte] != 0 {
entries.push(idx);
}
}
}
fn add_entry_to_first_byte_table(table: &mut [Vec<u16>; 256], reg: &RegexType, idx: u16) {
if reg.optimize == OptimizeType::Map && reg.dist_min == 0 {
for (b, slot) in table.iter_mut().enumerate() {
if reg.map[b] != 0 {
slot.push(idx);
}
}
} else if reg.dist_min == 0 && !reg.exact.is_empty() {
table[reg.exact[0] as usize].push(idx);
} else if reg.has_first_byte_map {
for (b, slot) in table.iter_mut().enumerate() {
if reg.first_byte_map[b] != 0 {
slot.push(idx);
}
}
} else {
for slot in table.iter_mut() {
slot.push(idx);
}
}
}
fn compute_skip_needle(table: &[Vec<u16>; 256]) -> SkipNeedle {
let mut bytes: Vec<u8> = Vec::new();
for (b, slot) in table.iter().enumerate() {
if !slot.is_empty() {
bytes.push(b as u8);
if bytes.len() > 3 {
return SkipNeedle::None;
}
}
}
match bytes.len() {
0 => SkipNeedle::None,
1 => SkipNeedle::One(bytes[0]),
2 => SkipNeedle::Two(bytes[0], bytes[1]),
3 => SkipNeedle::Three(bytes[0], bytes[1], bytes[2]),
_ => SkipNeedle::None,
}
}
fn build_first_byte_table(set: &mut OnigRegSet) {
let mut table: Box<[Vec<u16>; 256]> = Box::new(std::array::from_fn(|_| Vec::new()));
let mut fallback_search_candidates = Vec::new();
let mut table_entries = Vec::new();
for (i, entry) in set.entries.iter_mut().enumerate() {
entry.fallback = false;
entry.gated = false;
if has_variable_optimizer(&entry.reg) {
if table_routes_by_start_map(&entry.reg) {
if let Some(start_map) = table_start_map(&entry.reg) {
add_entry_by_start_map(&mut table, &start_map, i as u16);
table_entries.push(i as u16);
entry.gated = true;
continue;
}
}
entry.fallback = true;
fallback_search_candidates.push(FallbackCandidate::new(i, entry));
} else {
add_entry_to_first_byte_table(&mut table, &entry.reg, i as u16);
table_entries.push(i as u16);
}
}
set.skip_needle = compute_skip_needle(&table);
set.first_byte_candidates = table;
set.table_entry_count = table_entries.len();
set.table_entries = table_entries;
set.fallback_search_candidates = fallback_search_candidates;
set.fallback_memo_key = None;
set.fallback_memos = vec![Vec::new(); set.entries.len()];
set.scratch_limits = None;
set.scratch_limits_revision = None;
set.scratch_table_retry_counters = vec![0; set.entries.len()];
set.scratch_msa = None;
set.table_start_bytes = None;
set.has_gated = set.entries.iter().any(|entry| entry.gated);
set.has_gated_callouts = set
.entries
.iter()
.any(|entry| entry.gated && entry.has_callouts);
set.gates = vec![EntryGate::STALE; set.entries.len()];
set.gate_generation = 0;
set.gate_subject = None;
}
pub fn onig_regset_new(regs: Vec<Box<RegexType>>) -> (Option<Box<OnigRegSet>>, i32) {
let mut set = regset_alloc();
for reg in regs {
let r = onig_regset_add(&mut set, reg);
if r != ONIG_NORMAL {
return (None, r);
}
}
build_first_byte_table(&mut set);
(Some(set), ONIG_NORMAL)
}
pub(crate) fn onig_regset_new_shared(regs: Vec<Arc<RegexType>>) -> (Option<Box<OnigRegSet>>, i32) {
let mut set = regset_alloc();
for reg in regs {
let r = onig_regset_add_shared(&mut set, reg);
if r != ONIG_NORMAL {
return (None, r);
}
}
build_first_byte_table(&mut set);
(Some(set), ONIG_NORMAL)
}
fn regset_alloc() -> Box<OnigRegSet> {
Box::new(OnigRegSet {
entries: Vec::new(),
enc: &crate::encodings::utf8::ONIG_ENCODING_UTF8,
anchor: 0,
anc_dmin: 0,
anc_dmax: 0,
all_low_high: false,
anychar_inf: false,
first_byte_candidates: Box::new(std::array::from_fn(|_| Vec::new())),
table_entry_count: 0,
table_entries: Vec::new(),
fallback_search_candidates: Vec::new(),
fallback_memo_key: None,
fallback_memos: Vec::new(),
scratch_limits: None,
scratch_limits_revision: None,
scratch_table_retry_counters: Vec::new(),
skip_needle: SkipNeedle::None,
scratch_msa: None,
last_match_len: ONIG_MISMATCH,
table_start_bytes: None,
#[cfg(test)]
table_positions_scanned: 0,
gates: Vec::new(),
gate_generation: 0,
gate_subject: None,
has_gated: false,
has_gated_callouts: false,
})
}
pub fn onig_regset_add(set: &mut OnigRegSet, reg: Box<RegexType>) -> i32 {
onig_regset_add_shared(set, Arc::from(reg))
}
pub(crate) fn onig_regset_add_shared(set: &mut OnigRegSet, reg: Arc<RegexType>) -> i32 {
if opton_find_longest(reg.options) {
return ONIGERR_INVALID_ARGUMENT;
}
if !set.entries.is_empty() && !std::ptr::eq(reg.enc, set.enc) {
return ONIGERR_INVALID_ARGUMENT;
}
let region = Some(OnigRegion::new());
let fallback_memo_safe = fallback_memo_is_safe(®);
let required_literals_safe = required_literals_are_safe(®);
let start_filter = fallback_start_filter(®);
let first_op = first_op_test(®);
set.entries.push(RegSetEntry {
reg,
region,
fallback_memo_safe,
required_literals_safe,
start_filter,
fallback: false,
gated: false,
has_callouts: false,
first_op,
});
let entry = set.entries.last_mut().expect("just pushed");
entry.has_callouts = entry
.reg
.extp
.as_ref()
.is_some_and(|ext| ext.callout_num != 0);
set.table_start_bytes = None;
set.fallback_memo_key = None;
set.fallback_memos.resize_with(set.entries.len(), Vec::new);
set.scratch_limits = None;
set.scratch_limits_revision = None;
set.scratch_table_retry_counters
.resize(set.entries.len(), 0);
set.scratch_table_retry_counters.fill(0);
set.scratch_msa = None;
let new_idx = (set.entries.len() - 1) as u16;
if has_variable_optimizer(&set.entries[new_idx as usize].reg) {
let start_map = table_routes_by_start_map(&set.entries[new_idx as usize].reg)
.then(|| table_start_map(&set.entries[new_idx as usize].reg))
.flatten();
if let Some(start_map) = start_map {
add_entry_by_start_map(&mut set.first_byte_candidates, &start_map, new_idx);
set.table_entry_count += 1;
set.table_entries.push(new_idx);
set.entries[new_idx as usize].gated = true;
set.has_gated = true;
set.has_gated_callouts |= set.entries[new_idx as usize].has_callouts;
set.gates.resize(set.entries.len(), EntryGate::STALE);
set.skip_needle = compute_skip_needle(&set.first_byte_candidates);
} else {
set.entries[new_idx as usize].fallback = true;
let candidate =
FallbackCandidate::new(new_idx as usize, &set.entries[new_idx as usize]);
set.fallback_search_candidates.push(candidate);
}
} else {
add_entry_to_first_byte_table(
&mut set.first_byte_candidates,
&set.entries[new_idx as usize].reg,
new_idx,
);
set.table_entry_count += 1;
set.table_entries.push(new_idx);
set.skip_needle = compute_skip_needle(&set.first_byte_candidates);
}
let n = set.entries.len();
let reg_ref = &*set.entries[n - 1].reg;
let anchor = reg_ref.anchor;
let anc_dist_min = reg_ref.anc_dist_min;
let anc_dist_max = reg_ref.anc_dist_max;
let optimize = reg_ref.optimize;
let dist_max = reg_ref.dist_max;
if n == 1 {
set.enc = reg_ref.enc;
set.anchor = anchor;
set.anc_dmin = anc_dist_min;
set.anc_dmax = anc_dist_max;
set.all_low_high = optimize != OptimizeType::None && dist_max != INFINITE_LEN;
set.anychar_inf = (anchor & ANCR_ANYCHAR_INF) != 0;
} else {
let new_anchor = set.anchor & anchor;
if new_anchor != 0 {
if anc_dist_min < set.anc_dmin {
set.anc_dmin = anc_dist_min;
}
if anc_dist_max > set.anc_dmax {
set.anc_dmax = anc_dist_max;
}
}
set.anchor = new_anchor;
if optimize == OptimizeType::None || dist_max == INFINITE_LEN {
set.all_low_high = false;
}
if (anchor & ANCR_ANYCHAR_INF) != 0 {
set.anychar_inf = true;
}
}
ONIG_NORMAL
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn onig_regset_replace(set: &mut OnigRegSet, at: usize, reg: Option<Box<RegexType>>) -> i32 {
if at >= set.entries.len() {
return ONIGERR_INVALID_ARGUMENT;
}
match reg {
None => {
set.entries.remove(at);
}
Some(reg) => {
if opton_find_longest(reg.options) {
return ONIGERR_INVALID_ARGUMENT;
}
if set.entries.len() > 1 && !std::ptr::eq(reg.enc, set.enc) {
return ONIGERR_INVALID_ARGUMENT;
}
set.entries[at].fallback_memo_safe = fallback_memo_is_safe(®);
set.entries[at].required_literals_safe = required_literals_are_safe(®);
set.entries[at].has_callouts =
reg.extp.as_ref().is_some_and(|ext| ext.callout_num != 0);
set.entries[at].start_filter = fallback_start_filter(®);
set.entries[at].first_op = first_op_test(®);
set.entries[at].reg = Arc::from(reg);
}
}
if !set.entries.is_empty() {
let first_enc = set.entries[0].reg.enc;
set.enc = first_enc;
set.anchor = 0;
set.anc_dmin = 0;
set.anc_dmax = 0;
set.all_low_high = false;
set.anychar_inf = false;
let reg_data: Vec<(i32, OnigLen, OnigLen, OptimizeType, OnigLen, i32)> = set
.entries
.iter()
.map(|e| {
(
e.reg.anchor,
e.reg.anc_dist_min,
e.reg.anc_dist_max,
e.reg.optimize,
e.reg.dist_max,
0, )
})
.collect();
for (i, (anchor, anc_dist_min, anc_dist_max, optimize, dist_max, _)) in
reg_data.iter().enumerate()
{
if i == 0 {
set.anchor = *anchor;
set.anc_dmin = *anc_dist_min;
set.anc_dmax = *anc_dist_max;
set.all_low_high = *optimize != OptimizeType::None && *dist_max != INFINITE_LEN;
set.anychar_inf = (*anchor & ANCR_ANYCHAR_INF) != 0;
} else {
let new_anchor = set.anchor & anchor;
if new_anchor != 0 {
if *anc_dist_min < set.anc_dmin {
set.anc_dmin = *anc_dist_min;
}
if *anc_dist_max > set.anc_dmax {
set.anc_dmax = *anc_dist_max;
}
}
set.anchor = new_anchor;
if *optimize == OptimizeType::None || *dist_max == INFINITE_LEN {
set.all_low_high = false;
}
if (*anchor & ANCR_ANYCHAR_INF) != 0 {
set.anychar_inf = true;
}
}
}
}
build_first_byte_table(set);
ONIG_NORMAL
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn onig_regset_number_of_regex(set: &OnigRegSet) -> i32 {
set.entries.len() as i32
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn onig_regset_get_regex(set: &OnigRegSet, at: usize) -> Option<&RegexType> {
set.entries.get(at).map(|e| e.reg.as_ref())
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn onig_regset_get_region(set: &OnigRegSet, at: usize) -> Option<&OnigRegion> {
set.entries.get(at).and_then(|e| e.region.as_ref())
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn onig_regset_last_match_len(set: &OnigRegSet) -> i32 {
set.last_match_len
}
#[derive(Clone, Copy)]
struct RegSetWinner {
index: i32,
position: i32,
match_len: i32,
}
#[derive(Clone, Copy)]
struct RegSetError {
code: i32,
index: i32,
position: i32,
}
#[derive(Clone, Copy)]
enum RegSetDecision {
Match(RegSetWinner),
Error(RegSetError),
}
#[inline]
fn decision_position_and_index(decision: RegSetDecision) -> (i32, i32) {
match decision {
RegSetDecision::Match(candidate) => (candidate.position, candidate.index),
RegSetDecision::Error(error) => (error.position, error.index),
}
}
#[inline]
fn decision_is_better(candidate: RegSetDecision, current: Option<RegSetDecision>) -> bool {
current.is_none_or(|current| {
decision_position_and_index(candidate) < decision_position_and_index(current)
})
}
fn clear_regset_entry_region(set: &mut OnigRegSet, index: i32) {
if let Some(region) = set.entries[index as usize].region.as_mut() {
region.clear();
}
}
fn record_regset_decision(
set: &mut OnigRegSet,
current: &mut Option<RegSetDecision>,
candidate: RegSetDecision,
) {
if decision_is_better(candidate, *current) {
if let Some(RegSetDecision::Match(previous)) = current {
clear_regset_entry_region(set, previous.index);
}
*current = Some(candidate);
} else if let RegSetDecision::Match(candidate) = candidate {
clear_regset_entry_region(set, candidate.index);
}
}
#[inline]
fn region_is_redundant(reg: &RegexType) -> bool {
reg.num_mem == 0 && !reg.keep_moves_match_start
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EntryRegion {
Fill,
FillOnMatch,
}
impl EntryRegion {
fn of(reg: &RegexType, option: OnigOptionType, msa: &MatchArg) -> Self {
if reg.num_mem > 0
&& !reg.needs_capture_tracking
&& reg.extp.as_ref().is_none_or(|ext| ext.callout_num == 0)
&& msa.time_limit == 0
&& !opton_find_longest(option | reg.options)
{
EntryRegion::FillOnMatch
} else {
EntryRegion::Fill
}
}
}
#[allow(clippy::too_many_arguments)]
fn attempt_entry_match(
entry: &mut RegSetEntry,
text: &[u8],
end: usize,
at: usize,
start: usize,
option: OnigOptionType,
msa: &mut MatchArg,
) -> i32 {
onig_match_with_msa_start(&entry.reg, text, end, at, start, option, msa)
}
#[inline(always)]
#[allow(clippy::too_many_arguments)]
fn first_op_rejects(
entry: &RegSetEntry,
skips: bool,
str_data: &[u8],
end: usize,
position: usize,
search_start: usize,
option: OnigOptionType,
msa: &mut MatchArg,
) -> bool {
let Some(test) = entry.first_op.filter(|_| skips) else {
return false;
};
if !first_op_fails(test, &entry.reg, str_data, end, position, option) {
return false;
}
msa.reset_for_match(&entry.reg, option, search_start);
msa.retry_limit_in_search_counter += 1;
#[cfg(test)]
FIRST_OP_REJECTS.with(|rejects| rejects.set(rejects.get() + 1));
true
}
#[cfg(test)]
thread_local! {
static FIRST_OP_REJECTS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[allow(clippy::too_many_arguments)]
fn attempt_fallback_entry(
entry: &mut RegSetEntry,
str_data: &[u8],
end: usize,
position: usize,
search_start: usize,
option: OnigOptionType,
fill: EntryRegion,
msa: &mut MatchArg,
) -> i32 {
if fill == EntryRegion::Fill {
msa.region = entry.region.take();
let result = attempt_entry_match(entry, str_data, end, position, search_start, option, msa);
entry.region = msa.region.take();
return result;
}
msa.region = None;
let result = attempt_entry_match(entry, str_data, end, position, search_start, option, msa);
if result < 0 {
return result;
}
let limits = (
msa.retry_limit_in_match,
msa.retry_limit_in_search,
msa.retry_limit_in_search_counter,
);
msa.retry_limit_in_match = 0;
msa.retry_limit_in_search = 0;
msa.region = entry.region.take();
let captured = attempt_entry_match(entry, str_data, end, position, search_start, option, msa);
entry.region = msa.region.take();
(
msa.retry_limit_in_match,
msa.retry_limit_in_search,
msa.retry_limit_in_search_counter,
) = limits;
if captured >= 0 {
return captured;
}
msa.region = entry.region.take();
let result = attempt_entry_match(entry, str_data, end, position, search_start, option, msa);
entry.region = msa.region.take();
result
}
#[derive(Clone, Copy)]
enum EntrySearchRange {
AllRange { until: usize },
LowHigh {
low: usize,
high: usize,
sch_range: usize,
},
}
fn entry_search_range(
reg: &RegexType,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
) -> Option<EntrySearchRange> {
if reg.optimize == OptimizeType::None {
return Some(EntrySearchRange::AllRange { until: usize::MAX });
}
if reg.dist_max != INFINITE_LEN {
let sch_range = if end - range > reg.dist_max as usize {
range + reg.dist_max as usize
} else {
end
};
let (low, high) = forward_search(reg, str_data, end, start, sch_range)?;
Some(EntrySearchRange::LowHigh {
low,
high,
sch_range,
})
} else {
let (_, until) = forward_search(reg, str_data, end, start, end)?;
Some(EntrySearchRange::AllRange { until })
}
}
#[derive(Clone, Copy)]
struct EntryGate {
generation: u32,
from: usize,
range: Option<EntrySearchRange>,
}
impl EntryGate {
const STALE: EntryGate = EntryGate {
generation: 0,
from: 0,
range: None,
};
#[inline]
fn serves(&self, generation: u32, start: usize) -> bool {
self.generation == generation
&& self.from <= start
&& start
<= match self.range {
None => usize::MAX,
Some(EntrySearchRange::AllRange { until }) => until,
Some(EntrySearchRange::LowHigh { high, .. }) => high,
}
}
}
fn fallback_attempt_decision(
result: i32,
index: usize,
position: usize,
msa: &MatchArg,
) -> Option<RegSetDecision> {
if result >= 0 {
return Some(RegSetDecision::Match(RegSetWinner {
index: index as i32,
position: position as i32,
match_len: result,
}));
}
let code = if result != ONIG_MISMATCH {
result
} else if msa.retry_limit_in_search != 0
&& msa.retry_limit_in_search_counter >= msa.retry_limit_in_search
{
ONIGERR_RETRY_LIMIT_IN_SEARCH_OVER
} else {
return None;
};
Some(RegSetDecision::Error(RegSetError {
code,
index: index as i32,
position: position as i32,
}))
}
#[allow(clippy::too_many_arguments)]
#[inline(never)]
fn attempt_fallback_entry_at_start(
entry: &mut RegSetEntry,
index: usize,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
option: OnigOptionType,
msa: &mut MatchArg,
) -> Option<RegSetDecision> {
if msa.retry_limit_in_search == 0
&& start < end
&& entry
.start_filter
.as_deref()
.is_some_and(|filter| filter[str_data[start] as usize] == 0)
{
return None;
}
if required_literals(entry, option, msa)
.is_some_and(|required| required.find(str_data, start, end).is_none())
{
return None;
}
let fill = EntryRegion::of(&entry.reg, option, msa);
msa.retry_limit_in_search_counter = 0;
let skips = may_skip_first_op_failures(msa, option);
if first_op_rejects(entry, skips, str_data, end, start, start, option, msa) {
return None;
}
let result = attempt_fallback_entry(entry, str_data, end, start, start, option, fill, msa);
let decision = fallback_attempt_decision(result, index, start, msa)?;
let admitted = match entry_search_range(&entry.reg, str_data, end, start, range) {
None => false,
Some(EntrySearchRange::AllRange { .. }) => true,
Some(EntrySearchRange::LowHigh { low, .. }) => start >= low,
};
if admitted {
Some(decision)
} else {
if let Some(region) = entry.region.as_mut() {
region.clear();
}
None
}
}
#[allow(clippy::too_many_arguments)]
#[inline(never)]
fn search_fallback_entry(
set: &mut OnigRegSet,
index: usize,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
stop: usize,
option: OnigOptionType,
msa: &mut MatchArg,
) -> Option<RegSetDecision> {
let enc = set.enc;
let anychar_inf = set.anychar_inf;
let entry = &mut set.entries[index];
if stop == start
&& entry
.reg
.extp
.as_ref()
.is_none_or(|ext| ext.callout_num == 0)
{
return attempt_fallback_entry_at_start(
entry, index, str_data, end, start, range, option, msa,
);
}
let mut required_hit = match required_literals(entry, option, msa) {
Some(required) => Some(required.find(str_data, start, end)?),
None => None,
};
let mut search_range = entry_search_range(&entry.reg, str_data, end, start, range)?;
let after_newline_only = anychar_inf && (entry.reg.anchor & ANCR_ANYCHAR_INF) != 0;
let fill = EntryRegion::of(&entry.reg, option, msa);
let has_start_filter = entry.start_filter.is_some() && msa.retry_limit_in_search == 0;
let first_op_skips = may_skip_first_op_failures(msa, option);
msa.retry_limit_in_search_counter = 0;
let mut s = start;
loop {
if s > stop {
return None;
}
if required_hit.is_some_and(|hit| s > hit) {
required_hit = Some(next_required_literal(&entry.reg, str_data, s, end)?);
}
let admitted = match search_range {
EntrySearchRange::LowHigh {
low,
high,
sch_range,
} => {
if s < low {
s = low;
continue;
}
if s >= high {
let (low, high) = forward_search(&entry.reg, str_data, end, s, sch_range)?;
search_range = EntrySearchRange::LowHigh {
low,
high,
sch_range,
};
if s < low {
s = low;
continue;
}
}
true
}
EntrySearchRange::AllRange { .. } => {
!has_start_filter
|| s >= end
|| entry
.start_filter
.as_deref()
.is_none_or(|filter| filter[str_data[s] as usize] != 0)
}
} && !(after_newline_only && s > start && str_data[s - 1] != b'\n');
if admitted
&& !first_op_rejects(entry, first_op_skips, str_data, end, s, start, option, msa)
{
let result = attempt_fallback_entry(entry, str_data, end, s, start, option, fill, msa);
if let Some(decision) = fallback_attempt_decision(result, index, s, msa) {
return Some(decision);
}
}
if s >= stop || s >= end {
return None;
}
s = if admitted
&& !after_newline_only
&& matches!(search_range, EntrySearchRange::AllRange { .. })
&& entry.reg.leading_run.is_some()
{
crate::regexec::after_failed_run(
&entry.reg,
opton_find_longest(option | entry.reg.options),
msa,
str_data,
end,
end,
end,
s,
)
} else {
(s + enclen(enc, str_data, s)).min(end)
};
}
}
#[inline]
fn regset_decision_result(set: &mut OnigRegSet, decision: Option<RegSetDecision>) -> (i32, i32) {
match decision {
Some(RegSetDecision::Match(winner)) => {
set.last_match_len = winner.match_len;
(winner.index, winner.position)
}
Some(RegSetDecision::Error(error)) => {
set.last_match_len = ONIG_MISMATCH;
(error.code, 0)
}
None => {
set.last_match_len = ONIG_MISMATCH;
(ONIG_MISMATCH, 0)
}
}
}
#[inline]
fn regset_search_body_position_lead_table(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
option: OnigOptionType,
skip_region_for_nomem: bool,
) -> Option<RegSetDecision> {
if table_scan_gates_eagerly(set) {
regset_table_scan::<true>(
set,
str_data,
end,
start,
range,
start,
range,
option,
skip_region_for_nomem,
)
.0
} else {
regset_table_scan::<false>(
set,
str_data,
end,
start,
range,
start,
range,
option,
skip_region_for_nomem,
)
.0
}
}
#[inline]
fn table_scan_gates_eagerly(set: &OnigRegSet) -> bool {
set.has_gated
&& (set.has_gated_callouts
|| set
.scratch_limits
.is_some_and(|limits| limits.retry_limit_in_search != 0))
}
fn table_gate_admits(
set: &mut OnigRegSet,
index: usize,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
s: usize,
) -> bool {
let reg = &*set.entries[index].reg;
let gate = &mut set.gates[index];
if !gate.serves(set.gate_generation, start) {
*gate = EntryGate {
generation: set.gate_generation,
from: start,
range: entry_search_range(reg, str_data, end, start, range),
};
}
entry_range_admits(reg, str_data, end, &mut gate.range, &mut gate.from, s)
}
#[inline]
fn entry_range_admits(
reg: &RegexType,
str_data: &[u8],
end: usize,
search_range: &mut Option<EntrySearchRange>,
searched_from: &mut usize,
s: usize,
) -> bool {
match *search_range {
None => false,
Some(EntrySearchRange::AllRange { .. }) => true,
Some(EntrySearchRange::LowHigh {
low,
high,
sch_range,
}) => {
if s < low {
return false;
}
if s < high {
return true;
}
*searched_from = s;
match forward_search(reg, str_data, end, s, sch_range) {
Some((low, high)) => {
*search_range = Some(EntrySearchRange::LowHigh {
low,
high,
sch_range,
});
s >= low
}
None => {
*search_range = None;
false
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
#[inline(always)]
fn regset_table_scan<const EAGER_GATES: bool>(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
search_range: usize,
from: usize,
to: usize,
option: OnigOptionType,
skip_region_for_nomem: bool,
) -> (Option<RegSetDecision>, usize) {
if set.table_entry_count == 0 {
return (None, to.saturating_add(1));
}
let enc = set.enc;
let mut s = from;
let range = to;
let mut msa = if let Some(msa) = set.scratch_msa.take() {
msa
} else {
let first_reg = &*set.entries[0].reg;
MatchArg::new(first_reg, option, None, start)
};
let track_search_retry_limit = msa.retry_limit_in_search != 0;
if track_search_retry_limit && from == start {
if set.scratch_table_retry_counters.len() != set.entries.len() {
set.scratch_table_retry_counters
.resize(set.entries.len(), 0);
}
set.scratch_table_retry_counters.fill(0);
}
let prev_is_newline_check = set.anychar_inf;
let first_op_skips = may_skip_first_op_failures(&msa, option);
let mut result = None;
'search: loop {
if s > range {
break;
}
if s < range {
s = match set.skip_needle {
SkipNeedle::None => s,
SkipNeedle::One(b) => {
memchr::memchr(b, &str_data[s..range]).map_or(range, |off| s + off)
}
SkipNeedle::Two(b1, b2) => {
memchr::memchr2(b1, b2, &str_data[s..range]).map_or(range, |off| s + off)
}
SkipNeedle::Three(b1, b2, b3) => {
memchr::memchr3(b1, b2, b3, &str_data[s..range]).map_or(range, |off| s + off)
}
};
}
#[cfg(test)]
{
set.table_positions_scanned += 1;
}
let prev_is_newline = if prev_is_newline_check && s > start {
str_data[s - 1] == b'\n'
} else {
true };
let remaining = end - s;
let at_end = s == end;
let candidate_count = if at_end {
set.table_entries.len()
} else {
set.first_byte_candidates[str_data[s] as usize].len()
};
for candidate_at in 0..candidate_count {
let i = if at_end {
set.table_entries[candidate_at] as usize
} else {
set.first_byte_candidates[str_data[s] as usize][candidate_at] as usize
};
if (set.entries[i].reg.anchor & ANCR_ANYCHAR_INF) != 0 && !prev_is_newline {
continue;
}
if set.entries[i].reg.threshold_len > 0
&& remaining < set.entries[i].reg.threshold_len as usize
{
continue;
}
if EAGER_GATES
&& set.entries[i].gated
&& !table_gate_admits(set, i, str_data, end, start, search_range, s)
{
continue;
}
if track_search_retry_limit {
msa.retry_limit_in_search_counter = set.scratch_table_retry_counters[i];
}
if first_op_rejects(
&set.entries[i],
first_op_skips,
str_data,
end,
s,
start,
option,
&mut msa,
) {
continue;
}
let r = if skip_region_for_nomem && region_is_redundant(&set.entries[i].reg) {
msa.region = None;
attempt_entry_match(
&mut set.entries[i],
str_data,
end,
s,
start,
option,
&mut msa,
)
} else {
msa.region = set.entries[i].region.take();
let r = attempt_entry_match(
&mut set.entries[i],
str_data,
end,
s,
start,
option,
&mut msa,
);
set.entries[i].region = msa.region.take();
r
};
if track_search_retry_limit {
set.scratch_table_retry_counters[i] = msa.retry_limit_in_search_counter;
}
if !EAGER_GATES
&& r != ONIG_MISMATCH
&& set.entries[i].gated
&& !table_gate_admits(set, i, str_data, end, start, search_range, s)
{
if r >= 0 {
clear_regset_entry_region(set, i as i32);
}
continue;
}
if r >= 0 {
result = Some(RegSetDecision::Match(RegSetWinner {
index: i as i32,
position: s as i32,
match_len: r,
}));
break 'search;
}
if r != ONIG_MISMATCH {
result = Some(RegSetDecision::Error(RegSetError {
code: r,
index: i as i32,
position: s as i32,
}));
break 'search;
}
if track_search_retry_limit
&& set.scratch_table_retry_counters[i] > msa.retry_limit_in_search
{
result = Some(RegSetDecision::Error(RegSetError {
code: ONIGERR_RETRY_LIMIT_IN_SEARCH_OVER,
index: i as i32,
position: s as i32,
}));
break 'search;
}
}
if s >= range {
s = s.saturating_add(enclen(enc, str_data, s));
break;
}
s += enclen(enc, str_data, s);
}
set.scratch_msa = Some(msa);
(result, s)
}
#[inline(never)]
fn take_scratch_msa(set: &mut OnigRegSet, option: OnigOptionType, start: usize) -> MatchArg {
set.scratch_msa
.take()
.unwrap_or_else(|| MatchArg::new(&set.entries[0].reg, option, None, start))
}
#[inline]
fn refresh_scratch_limits(set: &mut OnigRegSet) -> FallbackMemoLimits {
let limit_revision = onig_get_global_limit_revision();
if set.scratch_limits_revision != Some(limit_revision) {
set.scratch_limits = Some(FallbackMemoLimits::current());
set.scratch_limits_revision = Some(limit_revision);
set.scratch_msa = None;
}
let limits = set
.scratch_limits
.expect("global limit revision always initializes cached limits");
if limits.time_limit != 0 {
set.scratch_msa = None;
}
limits
}
#[allow(clippy::too_many_arguments)]
fn search_table_entry(
set: &mut OnigRegSet,
index: usize,
start_bytes: &[u64; 4],
str_data: &[u8],
end: usize,
start: usize,
stop: usize,
option: OnigOptionType,
msa: &mut MatchArg,
) -> Option<RegSetDecision> {
let enc = set.enc;
let prev_is_newline_check = set.anychar_inf;
let entry = &mut set.entries[index];
let after_newline_only = (entry.reg.anchor & ANCR_ANYCHAR_INF) != 0;
let threshold_len = entry.reg.threshold_len.max(0) as usize;
let gate_first = entry.gated && (msa.retry_limit_in_search != 0 || entry.has_callouts);
let mut gate_from = start;
let mut gate: Option<Option<EntrySearchRange>> = None;
msa.retry_limit_in_search_counter = 0;
let mut s = start;
loop {
let mut admitted = (s == end
|| (start_bytes[str_data[s] as usize / 64] >> (str_data[s] % 64)) & 1 != 0)
&& !(after_newline_only
&& prev_is_newline_check
&& s > start
&& str_data[s - 1] != b'\n')
&& end - s >= threshold_len;
if admitted && gate_first {
let range = gate
.get_or_insert_with(|| entry_search_range(&entry.reg, str_data, end, start, end));
admitted = entry_range_admits(&entry.reg, str_data, end, range, &mut gate_from, s);
}
if admitted {
let result = attempt_fallback_entry(
entry,
str_data,
end,
s,
start,
option,
EntryRegion::Fill,
msa,
);
let decision = if result >= 0 {
Some(RegSetDecision::Match(RegSetWinner {
index: index as i32,
position: s as i32,
match_len: result,
}))
} else if result != ONIG_MISMATCH {
Some(RegSetDecision::Error(RegSetError {
code: result,
index: index as i32,
position: s as i32,
}))
} else if msa.retry_limit_in_search != 0
&& msa.retry_limit_in_search_counter > msa.retry_limit_in_search
{
Some(RegSetDecision::Error(RegSetError {
code: ONIGERR_RETRY_LIMIT_IN_SEARCH_OVER,
index: index as i32,
position: s as i32,
}))
} else {
None
};
if let Some(decision) = decision {
let admits = !entry.gated || gate_first || {
let range = gate.get_or_insert_with(|| {
entry_search_range(&entry.reg, str_data, end, start, end)
});
entry_range_admits(&entry.reg, str_data, end, range, &mut gate_from, s)
};
if admits {
return Some(decision);
}
if let Some(region) = entry.region.as_mut() {
region.clear();
}
}
}
if s >= stop || s >= end {
return None;
}
s = (s + enclen(enc, str_data, s)).min(end);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RegSetEntryEvent {
None,
Match { position: usize },
Error { position: usize, code: i32 },
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn onig_regset_entry_search(
set: &mut OnigRegSet,
index: usize,
str_data: &[u8],
end: usize,
start: usize,
stop: usize,
option: OnigOptionType,
) -> RegSetEntryEvent {
let end = end.min(str_data.len());
if start > end {
return RegSetEntryEvent::None;
}
let stop = stop.min(end);
refresh_scratch_limits(set);
let mut msa = set
.scratch_msa
.take()
.unwrap_or_else(|| MatchArg::new(&set.entries[0].reg, option, None, start));
let decision = if set.entries[index].fallback {
search_fallback_entry(
set, index, str_data, end, start, end, stop, option, &mut msa,
)
} else {
let start_bytes = table_start_bytes(set, index);
search_table_entry(
set,
index,
&start_bytes,
str_data,
end,
start,
stop,
option,
&mut msa,
)
};
set.scratch_msa = Some(msa);
match decision {
None => RegSetEntryEvent::None,
Some(RegSetDecision::Match(winner)) => RegSetEntryEvent::Match {
position: winner.position as usize,
},
Some(RegSetDecision::Error(error)) => RegSetEntryEvent::Error {
position: error.position as usize,
code: error.code,
},
}
}
fn table_start_bytes(set: &mut OnigRegSet, index: usize) -> [u64; 4] {
let n = set.entries.len();
let table = &set.first_byte_candidates;
let bytes = set.table_start_bytes.get_or_insert_with(|| {
let mut bytes = vec![Some([0u64; 4]); n];
for (byte, slot) in table.iter().enumerate() {
for &i in slot {
if let Some(bits) = bytes[i as usize].as_mut() {
bits[byte / 64] |= 1 << (byte % 64);
}
}
}
bytes
});
bytes[index].unwrap_or([0; 4])
}
pub(crate) fn onig_regset_swap_region(
set: &mut OnigRegSet,
index: usize,
region: &mut Option<OnigRegion>,
) {
std::mem::swap(&mut set.entries[index].region, region);
set.entries[index]
.region
.get_or_insert_with(OnigRegion::new);
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn regset_search_body_position_lead(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
option: OnigOptionType,
skip_region_for_nomem: bool,
fallback_memo_id: Option<FallbackMemoIdentity>,
) -> (i32, i32) {
let limits = refresh_scratch_limits(set);
if set.has_gated {
let subject = fallback_memo_id
.filter(|_| range == end)
.map(|identity| (identity, end));
if subject.is_none() || subject != set.gate_subject {
set.gate_subject = subject;
set.gate_generation = set.gate_generation.wrapping_add(1);
if set.gate_generation == 0 {
set.gates.fill(EntryGate::STALE);
set.gate_generation = 1;
}
}
}
if set.fallback_search_candidates.is_empty() {
let decision = regset_search_body_position_lead_table(
set,
str_data,
end,
start,
range,
option,
skip_region_for_nomem,
);
return regset_decision_result(set, decision);
}
let memo_enabled =
fallback_memo_id.is_some() && range == end && limits.retry_limit_in_search == 0;
if let Some(identity) = fallback_memo_id.filter(|_| memo_enabled) {
let key = FallbackMemoKey {
identity,
end,
option,
retry_limit_in_match: limits.retry_limit_in_match,
retry_limit_in_search: limits.retry_limit_in_search,
match_stack_limit: limits.match_stack_limit,
time_limit: limits.time_limit,
};
if set.fallback_memo_key != Some(key) {
set.fallback_memo_key = Some(key);
for memos in &mut set.fallback_memos {
memos.clear();
}
for candidate in &mut set.fallback_search_candidates {
candidate.no_match_from = usize::MAX;
candidate.exact_miss = usize::MAX;
candidate.match_from = usize::MAX;
candidate.match_at = usize::MAX;
}
}
}
const FIRST_WINDOW: usize = 256;
let mut from = start;
let mut to = if range - start > FIRST_WINDOW {
start + FIRST_WINDOW
} else {
range
};
let mut fallback_bound: Option<RegSetDecision> = None;
let decision = loop {
let bound_position =
fallback_bound.map(|found| decision_position_and_index(found).0 as usize);
let scan_to = bound_position.map_or(to, |position| to.min(position));
let (table, next) = if table_scan_gates_eagerly(set) {
regset_table_scan::<true>(
set,
str_data,
end,
start,
range,
from,
scan_to,
option,
skip_region_for_nomem,
)
} else {
regset_table_scan::<false>(
set,
str_data,
end,
start,
range,
from,
scan_to,
option,
skip_region_for_nomem,
)
};
if table.is_some()
|| scan_to >= range
|| bound_position.is_some_and(|position| next > position)
{
break search_fallback_entries(
set,
str_data,
end,
start,
range,
range,
option,
memo_enabled,
table.or(fallback_bound),
);
}
if fallback_bound.is_none() {
fallback_bound = search_fallback_entries(
set,
str_data,
end,
start,
range,
to,
option,
memo_enabled,
None,
);
if let Some(found) = fallback_bound {
if decision_position_and_index(found).0 as usize <= to {
break Some(found);
}
}
}
from = next;
to = range.min(start + (to - start).saturating_mul(4));
};
regset_decision_result(set, decision)
}
#[allow(clippy::too_many_arguments)]
#[inline(always)]
fn search_fallback_entries(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
limit: usize,
option: OnigOptionType,
memo_enabled: bool,
mut decision: Option<RegSetDecision>,
) -> Option<RegSetDecision> {
let mut fallback_msa: Option<MatchArg> = None;
let mut before_decision: Option<(usize, Option<usize>)> = None;
for candidate_at in 0..set.fallback_search_candidates.len() {
let candidate = set.fallback_search_candidates[candidate_at];
let index = candidate.index as usize;
let bound = decision.map(decision_position_and_index);
if bound
.is_some_and(|(position, winner)| position as usize <= start && index as i32 >= winner)
{
break;
}
let memo_enabled = memo_enabled && candidate.memo_safe;
let memo_reusable = memo_enabled
&& !(candidate.after_newline_only && start > 0 && str_data[start - 1] != b'\n');
if memo_reusable && start >= candidate.no_match_from {
continue;
}
macro_rules! fallback_msa {
() => {
fallback_msa.get_or_insert_with(|| take_scratch_msa(set, option, start))
};
}
if candidate.begin_position {
if bound.is_some_and(|bound| (start as i32, index as i32) >= bound) {
continue;
}
let msa = fallback_msa!();
if let Some(candidate) =
search_fallback_entry(set, index, str_data, end, start, range, start, option, msa)
{
record_regset_decision(set, &mut decision, candidate);
}
continue;
}
let search_range = match bound {
Some((position, winner)) if index as i32 >= winner => {
let position = position as usize;
let before = match before_decision {
Some((cached, before)) if cached == position => before,
_ => {
let before = onigenc_get_prev_char_head(set.enc, start, position, str_data);
before_decision = Some((position, before));
before
}
};
match before {
Some(position) => position,
None => continue,
}
}
Some((position, _)) => position as usize,
None => limit,
};
if search_range < start {
continue;
}
if memo_reusable {
let newest_match = (candidate.match_from <= start && start <= candidate.match_at)
.then_some((candidate.match_from, candidate.match_at));
if newest_match.is_none() && candidate.exact_miss == start {
continue;
}
if let Some((searched_from, position)) = newest_match.or_else(|| {
set.fallback_memos[index]
.iter()
.filter_map(|memo| match *memo {
FallbackMemo::MatchAt {
searched_from,
position,
} if start >= searched_from && position >= start => {
Some((searched_from, position))
}
_ => None,
})
.min_by_key(|(_, position)| *position)
}) {
if position > search_range
|| decision.is_some_and(|current| {
(position as i32, index as i32) >= decision_position_and_index(current)
})
{
continue;
}
let msa = fallback_msa!();
let result = attempt_fallback_entry(
&mut set.entries[index],
str_data,
end,
position,
start,
option,
EntryRegion::Fill,
msa,
);
if result >= 0 {
record_regset_decision(
set,
&mut decision,
RegSetDecision::Match(RegSetWinner {
index: index as i32,
position: position as i32,
match_len: result,
}),
);
continue;
}
set.fallback_memos[index].retain(|memo| {
!matches!(memo, FallbackMemo::MatchAt { searched_from: from, .. } if *from == searched_from)
});
let candidate = &mut set.fallback_search_candidates[candidate_at];
if candidate.match_from == searched_from {
candidate.match_from = usize::MAX;
candidate.match_at = usize::MAX;
}
} else if set.fallback_memos[index]
.iter()
.any(|memo| matches!(memo, FallbackMemo::ExactStartMiss(miss) if *miss == start))
{
continue;
}
}
let has_different_exact_start_miss = memo_enabled
&& set.fallback_memos[index].iter().any(|memo| {
matches!(memo, FallbackMemo::ExactStartMiss(exact_start) if *exact_start != start)
});
let msa = fallback_msa!();
if search_range == start && !has_different_exact_start_miss {
let found =
search_fallback_entry(set, index, str_data, end, start, range, start, option, msa);
if let Some(candidate) = found {
record_regset_decision(set, &mut decision, candidate);
} else if memo_enabled {
let memos = &mut set.fallback_memos[index];
if memos.len() == FALLBACK_MEMO_CAPACITY {
memos.remove(0);
}
memos.push(FallbackMemo::ExactStartMiss(start));
set.fallback_search_candidates[candidate_at].exact_miss = start;
}
continue;
}
let found = search_fallback_entry(
set,
index,
str_data,
end,
start,
range,
if memo_enabled { range } else { search_range },
option,
msa,
);
match found {
Some(RegSetDecision::Match(winner)) => {
if memo_enabled {
let memos = &mut set.fallback_memos[index];
memos.retain(|memo| {
!matches!(memo, FallbackMemo::ExactStartMiss(_))
&& !matches!(memo, FallbackMemo::MatchAt { searched_from, .. } if *searched_from == start)
});
if memos.len() == FALLBACK_MEMO_CAPACITY {
memos.remove(0);
}
memos.push(FallbackMemo::MatchAt {
searched_from: start,
position: winner.position as usize,
});
let candidate = &mut set.fallback_search_candidates[candidate_at];
candidate.exact_miss = usize::MAX;
candidate.match_from = start;
candidate.match_at = winner.position as usize;
}
record_regset_decision(set, &mut decision, RegSetDecision::Match(winner));
}
Some(error @ RegSetDecision::Error(_)) => {
record_regset_decision(set, &mut decision, error);
}
None => {
if memo_enabled {
set.fallback_memos[index].clear();
let candidate = &mut set.fallback_search_candidates[candidate_at];
candidate.no_match_from = start;
candidate.exact_miss = usize::MAX;
candidate.match_from = usize::MAX;
candidate.match_at = usize::MAX;
}
}
}
}
if fallback_msa.is_some() {
set.scratch_msa = fallback_msa;
}
decision
}
fn regset_search_body_regex_lead(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
orig_range: usize,
lead: OnigRegSetLead,
option: OnigOptionType,
) -> (i32, i32) {
let n = set.entries.len();
let mut match_index: i32 = ONIG_MISMATCH;
let mut match_pos: i32 = 0;
let mut ep = orig_range;
for i in 0..n {
let region = set.entries[i].region.take();
let (r, returned_region) = search_in_range(
&set.entries[i].reg,
str_data,
end,
start,
ep,
orig_range,
region,
option,
None,
);
set.entries[i].region = returned_region;
if r > 0 {
if (r as usize) < ep {
match_index = i as i32;
match_pos = r;
if lead == OnigRegSetLead::PriorityToRegexOrder {
break;
}
ep = r as usize;
}
} else if r == 0 {
match_index = i as i32;
match_pos = 0;
break;
}
}
(match_index, match_pos)
}
#[allow(clippy::too_many_arguments)]
fn onig_regset_search_impl(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
lead: OnigRegSetLead,
option: OnigOptionType,
eager_region_reset: bool,
fallback_memo_id: Option<FallbackMemoIdentity>,
) -> (i32, i32) {
let n = set.entries.len();
if n == 0 {
return (ONIG_MISMATCH, 0);
}
set.last_match_len = ONIG_MISMATCH;
let end = end.min(str_data.len());
let range = range.min(end);
if start > end {
return (ONIG_MISMATCH, 0);
}
if !str_data.is_empty() && range < start {
return (ONIGERR_INVALID_ARGUMENT, 0);
}
if eager_region_reset {
for entry in &mut set.entries {
if let Some(ref mut region) = entry.region {
region.resize(entry.reg.num_mem + 1);
region.clear();
}
}
}
if end == 0 {
for i in 0..n {
if set.entries[i].reg.threshold_len == 0 {
let region = set.entries[i].region.take();
let (r, returned_region) =
onig_match(&set.entries[i].reg, str_data, end, start, region, option);
set.entries[i].region = returned_region;
if r >= 0 {
set.last_match_len = r;
return (i as i32, start as i32);
}
if r != ONIG_MISMATCH {
return (r, 0); }
}
}
return (ONIG_MISMATCH, 0);
}
let mut cur_start = start;
let mut cur_range = range;
let orig_range = range;
if set.anchor != 0 && !str_data.is_empty() {
if (set.anchor & ANCR_BEGIN_POSITION) != 0 {
cur_range = start + 1;
} else if (set.anchor & ANCR_BEGIN_BUF) != 0 {
if start != 0 {
return (ONIG_MISMATCH, 0);
}
cur_range = 1;
} else if (set.anchor & ANCR_END_BUF) != 0 {
let min_semi_end = end;
let max_semi_end = end;
if (max_semi_end as OnigLen) < set.anc_dmin {
return (ONIG_MISMATCH, 0);
}
if min_semi_end.saturating_sub(start) > set.anc_dmax as usize
&& set.anc_dmax != INFINITE_LEN
{
cur_start = min_semi_end - set.anc_dmax as usize;
}
if max_semi_end.saturating_sub(cur_range.saturating_sub(1)) < set.anc_dmin as usize {
cur_range = max_semi_end.saturating_sub(set.anc_dmin as usize) + 1;
}
if cur_start > cur_range {
return (ONIG_MISMATCH, 0);
}
} else if (set.anchor & ANCR_SEMI_END_BUF) != 0 {
let max_semi_end = end;
let mut min_semi_end = end;
if end > 0 && str_data[end - 1] == b'\n' {
min_semi_end = end - 1;
}
if (max_semi_end as OnigLen) < set.anc_dmin {
return (ONIG_MISMATCH, 0);
}
if min_semi_end.saturating_sub(start) > set.anc_dmax as usize
&& set.anc_dmax != INFINITE_LEN
{
cur_start = min_semi_end - set.anc_dmax as usize;
}
if max_semi_end.saturating_sub(cur_range.saturating_sub(1)) < set.anc_dmin as usize {
cur_range = max_semi_end.saturating_sub(set.anc_dmin as usize) + 1;
}
if cur_start > cur_range {
return (ONIG_MISMATCH, 0);
}
} else if (set.anchor & ANCR_ANYCHAR_INF_ML) != 0 {
cur_range = start + 1;
}
}
let cur_range = cur_range.min(end);
let (result, match_pos) = if lead == OnigRegSetLead::PositionLead {
regset_search_body_position_lead(
set,
str_data,
end,
cur_start,
cur_range,
option,
!eager_region_reset,
fallback_memo_id,
)
} else {
regset_search_body_regex_lead(set, str_data, end, cur_start, orig_range, lead, option)
};
if eager_region_reset {
if result >= 0 {
for i in 0..n {
if opton_find_not_empty(set.entries[i].reg.options) {
if let Some(ref mut region) = set.entries[i].region {
if (i as i32) != result {
region.clear();
}
}
}
}
}
}
(result, match_pos)
}
pub fn onig_regset_search(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
lead: OnigRegSetLead,
option: OnigOptionType,
) -> (i32, i32) {
onig_regset_search_impl(set, str_data, end, start, range, lead, option, true, None)
}
pub fn onig_regset_search_fast(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
lead: OnigRegSetLead,
option: OnigOptionType,
) -> (i32, i32) {
onig_regset_search_impl(set, str_data, end, start, range, lead, option, false, None)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn onig_regset_search_fast_with_id(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
lead: OnigRegSetLead,
option: OnigOptionType,
identity: FallbackMemoIdentity,
) -> (i32, i32) {
onig_regset_search_impl(
set,
str_data,
end,
start,
range,
lead,
option,
false,
Some(identity),
)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[allow(clippy::too_many_arguments)]
pub fn onig_regset_search_with_param(
set: &mut OnigRegSet,
str_data: &[u8],
end: usize,
start: usize,
range: usize,
lead: OnigRegSetLead,
option: OnigOptionType,
mps: &[OnigMatchParam],
) -> (i32, i32) {
let n = set.entries.len();
if n == 0 {
return (ONIG_MISMATCH, 0);
}
if mps.len() < n {
return (ONIGERR_INVALID_ARGUMENT, 0);
}
set.last_match_len = ONIG_MISMATCH;
let end = end.min(str_data.len());
let range = range.min(end);
if start > end {
return (ONIG_MISMATCH, 0);
}
if !str_data.is_empty() && range < start {
return (ONIGERR_INVALID_ARGUMENT, 0);
}
for entry in &mut set.entries {
if let Some(ref mut region) = entry.region {
region.resize(entry.reg.num_mem + 1);
region.clear();
}
}
if end == 0 {
for i in 0..n {
if set.entries[i].reg.threshold_len == 0 {
let region = set.entries[i].region.take();
let (r, returned_region) =
onig_match(&set.entries[i].reg, str_data, end, start, region, option);
set.entries[i].region = returned_region;
if r >= 0 {
set.last_match_len = r;
return (i as i32, start as i32);
}
if r != ONIG_MISMATCH {
return (r, 0);
}
}
}
return (ONIG_MISMATCH, 0);
}
if lead != OnigRegSetLead::PositionLead {
let orig_range = range;
let mut match_index: i32 = ONIG_MISMATCH;
let mut match_pos: i32 = 0;
let mut ep = orig_range;
for (i, entry) in set.entries.iter_mut().take(n).enumerate() {
let region = entry.region.take();
let (r, returned_region) = search_in_range(
&entry.reg,
str_data,
end,
start,
ep,
orig_range,
region,
option,
Some(&mps[i]),
);
entry.region = returned_region;
if r > 0 {
if (r as usize) < ep {
match_index = i as i32;
match_pos = r;
if lead == OnigRegSetLead::PriorityToRegexOrder {
break;
}
ep = r as usize;
}
} else if r == 0 {
match_index = i as i32;
match_pos = 0;
break;
}
}
return (match_index, match_pos);
}
regset_search_body_position_lead(set, str_data, end, start, range, option, false, None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encodings::utf8::ONIG_ENCODING_UTF8;
use crate::regcomp::onig_new;
use crate::regexec::onig_search;
use crate::regexec::{
LIMIT_TEST_LOCK, onig_get_global_limit_revision, onig_get_match_stack_limit,
onig_get_retry_limit_in_match, onig_get_retry_limit_in_search, onig_get_time_limit,
onig_set_match_stack_limit, onig_set_retry_limit_in_match, onig_set_retry_limit_in_search,
onig_set_time_limit,
};
use crate::regsyntax::OnigSyntaxOniguruma;
#[allow(dead_code)]
mod grammar_loader {
use crate as ferroni;
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/benches/grammar_loader.rs"
));
}
fn reg_mut(entry: &mut RegSetEntry) -> &mut RegexType {
Arc::get_mut(&mut entry.reg).expect("a test set owns its regexes")
}
fn fallback_indices(set: &OnigRegSet) -> Vec<u16> {
set.fallback_search_candidates
.iter()
.map(|candidate| candidate.index)
.collect()
}
fn assert_settled_from(set: &OnigRegSet, start: usize) {
assert_eq!(set.fallback_search_candidates[0].no_match_from, start);
assert!(set.fallback_memos[0].is_empty());
}
#[test]
fn grammar_fast_path_census() {
#[derive(Debug, PartialEq)]
struct Census {
patterns: usize,
table_entries: usize,
fallback_entries: usize,
fallback_start_filters: usize,
literal_tries: usize,
folded_literal_tries: usize,
without_optimizer: usize,
capture_tracking: usize,
fused_look_behinds: usize,
stepping_look_behinds: usize,
byte_set_push_guards: usize,
unguarded_pushes: usize,
}
let census = |patterns: Vec<String>| {
let regs: Vec<Box<RegexType>> =
patterns.iter().map(|p| compile(p.as_bytes())).collect();
crate::regcomp::PUSH_GUARDS_DISABLED.with(|disabled| disabled.set(true));
let unguarded: Vec<Box<RegexType>> =
patterns.iter().map(|p| compile(p.as_bytes())).collect();
crate::regcomp::PUSH_GUARDS_DISABLED.with(|disabled| disabled.set(false));
for ((pattern, reg), reference) in patterns.iter().zip(®s).zip(&unguarded) {
assert_eq!(
derive_start_byte_map(reg),
derive_start_byte_map(reference),
"{pattern}"
);
}
let tries = || regs.iter().flat_map(|reg| reg.literal_tries.iter());
let literal_tries = tries().count();
let folded_literal_tries = tries().filter(|trie| trie.is_case_insensitive()).count();
let without_optimizer = regs
.iter()
.filter(|reg| reg.optimize == OptimizeType::None)
.count();
let capture_tracking = regs.iter().filter(|reg| reg.needs_capture_tracking).count();
let ops = |opcode: OpCode| {
regs.iter()
.flat_map(|reg| reg.ops.iter())
.filter(|op| op.opcode == opcode)
.count()
};
let fused_look_behinds = ops(OpCode::LookBehindOp);
let stepping_look_behinds = ops(OpCode::StepBackStart);
let byte_set_push_guards = ops(OpCode::PushOrJumpByteSet);
let unguarded_pushes = ops(OpCode::Push);
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let set = set.unwrap();
Census {
patterns: patterns.len(),
table_entries: set.table_entry_count,
fallback_entries: set.fallback_search_candidates.len(),
fallback_start_filters: set
.fallback_search_candidates
.iter()
.filter(|c| set.entries[c.index as usize].start_filter.is_some())
.count(),
literal_tries,
folded_literal_tries,
without_optimizer,
capture_tracking,
fused_look_behinds,
stepping_look_behinds,
byte_set_push_guards,
unguarded_pushes,
}
};
assert_eq!(
census(grammar_loader::typescript_patterns()),
Census {
patterns: 279,
table_entries: 200,
fallback_entries: 79,
fallback_start_filters: 79,
literal_tries: 24,
folded_literal_tries: 0,
without_optimizer: 3,
capture_tracking: 0,
fused_look_behinds: 443,
stepping_look_behinds: 50,
byte_set_push_guards: 2164,
unguarded_pushes: 166,
}
);
assert_eq!(
census(grammar_loader::css_patterns()),
Census {
patterns: 117,
table_entries: 107,
fallback_entries: 10,
fallback_start_filters: 8,
literal_tries: 33,
folded_literal_tries: 33,
without_optimizer: 6,
capture_tracking: 0,
fused_look_behinds: 64,
stepping_look_behinds: 11,
byte_set_push_guards: 1275,
unguarded_pushes: 70,
}
);
assert_eq!(
census(grammar_loader::rust_patterns()),
Census {
patterns: 81,
table_entries: 78,
fallback_entries: 3,
fallback_start_filters: 2,
literal_tries: 8,
folded_literal_tries: 0,
without_optimizer: 0,
capture_tracking: 0,
fused_look_behinds: 6,
stepping_look_behinds: 2,
byte_set_push_guards: 28,
unguarded_pushes: 4,
}
);
}
fn compile(pattern: &[u8]) -> Box<RegexType> {
let reg = onig_new(
pattern,
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
);
match reg {
Ok(r) => Box::new(r),
Err(e) => panic!(
"failed to compile {:?}: error {}",
std::str::from_utf8(pattern),
e
),
}
}
#[test]
fn regset_basic_position_lead() {
let regs = vec![compile(b"abc"), compile(b"def"), compile(b"ghi")];
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"xxxdefyyy";
let (idx, pos) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!(idx, 1); assert_eq!(pos, 3); }
#[test]
fn start_byte_map_includes_optional_prefix_and_required_byte() {
let reg = compile(b"a?[bcd]");
assert!(has_finite_variable_optimizer(®));
let map = derive_start_byte_map(®).expect("simple optional prefix is analyzable");
for byte in *b"abcd" {
assert_ne!(map[byte as usize], 0, "missing byte {byte:?}");
}
assert_eq!(map[b'x' as usize], 0);
}
#[test]
fn start_byte_map_handles_compiled_consumer_and_control_flow_shapes() {
fn str1(byte: u8) -> Operation {
let mut exact = [0; 16];
exact[0] = byte;
Operation {
opcode: OpCode::Str1,
payload: OperationPayload::Exact { s: exact },
}
}
fn map_for(ops: Vec<Operation>, repeat_range: Vec<RepeatRange>) -> [u8; CHAR_MAP_SIZE] {
let mut reg = compile(b"a");
reg.ops = ops;
reg.repeat_range = repeat_range;
derive_start_byte_map(®).expect("analyzable bytecode")
}
let consumer_cases = [
Operation {
opcode: OpCode::StrN,
payload: OperationPayload::ExactN {
s: b"long literal".to_vec(),
n: 12,
},
},
Operation {
opcode: OpCode::StrMb2n1,
payload: OperationPayload::ExactLenN {
s: "é".as_bytes().to_vec(),
n: 2,
len: 1,
},
},
Operation {
opcode: OpCode::CClassMb,
payload: OperationPayload::CClassMb { mb: Vec::new() },
},
Operation {
opcode: OpCode::CClassMix,
payload: OperationPayload::CClassMix {
mb: Vec::new(),
bsp: Box::new([0; BITSET_REAL_SIZE]),
},
},
Operation {
opcode: OpCode::Word,
payload: OperationPayload::None,
},
Operation {
opcode: OpCode::WordAscii,
payload: OperationPayload::None,
},
Operation {
opcode: OpCode::AnyCharStar,
payload: OperationPayload::None,
},
];
for operation in consumer_cases {
assert!(
map_for(vec![operation], Vec::new())
.iter()
.any(|&value| value != 0)
);
}
for opcode in [
OpCode::Push,
OpCode::PushOrJumpExact1,
OpCode::PushIfPeekNext,
] {
let payload = match opcode {
OpCode::Push => OperationPayload::Push { addr: 2 },
OpCode::PushOrJumpExact1 => OperationPayload::PushOrJumpExact1 {
addr: 2,
c: b'a',
skipped_retries: 0,
},
_ => OperationPayload::PushIfPeekNext { addr: 2, c: b'a' },
};
let map = map_for(
vec![Operation { opcode, payload }, str1(b'a'), str1(b'b')],
Vec::new(),
);
assert_ne!(map[b'a' as usize], 0);
assert_ne!(map[b'b' as usize], 0);
}
let jump_map = map_for(
vec![
Operation {
opcode: OpCode::Jump,
payload: OperationPayload::Jump { addr: 1 },
},
str1(b'j'),
],
Vec::new(),
);
assert_ne!(jump_map[b'j' as usize], 0);
let repeat_map = map_for(
vec![
Operation {
opcode: OpCode::Repeat,
payload: OperationPayload::Repeat { id: 0, addr: 2 },
},
str1(b'r'),
str1(b's'),
],
vec![RepeatRange {
lower: 0,
upper: 1,
u_offset: 0,
}],
);
assert_ne!(repeat_map[b'r' as usize], 0);
assert_ne!(repeat_map[b's' as usize], 0);
}
#[test]
fn lookbehind_start_map_ignores_bytes_before_the_match_start() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let reg = compile(b"(?<=x)a?bc");
assert!(has_finite_variable_optimizer(®));
let map = derive_start_byte_map(®).expect("lookbehind is analyzable");
assert_ne!(map[b'a' as usize], 0);
assert_ne!(map[b'b' as usize], 0);
assert_eq!(map[b'x' as usize], 0);
let (set, result) = onig_regset_new(vec![reg]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert!(set.fallback_search_candidates.is_empty());
let input = b"xabc";
let (index, position) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!((index, position), (0, 1));
}
#[test]
fn fallback_run_skips_preserve_winners_captures_bounds_and_limits() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
struct RestoreLimits(u64, u64, u32, u64);
impl Drop for RestoreLimits {
fn drop(&mut self) {
onig_set_retry_limit_in_match(self.0);
onig_set_retry_limit_in_search(self.1);
onig_set_match_stack_limit(self.2);
onig_set_time_limit(self.3);
}
}
let _restore = RestoreLimits(
onig_get_retry_limit_in_match(),
onig_get_retry_limit_in_search(),
onig_get_match_stack_limit(),
onig_get_time_limit(),
);
onig_set_time_limit(0);
let patterns = [
r"(?=\w?[\w\s]*\brecord\s+[$\w]+)",
r"(?=\w?[-\w\s]*\b(?:class|(?<!@)interface|enum)\s+[$\w]+)",
r"(?=\w?[\w\s]*\b(record)\s+(\w+))",
r"(?=[\w\s]*\brecord\s+\w+)",
r"(?=\w?[a-z]*record)",
r"(?=[a-z]*a)b",
r"[a-z]+(x+)+z",
r"a*bc",
r"(\w+)@([\w-]+\.\w+)",
];
let inputs: &[&[u8]] = &[
b"",
b"private int id = 1;",
b" private red green;",
b" record Order",
b"recordX record Order;",
b"@interface Api",
b"public class Order",
b"enum Status",
b"aba",
b"aaaa bc",
b"aa xxxxx y",
b"user@example.org",
"é abc record 名".as_bytes(),
b"a\xc3\xa9 abc",
b"aa\xffbbb record R",
b"a\xe2\x82 record R",
];
let limits = [
(0, 0, 0),
(1, 0, 0),
(2, 0, 0),
(3, 0, 0),
(4, 0, 0),
(8, 0, 0),
(16, 0, 0),
(32, 0, 0),
(64, 0, 0),
(10_000_000, 0, 0),
(0, 2, 0),
(0, 8, 0),
(0, 0, 2),
(0, 0, 8),
];
let outcome = |set: &mut OnigRegSet, input: &[u8], end, start, range, option, fast| {
let found = if fast {
onig_regset_search_fast_with_id(
set,
input,
end,
start,
range,
OnigRegSetLead::PositionLead,
option,
FallbackMemoIdentity::Caller(71),
)
} else {
onig_regset_search(
set,
input,
end,
start,
range,
OnigRegSetLead::PositionLead,
option,
)
};
let region = if found.0 >= 0 {
let region = onig_regset_get_region(set, found.0 as usize).unwrap();
(region.beg.clone(), region.end.clone())
} else {
(Vec::new(), Vec::new())
};
(found, onig_regset_last_match_len(set), region)
};
let mut checked = 0;
for pattern in patterns {
for &input in inputs {
for end in [input.len(), input.len().saturating_sub(1)] {
for (retry, search_retry, stack) in limits {
onig_set_retry_limit_in_match(retry);
onig_set_retry_limit_in_search(search_retry);
onig_set_match_stack_limit(stack);
for fast in [false, true] {
let (mut optimized, status) = onig_regset_new(vec![
compile(pattern.as_bytes()),
compile(b"(id|green|Order|b)"),
]);
assert_eq!(status, ONIG_NORMAL);
let (mut reference, status) = onig_regset_new(vec![
compile(pattern.as_bytes()),
compile(b"(id|green|Order|b)"),
]);
assert_eq!(status, ONIG_NORMAL);
let optimized = optimized.as_mut().unwrap();
let reference = reference.as_mut().unwrap();
for entry in &mut reference.entries {
reg_mut(entry).leading_run = None;
}
for start in 0..=end {
for range in [start, (start + 2).min(end), end] {
for option in [
ONIG_OPTION_NONE,
ONIG_OPTION_FIND_LONGEST,
ONIG_OPTION_FIND_NOT_EMPTY,
] {
assert_eq!(
outcome(
optimized, input, end, start, range, option, fast
),
outcome(
reference, input, end, start, range, option, fast
),
"{pattern} {input:?} end={end} start={start} range={range} option={option:?} fast={fast} retry={retry} search_retry={search_retry} stack={stack}"
);
checked += 1;
}
}
}
}
}
}
}
}
eprintln!("Compared {checked} RegSet search outcomes, including captures and limit errors");
assert!(checked > 100_000, "{checked}");
}
#[test]
fn required_literals_preserve_winners_captures_bounds_and_limits() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
struct RestoreLimits(u64, u64, u32, u64, u64);
impl Drop for RestoreLimits {
fn drop(&mut self) {
onig_set_retry_limit_in_match(self.0);
onig_set_retry_limit_in_search(self.1);
onig_set_match_stack_limit(self.2);
onig_set_time_limit(self.3);
crate::regexec::onig_set_subexp_call_limit_in_search(self.4);
}
}
let _restore = RestoreLimits(
onig_get_retry_limit_in_match(),
onig_get_retry_limit_in_search(),
onig_get_match_stack_limit(),
onig_get_time_limit(),
crate::regexec::onig_get_subexp_call_limit_in_search(),
);
onig_set_time_limit(0);
let patterns = [
r"\s*(\{)",
r"\s*é(\w?)",
r"(?:\s+|^)(?:foo|bar)_x(\w*)",
r"(?:\s+|(?<=\W)|^)((?<!\w)(?:reinterpret|dynamic|static|const)_cast(?!\w))",
r"(?:\s+|^)\w*(?=\s*\{)",
r"(?:\s+|^)[{(]x?",
r"(?:\s+|^)(?i:st)\w*",
r"(?:\s+|^)(?<p>\((?:[^()]|\g<p>)*\))",
r"(?:\s+|^)(ab)\w*\1",
r"(?:\s+|^|(?<=b))(?<=ab)(?:c|d)",
r"(?:\s+|^|(?<=b))(?!ab)(?:c|d)",
r"(?:\s+|^)(?:a*a?)*x",
];
let inputs: &[&[u8]] = &[
b"",
b"{",
b" {x",
b"foo_x bar_xy foo_",
b" static_cast<int>(x); dynamic_cast",
b"abcd { efgh",
"\u{17f}t ST \u{fb06}x sT".as_bytes(),
b"a(b(c)d)e) f(",
b"ab abxab ab",
b"abc ab c abc",
b"\xc3\xa9x \xc3\xa9\xc3",
b"\xff{\xc3( \xc3\xa9",
b"aaaaaaaaaaaa",
b"aaaaaaaaaa x a",
b"no literal here at all",
];
let position_patterns = [
r"\G\s*(\{)",
r"(?:\s+|^|\G)(?:foo|bar)_x(\w*)",
r"\s*(?<=(?:\W|^)(?:ab|cd))(x\w*)",
r"\s*(?<!(?:\W|^)(?:ab|cd))(x\w*)",
r"\s*(x\w*)(?<=(?:\W|^)(?:ab|cd)x\w*)",
r"\s*(x)(?<!(?:\W|^)(?:ax|bx))",
r"\s*(?<=(?:\W|^)(?:é|ab))(x\w*)(?<!(?:\W|^)(?:yx|zx))",
];
let position_inputs: &[&[u8]] = &[
b"",
b"abx foo_x{ cdxx {",
b"yx zx ax bx abx",
"\u{e9}x abx \u{e9}{ zx".as_bytes(),
b"ab\xa9x cd\x80\x80x {\xa9 x",
b"\xf4\x8f\xbf\xbfx ab\xf5\x80\x80\x80x",
];
let limits = [
(0, 0, 0, 0),
(1, 0, 0, 0),
(2, 0, 0, 0),
(8, 0, 0, 0),
(64, 0, 0, 0),
(10_000_000, 0, 0, 0),
(0, 2, 0, 0),
(0, 8, 0, 0),
(0, 0, 2, 0),
(0, 0, 8, 0),
(0, 0, 0, 2),
];
let outcome = |set: &mut OnigRegSet, input: &[u8], end, start, range, option, fast| {
let found = if fast {
onig_regset_search_fast_with_id(
set,
input,
end,
start,
range,
OnigRegSetLead::PositionLead,
option,
FallbackMemoIdentity::Caller(73),
)
} else {
onig_regset_search(
set,
input,
end,
start,
range,
OnigRegSetLead::PositionLead,
option,
)
};
let region = if found.0 >= 0 {
let region = onig_regset_get_region(set, found.0 as usize).unwrap();
(region.beg.clone(), region.end.clone())
} else {
(Vec::new(), Vec::new())
};
(found, onig_regset_last_match_len(set), region)
};
let sets = |pattern: &str| {
let build = || {
let (set, status) = onig_regset_new(vec![
compile(pattern.as_bytes()),
compile(b"(id|green|Order|b)"),
]);
assert_eq!(status, ONIG_NORMAL);
set.unwrap()
};
let optimized = build();
assert_eq!(fallback_indices(&optimized), [0], "{pattern}");
let entry = &optimized.entries[0];
assert!(
entry.required_literals_safe && entry.reg.required_literals.is_some(),
"{pattern}"
);
let mut reference = build();
reg_mut(&mut reference.entries[0]).required_literals = None;
(optimized, reference, build())
};
let (mut checked, mut limit_differences) = (0, 0);
let cases = (patterns.iter().map(|&pattern| (pattern, inputs))).chain(
position_patterns
.iter()
.map(|&pattern| (pattern, position_inputs)),
);
for (pattern, inputs) in cases {
for &input in inputs {
for end in [input.len(), input.len().saturating_sub(1)] {
for (retry, search_retry, stack, calls) in limits {
onig_set_retry_limit_in_match(retry);
onig_set_retry_limit_in_search(search_retry);
onig_set_match_stack_limit(stack);
crate::regexec::onig_set_subexp_call_limit_in_search(calls);
for fast in [false, true] {
let (mut optimized, mut reference, mut unlimited) = sets(pattern);
for start in 0..=end {
for range in [start, (start + 2).min(end), end] {
for option in [
ONIG_OPTION_NONE,
ONIG_OPTION_FIND_NOT_EMPTY,
ONIG_OPTION_NOTBOL,
] {
let expected = outcome(
&mut reference,
input,
end,
start,
range,
option,
fast,
);
let actual = outcome(
&mut optimized,
input,
end,
start,
range,
option,
fast,
);
if actual != expected
&& retry != 0
&& expected.0.0 == ONIGERR_RETRY_LIMIT_IN_MATCH_OVER
{
onig_set_retry_limit_in_match(0);
let unlimited = outcome(
&mut unlimited,
input,
end,
start,
range,
option,
false,
);
onig_set_retry_limit_in_match(retry);
assert_eq!(
actual, unlimited,
"{pattern} {input:?} end={end} start={start} range={range} option={option:?} fast={fast} retry={retry}"
);
limit_differences += 1;
continue;
}
assert_eq!(
actual, expected,
"{pattern} {input:?} end={end} start={start} range={range} option={option:?} fast={fast} retry={retry} search_retry={search_retry} stack={stack} calls={calls}"
);
checked += 1;
}
}
}
for start in 0..=end {
for stop in [start, end] {
let event = |set: &mut OnigRegSet| {
let event = onig_regset_entry_search(
set,
0,
input,
end,
start,
stop,
ONIG_OPTION_NONE,
);
let region = onig_regset_get_region(set, 0).unwrap();
(event, region.beg.clone(), region.end.clone())
};
let expected = event(&mut reference);
let actual = event(&mut optimized);
if actual.0 != expected.0
&& retry != 0
&& matches!(
expected.0,
RegSetEntryEvent::Error {
code: ONIGERR_RETRY_LIMIT_IN_MATCH_OVER,
..
}
)
{
match actual.0 {
RegSetEntryEvent::Error { position, code } => {
assert_eq!(code, ONIGERR_RETRY_LIMIT_IN_MATCH_OVER);
assert!(matches!(
expected.0,
RegSetEntryEvent::Error { position: first, .. }
if first < position
));
}
_ => {
onig_set_retry_limit_in_match(0);
let unlimited = event(&mut unlimited);
onig_set_retry_limit_in_match(retry);
assert_eq!(
actual.0, unlimited.0,
"{pattern} {input:?} end={end} start={start} stop={stop} retry={retry}"
);
if matches!(
actual.0,
RegSetEntryEvent::Match { .. }
) {
assert_eq!(actual, unlimited);
}
}
}
limit_differences += 1;
continue;
}
assert_eq!(
actual.0, expected.0,
"{pattern} {input:?} end={end} start={start} stop={stop} retry={retry} search_retry={search_retry} stack={stack} calls={calls}"
);
if matches!(actual.0, RegSetEntryEvent::Match { .. }) {
assert_eq!(actual, expected);
}
checked += 1;
}
}
}
}
}
}
}
eprintln!(
"Compared {checked} RegSet outcomes with and without required literals; \
{limit_differences} retry-limit errors left out"
);
assert!(checked > 100_000, "{checked}");
assert!(limit_differences > 0);
}
#[test]
fn required_literals_keep_attempts_of_entries_with_position_checks() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let pattern = br"\s*(?<=y*\x{140000})ABC";
let subject = b"yyy\xf5\x80\x80\x80ABCD";
let end = subject.len();
let reg = compile(pattern);
let required = reg.required_literals.as_deref().unwrap();
assert_eq!(required.literal_list(), [b"ABC".to_vec()]);
assert_eq!(
onig_search(®, subject, end, 0, end, None, ONIG_OPTION_NONE).0,
10
);
assert_eq!(required.find(subject, 10, end), None);
let (set, status) = onig_regset_new(vec![compile(pattern), compile(b"(id|green|Order|b)")]);
assert_eq!(status, ONIG_NORMAL);
let mut set = set.unwrap();
assert_eq!(fallback_indices(&set), [0]);
let msa = MatchArg::new(&set.entries[0].reg, ONIG_OPTION_NONE, None, 0);
assert!(required_literals(&set.entries[0], ONIG_OPTION_NONE, &msa).is_none());
for start in 0..=7 {
assert_eq!(
onig_regset_search(
&mut set,
subject,
end,
start,
end,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
(0, 10),
"start={start}"
);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
subject,
end,
start,
end,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(181),
),
(0, 10),
"start={start}"
);
assert_eq!(
onig_regset_entry_search(&mut set, 0, subject, end, start, end, ONIG_OPTION_NONE),
RegSetEntryEvent::Match { position: 10 },
"start={start}"
);
}
}
#[test]
fn required_literals_gate_closes_for_callouts_and_look_behind_leads() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, status) = onig_regset_new(vec![compile(br"\s*(\{)")]);
assert_eq!(status, ONIG_NORMAL);
let mut set = set.unwrap();
for (pattern, filtered, memo_safe) in [
(&br"\s*(\{)(?{x})"[..], false, false),
(br"\G\s*(\{)", true, false),
(br"\s*(?<!(?:\W|^)(?:ab|cd))(x\w*)", true, false),
(br"\s*(?<=(?:\W|^)return)\s*(\[)", false, false),
(br"\s*(?<!(?:\W|^)return)\s*(\[)", false, false),
(br"\s*(\{)", true, true),
] {
assert_eq!(
onig_regset_replace(&mut set, 0, Some(compile(pattern))),
ONIG_NORMAL
);
let entry = &set.entries[0];
let msa = MatchArg::new(&entry.reg, ONIG_OPTION_NONE, None, 0);
let pattern = String::from_utf8_lossy(pattern);
assert_eq!(
entry.reg.required_literals.is_some(),
!entry.has_callouts,
"{pattern}"
);
assert_eq!(entry.required_literals_safe, filtered, "{pattern}");
assert_eq!(
required_literals(entry, ONIG_OPTION_NONE, &msa).is_some(),
filtered,
"{pattern}"
);
assert_eq!(entry.fallback_memo_safe, memo_safe, "{pattern}");
}
}
#[test]
fn required_literals_no_event_of_memo_unsafe_entry_is_not_memoized() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let input = b"ab{ cd x";
for (pattern, memo_safe) in [
(&br"(?:\s+|^)(?<!(?:\W|^)(?:ab|cd))(\{)"[..], false),
(br"(?:\s+|^)(\{)", true),
] {
let (set, status) = onig_regset_new(vec![compile(pattern)]);
assert_eq!(status, ONIG_NORMAL);
let mut set = set.unwrap();
assert_eq!(fallback_indices(&set), [0]);
let msa = MatchArg::new(&set.entries[0].reg, ONIG_OPTION_NONE, None, 0);
assert!(required_literals(&set.entries[0], ONIG_OPTION_NONE, &msa).is_some());
assert_eq!(set.entries[0].fallback_memo_safe, memo_safe);
for start in [0, 3] {
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::OnigString(181),
),
(ONIG_MISMATCH, 0)
);
}
if memo_safe {
assert_settled_from(&set, 0);
} else {
let candidate = set.fallback_search_candidates[0];
assert!(set.fallback_memos[0].is_empty());
assert_eq!(
[
candidate.no_match_from,
candidate.exact_miss,
candidate.match_from,
candidate.match_at
],
[usize::MAX; 4]
);
}
}
}
#[test]
fn required_literals_agree_on_generated_position_checks() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let subjects: &[&[u8]] = &[
b"A\xa9B yA\xa9\xa9C",
"y\u{e9}\u{a9}B ab\u{e9}C".as_bytes(),
b"ab\xa9AB y\xf4\x8f\xbf\xbfB",
b"yC \xf5\x80\x80AB\xa9",
b"abAB yAB C\tB",
b"abABAB abBB",
b"",
];
let (mut filtered, mut kept) = (0, 0);
for prefix in [r"\s*", r"(?:\s+|\G)"] {
for behind in [
"",
r"(?<=(?:\W|^)(?:ab|é))",
r"(?<!(?:\W|^)(?:ab|é))",
r"(?<=y*A)",
r"(?<!y*A)",
] {
for middle in ["", "."] {
for literal in ["B", "(?:AB|é)C?"] {
for after in ["", r"(?<=(?:\W|^)(?:yB|C))", r"(?<!y*B)"] {
let pattern = format!("{prefix}{behind}{middle}{literal}{after}");
let build = || {
let (set, status) =
onig_regset_new(vec![compile(pattern.as_bytes())]);
assert_eq!(status, ONIG_NORMAL);
set.unwrap()
};
let mut optimized = build();
let entry = &optimized.entries[0];
if fallback_indices(&optimized) != [0]
|| entry.reg.required_literals.is_none()
{
continue;
}
if entry.required_literals_safe {
filtered += 1;
} else {
kept += 1;
}
let mut reference = build();
reg_mut(&mut reference.entries[0]).required_literals = None;
for (id, &subject) in (0..).zip(subjects) {
let end = subject.len();
for fast in [false, true] {
for start in 0..=end {
let outcome = |set: &mut OnigRegSet| {
let found = if fast {
onig_regset_search_fast_with_id(
set,
subject,
end,
start,
end,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(id),
)
} else {
onig_regset_search(
set,
subject,
end,
start,
end,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
)
};
let region = onig_regset_get_region(set, 0).unwrap();
(found, (found.0 == 0).then(|| region.beg.clone()))
};
assert_eq!(
outcome(&mut optimized),
outcome(&mut reference),
"{pattern} {subject:?} start={start} fast={fast}"
);
}
}
}
}
}
}
}
}
assert_eq!((filtered, kept), (48, 72));
}
#[test]
fn first_op_rejects_preserve_winners_captures_and_limits() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
struct RestoreLimits(u64, u64, u32, u64);
impl Drop for RestoreLimits {
fn drop(&mut self) {
onig_set_retry_limit_in_match(self.0);
onig_set_retry_limit_in_search(self.1);
onig_set_match_stack_limit(self.2);
onig_set_time_limit(self.3);
}
}
let _restore = RestoreLimits(
onig_get_retry_limit_in_match(),
onig_get_retry_limit_in_search(),
onig_get_match_stack_limit(),
onig_get_time_limit(),
);
let patterns: [(&str, bool); 13] = [
(r"\b(?:if|for|while)\b", true),
(r"\b\w*\(", false),
(r"^\s*#\s*(\w+)", false),
(r"^[a-z]+:", true),
(r"(?<=\.)(\w+)", true),
(r"(?<![\w$])\d+(?:\.\d+)?", true),
(r"(?<=[=:])\s*(\w*);", false),
(r"\B\w*-", false),
(r"(?W)\b[a-zé]+", true),
(r"(?<=é)a", true),
(r"\b", true),
(r"^", true),
(r"(?<!a)", true),
];
let mut sets: Vec<Vec<&str>> = patterns.iter().map(|&(p, _)| vec![p]).collect();
let mut mixed: Vec<&str> = patterns[..10].iter().map(|&(p, _)| p).collect();
mixed.insert(4, "(id|b)");
sets.push(mixed);
let inputs: &[&[u8]] = &[
b"",
b"if (x) for",
b" # include <a>\n#define B",
b"a.b = 1.5; c: d;",
b"x -a- -b-c d-e-",
"é aé \u{e9}a 名.x".as_bytes(),
b"aa\xffbbb if(",
b"a\xe2\x82 while (",
b"\n\nab:\n cd:",
b"$1 a1 2.3 id",
];
let limits = [
(0, 0, 0, 0, true),
(2, 0, 0, 0, true),
(10_000_000, 0, 0, 0, true),
(1, 0, 0, 0, false),
(0, 1, 0, 0, false),
(0, 6, 0, 0, false),
(0, 0, 1, 0, false),
(0, 0, 6, 0, false),
(0, 0, 0, 1_000_000, false),
];
let rejects = || FIRST_OP_REJECTS.with(|rejects| rejects.get());
let outcome = |set: &mut OnigRegSet, input: &[u8], id, end, start, range, option, mode| {
let found = match mode {
0 => onig_regset_search(
set,
input,
end,
start,
range,
OnigRegSetLead::PositionLead,
option,
),
1 => onig_regset_search_fast(
set,
input,
end,
start,
range,
OnigRegSetLead::PositionLead,
option,
),
_ => onig_regset_search_fast_with_id(
set,
input,
end,
start,
range,
OnigRegSetLead::PositionLead,
option,
FallbackMemoIdentity::Caller(id),
),
};
let region = if found.0 >= 0 {
let region = onig_regset_get_region(set, found.0 as usize).unwrap();
(region.beg.clone(), region.end.clone())
} else {
(Vec::new(), Vec::new())
};
(found, onig_regset_last_match_len(set), region)
};
let mut checked = 0;
let mut rejected_by_set = vec![0u64; sets.len()];
for (retry, search_retry, stack, time, skips) in limits {
onig_set_retry_limit_in_match(retry);
onig_set_retry_limit_in_search(search_retry);
onig_set_match_stack_limit(stack);
onig_set_time_limit(time);
for (set_at, set_patterns) in sets.iter().enumerate() {
let build = || {
let regs = set_patterns.iter().map(|p| compile(p.as_bytes())).collect();
let (set, status) = onig_regset_new(regs);
assert_eq!(status, ONIG_NORMAL);
set.unwrap()
};
let mut optimized = build();
let mut reference = build();
for entry in &mut reference.entries {
entry.first_op = None;
}
if let Some(&(pattern, table)) = patterns.get(set_at) {
assert!(optimized.entries[0].first_op.is_some(), "{pattern}");
assert_eq!(optimized.entries[0].fallback, !table, "{pattern}");
}
for mode in 0..3 {
for (id, &input) in inputs.iter().enumerate() {
for end in [input.len(), input.len().saturating_sub(1)] {
for option in [
ONIG_OPTION_NONE,
ONIG_OPTION_FIND_NOT_EMPTY,
ONIG_OPTION_NOTBOL,
ONIG_OPTION_FIND_LONGEST,
] {
for start in 0..=end {
for range in [start, (start + 2).min(end), end] {
let before = rejects();
let expected = outcome(
&mut reference,
input,
id as u64,
end,
start,
range,
option,
mode,
);
assert_eq!(rejects(), before);
let found = outcome(
&mut optimized,
input,
id as u64,
end,
start,
range,
option,
mode,
);
let rejected = rejects() - before;
let context = format!(
"{set_patterns:?} {input:?} end={end} start={start} range={range} option={option:?} mode={mode} limits={:?}",
(retry, search_retry, stack, time)
);
assert_eq!(found, expected, "{context}");
if !skips || option == ONIG_OPTION_FIND_LONGEST {
assert_eq!(rejected, 0, "{context}");
}
rejected_by_set[set_at] += rejected;
checked += 1;
}
}
}
}
}
}
}
}
for (set_patterns, rejected) in sets.iter().zip(&rejected_by_set) {
assert!(*rejected > 0, "{set_patterns:?}");
}
eprintln!(
"Compared {checked} RegSet search outcomes; {} attempts left out",
rejected_by_set.iter().sum::<u64>()
);
assert!(checked > 100_000, "{checked}");
}
#[test]
fn grammar_first_op_test_census() {
let census = |patterns: Vec<String>| {
let regs = patterns.iter().map(|p| compile(p.as_bytes())).collect();
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let set = set.unwrap();
let mut counts = [[0; 3]; 2];
for entry in &set.entries {
let kind = match entry.first_op {
None => continue,
Some(FirstOpTest::WordBoundary { .. }) => 0,
Some(FirstOpTest::BeginLine) => 1,
Some(FirstOpTest::LookBehind { .. }) => 2,
};
counts[usize::from(entry.fallback)][kind] += 1;
}
counts
};
assert_eq!(
census(grammar_loader::typescript_patterns()),
[[5, 2, 73], [0, 0, 43]]
);
assert_eq!(
census(grammar_loader::css_patterns()),
[[0, 0, 32], [0, 0, 3]]
);
assert_eq!(
census(grammar_loader::rust_patterns()),
[[36, 0, 3], [0, 0, 1]]
);
}
#[test]
fn unbounded_optimizer_stays_on_the_search_fallback() {
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
assert_eq!(set.table_entry_count, 0);
}
#[test]
fn fallback_search_fills_backtracked_push_captures() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"(?:(a)|b)*ab")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
let cases: [(&[u8], i32, i32); 2] = [
(b"xbab", ONIG_REGION_NOTPOS, ONIG_REGION_NOTPOS),
(b"xaab", 1, 2),
];
for (input, g1_beg, g1_end) in cases {
let (index, position) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!((index, position), (0, 1));
let region = onig_regset_get_region(&set, 0).expect("region");
assert_eq!((region.beg[0], region.end[0]), (1, 4));
assert_eq!((region.beg[1], region.end[1]), (g1_beg, g1_end));
}
}
#[test]
fn fallback_start_filter_only_skips_impossible_starts() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let patterns: [&[u8]; 2] = [b"\\s*(\\[)", b"x"];
let (set, result) = onig_regset_new(patterns.iter().map(|p| compile(p)).collect());
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
let filter = set.entries[0].start_filter.as_deref().expect("filter");
assert!(filter[b' ' as usize] != 0 && filter[b'[' as usize] != 0);
assert_eq!(filter[b'a' as usize], 0);
assert!(fallback_start_filter(&compile(b"\\s*(*COUNT)\\[")).is_none());
let input = b"ab [c \xc3\xa9[ x [";
for start in 0..=input.len() {
let expected = patterns
.iter()
.enumerate()
.filter_map(|(index, pattern)| {
let reg = compile(pattern);
let (pos, _) = onig_search(
®,
input,
input.len(),
start,
input.len(),
None,
ONIG_OPTION_NONE,
);
(pos >= 0).then_some((pos, index as i32))
})
.min()
.map_or((ONIG_MISMATCH, 0), |(pos, index)| (index, pos));
let found = onig_regset_search(
&mut set,
input,
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!(found, expected, "start {start}");
}
}
#[test]
fn start_map_reads_through_positive_lookaheads() {
let map_of = |pattern: &[u8]| derive_start_byte_map(&compile(pattern));
let members = |map: [u8; CHAR_MAP_SIZE]| -> Vec<u8> {
(0..=255u8).filter(|&b| map[b as usize] != 0).collect()
};
let map = map_of(b"(?<=:)(?=\\s*\\{)").expect("look-ahead fixes the start");
assert!(map[b' ' as usize] != 0 && map[b'\t' as usize] != 0 && map[b'{' as usize] != 0);
assert_eq!(map[b':' as usize], 0);
assert_eq!(map[b'a' as usize], 0);
assert_eq!(members(map_of(b"(?=a|b)").unwrap()), b"ab");
assert_eq!(members(map_of(b"(?=(?=ab)a)").unwrap()), b"a");
assert_eq!(members(map_of(b"(?=a?)x").unwrap()), b"x");
assert_eq!(members(map_of(b"(?>ab)").unwrap()), b"a");
assert_eq!(members(map_of(b"(?!x)b").unwrap()), b"b");
for zero_width in [
&b"(?=a?)"[..],
b"(?=)",
b"(?<=ab)",
b"(?<=a|bc)",
b"(?!a)",
b"(?>a|)",
] {
assert_eq!(
map_of(zero_width),
None,
"{:?}",
std::str::from_utf8(zero_width)
);
}
}
#[test]
fn lookahead_start_filters_keep_regset_results() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let patterns: [&[u8]; 5] = [
b"(?<=:)(?=\\s*\\{)",
b"(?=(\\w+)\\s*<)",
b"(?=a?)b",
b"(?<![a-z])(?=[a-z]+\\()",
b"x",
];
let (set, result) = onig_regset_new(patterns.iter().map(|p| compile(p)).collect());
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = b"a: {b} f(x) : {\xc3\xa9< ab <c> x:{";
for start in 0..=input.len() {
let expected = patterns
.iter()
.enumerate()
.filter_map(|(index, pattern)| {
let reg = compile(pattern);
let (pos, _) = onig_search(
®,
input,
input.len(),
start,
input.len(),
None,
ONIG_OPTION_NONE,
);
(pos >= 0).then_some((pos, index as i32))
})
.min()
.map_or((ONIG_MISMATCH, 0), |(pos, index)| (index, pos));
let found = onig_regset_search(
&mut set,
input,
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!(found, expected, "start {start}");
}
}
#[test]
fn table_entry_count_tracks_mixed_add_remove_and_replace_sets() {
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(set.table_entry_count, 0);
assert_eq!(onig_regset_add(&mut set, compile(b"x")), ONIG_NORMAL);
assert_eq!(set.table_entry_count, 1);
assert_eq!(
onig_regset_replace(&mut set, 1, None),
ONIG_NORMAL,
"removing the sole table entry restores a pure fallback set"
);
assert_eq!(set.table_entry_count, 0);
assert_eq!(
onig_regset_replace(&mut set, 0, Some(compile(b"x"))),
ONIG_NORMAL
);
assert_eq!(set.table_entry_count, 1);
assert!(set.fallback_search_candidates.is_empty());
assert_eq!(onig_regset_add(&mut set, compile(b"a*bc")), ONIG_NORMAL);
assert_eq!(set.table_entry_count, 1);
assert_eq!(fallback_indices(&set), [1]);
}
#[test]
fn all_fallback_memo_hit_skips_the_empty_table_pass() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = vec![b'a'; 80_000];
let identity = FallbackMemoIdentity::OnigString(11);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
&input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(ONIG_MISMATCH, 0)
);
assert_settled_from(&set, 0);
set.scratch_msa = None;
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
&input,
input.len(),
1,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(ONIG_MISMATCH, 0)
);
assert!(set.scratch_msa.is_none());
}
#[test]
fn mixed_set_keeps_its_table_search() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc"), compile(b"x")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(set.table_entry_count, 1);
let input = b"x";
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::OnigString(12),
),
(1, 0)
);
}
#[test]
fn fallback_memo_preserves_progressing_match_regions_and_lengths() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = b"aabcxxbc";
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(41),
),
(0, 0)
);
assert_eq!(onig_regset_last_match_len(&set), 4);
let region = onig_regset_get_region(&set, 0).expect("first region");
assert_eq!((region.beg[0], region.end[0]), (0, 4));
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
4,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(41),
),
(0, 6)
);
assert_eq!(onig_regset_last_match_len(&set), 2);
let region = onig_regset_get_region(&set, 0).expect("second region");
assert_eq!((region.beg[0], region.end[0]), (6, 8));
assert_eq!(set.fallback_memos[0].len(), 2);
}
#[test]
fn fallback_memo_invalidates_ids_limits_and_regset_changes() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = b"aaaa";
let search = |set: &mut OnigRegSet, id| {
onig_regset_search_fast_with_id(
set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
id,
)
};
assert_eq!(
search(&mut set, FallbackMemoIdentity::Caller(42)),
(ONIG_MISMATCH, 0)
);
assert_settled_from(&set, 0);
let first_key = set.fallback_memo_key.expect("memo key");
let first_revision = set
.scratch_limits_revision
.expect("first search captures the global-limit revision");
assert_eq!(
search(&mut set, FallbackMemoIdentity::Caller(43)),
(ONIG_MISMATCH, 0)
);
assert_ne!(
set.fallback_memo_key.expect("new ID key").identity,
first_key.identity
);
assert!(set.scratch_msa.is_some());
assert_eq!(
set.scratch_limits,
Some(FallbackMemoLimits::current()),
"identity changes clear result memos, not reusable MatchArg buffers"
);
let old_retry_match = onig_get_retry_limit_in_match();
let old_retry_search = onig_get_retry_limit_in_search();
let old_stack = onig_get_match_stack_limit();
let old_time = onig_get_time_limit();
onig_set_retry_limit_in_match(old_retry_match.saturating_add(1));
onig_set_match_stack_limit(old_stack.saturating_add(1));
onig_set_time_limit(old_time.saturating_add(1));
assert_ne!(
onig_get_global_limit_revision(),
first_revision,
"every global limit setter invalidates cached MatchArg limits"
);
assert_eq!(
search(&mut set, FallbackMemoIdentity::Caller(43)),
(ONIG_MISMATCH, 0)
);
let changed_key = set.fallback_memo_key.expect("limit key");
assert_ne!(
changed_key.retry_limit_in_match,
first_key.retry_limit_in_match
);
assert_ne!(changed_key.match_stack_limit, first_key.match_stack_limit);
assert_ne!(changed_key.time_limit, first_key.time_limit);
onig_set_retry_limit_in_match(old_retry_match);
onig_set_match_stack_limit(old_stack);
onig_set_time_limit(old_time);
onig_set_retry_limit_in_search(old_retry_search.saturating_add(1));
let key_before = set.fallback_memo_key;
assert_eq!(
search(&mut set, FallbackMemoIdentity::Caller(44)),
(ONIG_MISMATCH, 0)
);
assert!(set.fallback_memo_key == key_before);
onig_set_retry_limit_in_search(old_retry_search);
assert_eq!(onig_regset_add(&mut set, compile(b"x")), ONIG_NORMAL);
assert!(set.fallback_memo_key.is_none());
assert_eq!(set.fallback_memos.len(), 2);
assert_eq!(onig_regset_replace(&mut set, 1, None), ONIG_NORMAL);
assert!(set.fallback_memo_key.is_none());
assert_eq!(set.fallback_memos.len(), 1);
}
#[test]
fn fallback_memo_separates_caller_and_onig_string_id_domains() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let no_match = b"aaaa";
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
no_match,
no_match.len(),
0,
no_match.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(1),
),
(ONIG_MISMATCH, 0)
);
let matching = b"aabc";
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
matching,
matching.len(),
0,
matching.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::OnigString(1),
),
(0, 0)
);
}
#[test]
fn fallback_memo_rebuilds_match_arg_after_limit_change() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = b"aaaa";
let identity = FallbackMemoIdentity::OnigString(7);
onig_set_retry_limit_in_match(old_limit.saturating_add(1));
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(ONIG_MISMATCH, 0)
);
assert_eq!(
set.scratch_msa
.as_ref()
.expect("first search leaves scratch state")
.retry_limit_in_match,
old_limit.saturating_add(1)
);
let lowered = old_limit.saturating_sub(1);
onig_set_retry_limit_in_match(lowered);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(ONIG_MISMATCH, 0)
);
assert_eq!(
set.scratch_msa
.as_ref()
.expect("limit change rebuilds scratch state")
.retry_limit_in_match,
lowered
);
onig_set_retry_limit_in_match(old_limit);
}
#[test]
fn fallback_memo_limit_change_matches_a_fresh_regset_error() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
let input = format!("x{}b", "a".repeat(1_001));
let identity = FallbackMemoIdentity::OnigString(10);
let new_set = || {
let (set, result) = onig_regset_new(vec![compile(br".*x(a+)+b")]);
assert_eq!(result, ONIG_NORMAL);
set.expect("regset")
};
let mut reused = new_set();
onig_set_retry_limit_in_match(old_limit.max(1_000_000));
assert_eq!(
onig_regset_search_fast_with_id(
&mut reused,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(0, 0)
);
onig_set_retry_limit_in_match(1);
let reused_result = onig_regset_search_fast_with_id(
&mut reused,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
);
let mut fresh = new_set();
let fresh_result = onig_regset_search_fast_with_id(
&mut fresh,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
);
onig_set_retry_limit_in_match(old_limit);
assert_eq!(reused_result, fresh_result);
assert_eq!(reused_result, (ONIGERR_RETRY_LIMIT_IN_MATCH_OVER, 0));
}
#[test]
fn position_lead_refreshes_retry_limits_for_all_fallback_no_id_searches() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_match = onig_get_retry_limit_in_match();
let old_search = onig_get_retry_limit_in_search();
let input = format!("{}bx", "a".repeat(1_001));
let make = || {
let (set, result) = onig_regset_new(vec![compile(br"(a*)\1b")]);
assert_eq!(result, ONIG_NORMAL);
set.expect("regset")
};
let mut reused = make();
onig_set_retry_limit_in_match(old_match.max(1_000_000));
onig_set_retry_limit_in_search(0);
let _ = onig_regset_search(
&mut reused,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
onig_set_retry_limit_in_match(100);
let reused_result = onig_regset_search(
&mut reused,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
let mut fresh = make();
let fresh_result = onig_regset_search(
&mut fresh,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
onig_set_retry_limit_in_match(old_match);
onig_set_retry_limit_in_search(old_search);
assert_eq!(reused_result, fresh_result);
assert_eq!(reused_result, (ONIGERR_RETRY_LIMIT_IN_MATCH_OVER, 0));
}
#[test]
fn table_position_lead_refreshes_retry_limits_and_reports_search_over() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_match = onig_get_retry_limit_in_match();
let old_search = onig_get_retry_limit_in_search();
let input = format!("{}b", "a".repeat(32));
let make = || {
let (set, result) = onig_regset_new(vec![compile(br"(?:a|aa)*\z")]);
assert_eq!(result, ONIG_NORMAL);
set.expect("regset")
};
let mut reused = make();
assert_eq!(reused.table_entry_count, 1);
onig_set_retry_limit_in_match(0);
onig_set_retry_limit_in_search(1_000_000);
let _ = onig_regset_search(
&mut reused,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
onig_set_retry_limit_in_search(100);
let reused_result = onig_regset_search(
&mut reused,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
let mut fresh = make();
let fresh_result = onig_regset_search(
&mut fresh,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
onig_set_retry_limit_in_match(old_match);
onig_set_retry_limit_in_search(old_search);
assert_eq!(reused_result, fresh_result);
assert_eq!(reused_result, (ONIGERR_RETRY_LIMIT_IN_SEARCH_OVER, 0));
}
#[test]
fn table_position_lead_skips_retry_scratch_when_search_limit_is_disabled() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_search = onig_get_retry_limit_in_search();
onig_set_retry_limit_in_search(0);
let (set, result) = onig_regset_new(vec![compile(b"a")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(set.table_entry_count, 1);
set.scratch_table_retry_counters[0] = 123;
assert_eq!(
onig_regset_search(
&mut set,
b"a",
1,
0,
1,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
(0, 0)
);
assert_eq!(set.scratch_table_retry_counters, vec![123]);
onig_set_retry_limit_in_search(old_search);
}
#[test]
fn table_retry_search_budget_is_isolated_per_regex() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_match = onig_get_retry_limit_in_match();
let old_search = onig_get_retry_limit_in_search();
onig_set_retry_limit_in_match(0);
onig_set_retry_limit_in_search(137);
let input = format!("{}c", "a".repeat(16));
let upstream = compile(br"(a+)\1bc");
let (upstream_result, _) = onig_search(
&upstream,
input.as_bytes(),
input.len(),
0,
input.len(),
Some(OnigRegion::new()),
ONIG_OPTION_NONE,
);
assert_eq!(upstream_result, ONIG_MISMATCH);
for regs in [
vec![compile(br"(a+)\1bc")],
vec![compile(br"(a+)\1bc"), compile(br"(a+)\1bc")],
] {
let (set, result) = onig_regset_new(regs);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert!(set.table_entry_count >= 1);
assert_eq!(
onig_regset_search(
&mut set,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
(ONIG_MISMATCH, 0),
"each regex receives its own upstream-compatible search budget"
);
}
onig_set_retry_limit_in_match(old_match);
onig_set_retry_limit_in_search(old_search);
}
#[test]
fn nonzero_time_limit_starts_a_fresh_position_lead_search_clock() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_time = onig_get_time_limit();
let old_match = onig_get_retry_limit_in_match();
let old_search = onig_get_retry_limit_in_search();
let input = "a".repeat(600);
let make = || {
let (set, result) = onig_regset_new(vec![compile(br"(?:a|aa)*\z")]);
assert_eq!(result, ONIG_NORMAL);
set.expect("regset")
};
let mut reused = make();
onig_set_retry_limit_in_match(0);
onig_set_retry_limit_in_search(0);
onig_set_time_limit(50);
assert_eq!(
onig_regset_search(
&mut reused,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
(0, 0)
);
std::thread::sleep(std::time::Duration::from_millis(75));
let reused_result = onig_regset_search(
&mut reused,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
let mut fresh = make();
let fresh_result = onig_regset_search(
&mut fresh,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
onig_set_time_limit(old_time);
onig_set_retry_limit_in_match(old_match);
onig_set_retry_limit_in_search(old_search);
assert_eq!(reused_result, fresh_result);
assert_eq!(reused_result, (0, 0));
}
#[test]
fn fallback_memo_skips_position_dependent_patterns() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(br"a*(?:\Gx|y)")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
assert!(
set.entries[0]
.reg
.ops
.iter()
.any(|op| op.opcode == OpCode::CheckPosition)
);
let input = b"axc";
let identity = FallbackMemoIdentity::OnigString(8);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(ONIG_MISMATCH, 0)
);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
1,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(0, 1)
);
assert!(set.fallback_memos[0].is_empty());
}
#[test]
fn fallback_memo_safety_is_precomputed_when_entries_change() {
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert!(set.entries[0].fallback_memo_safe);
assert_eq!(
onig_regset_replace(&mut set, 0, Some(compile(br".*(?{x})a"))),
ONIG_NORMAL
);
assert!(!set.entries[0].fallback_memo_safe);
assert_eq!(
onig_regset_replace(&mut set, 0, Some(compile(br"a*(?:\Gx|y)"))),
ONIG_NORMAL
);
assert!(!set.entries[0].fallback_memo_safe);
}
#[test]
fn exact_start_fallback_checks_do_not_prefetch_the_suffix() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc"), compile(b"x")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = b"xaaaaaaaaaaaaaaaa";
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::OnigString(9),
),
(1, 0)
);
assert!(matches!(
set.fallback_memos[0].as_slice(),
[FallbackMemo::ExactStartMiss(0)]
));
}
#[test]
fn identical_direct_miss_is_cached_without_a_second_vm_attempt() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc"), compile(b"x")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = b"x";
let identity = FallbackMemoIdentity::OnigString(13);
for _ in 0..2 {
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(1, 0)
);
}
assert!(
set.fallback_memos[0]
.iter()
.any(|memo| matches!(memo, FallbackMemo::ExactStartMiss(0)))
);
}
#[test]
fn second_direct_miss_upgrades_to_an_advancing_optimizer_cursor() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc"), compile(b"x")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = vec![b'x'; 80_000];
let identity = FallbackMemoIdentity::OnigString(14);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
&input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(1, 0)
);
assert!(matches!(
set.fallback_memos[0].as_slice(),
[FallbackMemo::ExactStartMiss(0)]
));
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
&input,
input.len(),
1,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(1, 1)
);
assert_settled_from(&set, 1);
for start in [2, input.len() - 1] {
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
&input,
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(1, start as i32)
);
}
}
#[test]
fn direct_miss_upgrade_preserves_fallback_match_region_and_restart_passes() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc"), compile(b"x"), compile(b"a")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = b"xaabc";
let identity = FallbackMemoIdentity::OnigString(15);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(1, 0)
);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
1,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(0, 1)
);
assert_eq!(onig_regset_last_match_len(&set), 4);
let region = onig_regset_get_region(&set, 0).expect("fallback region");
assert_eq!((region.beg[0], region.end[0]), (1, 5));
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(1, 0)
);
assert!(
set.fallback_memos[0]
.iter()
.any(|memo| matches!(memo, FallbackMemo::ExactStartMiss(0)))
);
}
#[test]
fn direct_miss_upgrade_preserves_a_same_position_retry_error() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
onig_set_retry_limit_in_match(100);
let (set, result) =
onig_regset_new(vec![compile(br"a*x(a+)+b"), compile(b"y"), compile(b"x")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
let input = format!("yx{}c", "a".repeat(1_001));
let identity = FallbackMemoIdentity::OnigString(16);
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
(1, 0)
);
let error = onig_regset_search_fast_with_id(
&mut set,
input.as_bytes(),
input.len(),
1,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
);
onig_set_retry_limit_in_match(old_limit);
assert_eq!(error, (ONIGERR_RETRY_LIMIT_IN_MATCH_OVER, 0));
}
#[test]
fn fallback_memo_skips_callout_patterns() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(br".*(?{x})a")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert!(
set.entries[0]
.reg
.extp
.as_ref()
.is_some_and(|ext| ext.callout_num != 0)
);
assert_eq!(fallback_indices(&set), [0]);
let input = b"x";
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(5),
),
(ONIG_MISMATCH, 0)
);
assert!(set.fallback_memos[0].is_empty());
}
#[test]
fn unbounded_no_match_uses_one_optimizer_scan_then_memoizes() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(b"a*bc")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
assert!(set.first_byte_candidates[b'a' as usize].is_empty());
let input = vec![b'a'; 80_000];
for start in [0, 1] {
assert_eq!(
onig_regset_search_fast_with_id(
&mut set,
&input,
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(99),
),
(ONIG_MISMATCH, 0)
);
}
assert_settled_from(&set, 0);
}
#[test]
fn fallback_does_not_probe_a_start_after_an_unbeatable_table_winner() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
onig_set_retry_limit_in_match(100);
let (set, result) = onig_regset_new(vec![compile(br"(a*)\1b"), compile(b"x")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let input = format!("x{}b", "a".repeat(1_001));
let (index, position) = onig_regset_search(
&mut set,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
onig_set_retry_limit_in_match(old_limit);
assert_eq!((index, position), (1, 0));
}
#[test]
fn fallback_match_precedes_a_later_table_retry_error() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
onig_set_retry_limit_in_match(100);
let input = format!("bx{}c", "a".repeat(1_001));
for (patterns, expected_index) in [
([br"x(a+)+b".as_slice(), br"(a*)\1b".as_slice()], 1),
([br"(a*)\1b".as_slice(), br"x(a+)+b".as_slice()], 0),
] {
let (set, result) = onig_regset_new(patterns.into_iter().map(compile).collect());
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let (index, position) = onig_regset_search(
&mut set,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!((index, position), (expected_index, 0));
assert_eq!(onig_regset_last_match_len(&set), 1);
}
onig_set_retry_limit_in_match(old_limit);
}
#[test]
fn table_retry_error_wins_a_same_start_later_fallback_match() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
onig_set_retry_limit_in_match(100);
let input = format!("x{}c", "a".repeat(1_001));
let (set, result) = onig_regset_new(vec![compile(br"x(a+)+b"), compile(br"(x*)\1?")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let (index, position) = onig_regset_search(
&mut set,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
onig_set_retry_limit_in_match(old_limit);
assert_eq!((index, position), (ONIGERR_RETRY_LIMIT_IN_MATCH_OVER, 0));
}
#[test]
fn fallback_error_clears_a_superseded_winner_and_match_length() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
onig_set_retry_limit_in_match(100);
let input = format!("{}bx", "a".repeat(1_001));
let (set, result) = onig_regset_new(vec![compile(br"(a*)\1b"), compile(b"x")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let search_result = onig_regset_search(
&mut set,
input.as_bytes(),
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
onig_set_retry_limit_in_match(old_limit);
assert_eq!(search_result, (ONIGERR_RETRY_LIMIT_IN_MATCH_OVER, 0));
assert_eq!(onig_regset_last_match_len(&set), ONIG_MISMATCH);
for index in 0..2 {
let region = onig_regset_get_region(&set, index).expect("entry region");
assert_eq!(
(region.beg[0], region.end[0]),
(ONIG_REGION_NOTPOS, ONIG_REGION_NOTPOS)
);
}
}
#[test]
fn fallback_search_preserves_g_anchor_and_beats_a_later_table_match() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let (set, result) = onig_regset_new(vec![compile(br"\["), compile(br"\G\s*\[")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [1]);
let input = b"xx [";
let (index, position) = onig_regset_search(
&mut set,
input,
input.len(),
2,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!((index, position), (1, 2));
let region = onig_regset_get_region(&set, index as usize).expect("winning region");
assert_eq!((region.beg[0], region.end[0]), (2, 4));
}
#[test]
fn regset_basic_regex_lead() {
let regs = vec![compile(b"abc"), compile(b"def"), compile(b"ghi")];
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"xxxdefyyy";
let (idx, pos) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::RegexLead,
ONIG_OPTION_NONE,
);
assert_eq!(idx, 1); assert_eq!(pos, 3); }
#[test]
fn regset_earliest_match_regex_lead() {
let regs = vec![compile(b"yyy"), compile(b"def"), compile(b"xxx")];
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"xxxdefyyy";
let (idx, pos) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::RegexLead,
ONIG_OPTION_NONE,
);
assert_eq!(idx, 2);
assert_eq!(pos, 0);
}
#[test]
fn regset_priority_to_regex_order() {
let regs = vec![compile(b"def"), compile(b"xxx")];
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"xxxdefyyy";
let (idx, pos) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PriorityToRegexOrder,
ONIG_OPTION_NONE,
);
assert_eq!(idx, 0);
assert_eq!(pos, 3);
}
#[test]
fn regset_no_match() {
let regs = vec![compile(b"abc"), compile(b"def")];
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"xyz";
let (idx, _pos) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!(idx, ONIG_MISMATCH);
}
#[test]
fn regset_empty_string() {
let regs = vec![compile(b""), compile(b"x")];
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"";
let (idx, pos) = onig_regset_search(
&mut set,
input,
0,
0,
0,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!(idx, 0); assert_eq!(pos, 0);
}
#[test]
fn regset_fast_empty_string_records_zero_match_length() {
let (set, r) = onig_regset_new(vec![compile(b"$")]);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
assert_eq!(
onig_regset_search_fast(
&mut set,
b"",
0,
0,
0,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
(0, 0)
);
assert_eq!(onig_regset_last_match_len(&set), 0);
}
#[test]
fn fallback_attempts_only_positions_its_optimizer_admits() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
onig_set_retry_limit_in_match(10_000);
let input = "a".repeat(30);
let mut results = Vec::new();
for start in [0, 1, 5] {
let (set, result) = onig_regset_new(vec![compile(br"(\w+)+x"), compile(b"a")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
results.push(onig_regset_search(
&mut set,
input.as_bytes(),
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
));
results.push(onig_regset_search_fast_with_id(
&mut set,
input.as_bytes(),
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(17),
));
}
onig_set_retry_limit_in_match(old_limit);
assert_eq!(results, [(1, 0), (1, 0), (1, 1), (1, 1), (1, 5), (1, 5)]);
}
#[test]
fn a_fallback_match_at_the_start_ends_the_table_scan() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let n = 2_000;
let input = format!("{}(", "a".repeat(n));
for identity in [None, Some(FallbackMemoIdentity::Caller(19))] {
let (set, result) = onig_regset_new(vec![compile(br"(?=\s*[^;{])"), compile(br"\s+")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
for start in 0..=input.len() {
let found = match identity {
None => onig_regset_search_fast(
&mut set,
input.as_bytes(),
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
Some(identity) => onig_regset_search_fast_with_id(
&mut set,
input.as_bytes(),
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
};
let expected = if start < input.len() {
(0, start as i32)
} else {
(ONIG_MISMATCH, 0)
};
assert_eq!(found, expected, "start {start}");
}
assert!(
set.table_positions_scanned < 300 * (n as u64 + 2),
"{} positions",
set.table_positions_scanned
);
}
}
#[test]
fn start_byte_dispatch_keeps_c_s_optimizer_windows() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
onig_set_retry_limit_in_match(10_000);
let input = "a".repeat(30);
let (set, result) = onig_regset_new(vec![compile(b"(?:a|a){0,25}zzz"), compile(b"a")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert!(fallback_indices(&set).is_empty());
assert!(set.entries[0].gated);
let mut results = Vec::new();
for start in [0, 5, 29] {
results.push(onig_regset_search(
&mut set,
input.as_bytes(),
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
));
results.push(onig_regset_search_fast_with_id(
&mut set,
input.as_bytes(),
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
FallbackMemoIdentity::Caller(20),
));
}
let entry_events: Vec<_> = [0, 5]
.into_iter()
.map(|start| {
onig_regset_entry_search(
&mut set,
0,
input.as_bytes(),
input.len(),
start,
input.len(),
ONIG_OPTION_NONE,
)
})
.collect();
onig_set_retry_limit_in_match(old_limit);
assert_eq!(results, [(1, 0), (1, 0), (1, 5), (1, 5), (1, 29), (1, 29)]);
assert_eq!(
entry_events,
[RegSetEntryEvent::None, RegSetEntryEvent::None]
);
}
#[test]
fn negated_class_maps_do_not_widen_c_s_optimizer() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let old_limit = onig_get_retry_limit_in_match();
onig_set_retry_limit_in_match(10_000);
let input = "a".repeat(30);
let mut results = Vec::new();
for pattern in [
&br"(?i)([^\s]+)+x"[..],
br"(?:\d|.[ab]*)*[^\s]b+.|b*(?:=+) *",
] {
for start in [0, 5] {
let (set, result) = onig_regset_new(vec![compile(pattern), compile(b"a")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
results.push(onig_regset_search(
&mut set,
input.as_bytes(),
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
));
}
}
onig_set_retry_limit_in_match(old_limit);
assert_eq!(results, [(1, 0), (1, 5), (1, 0), (1, 5)]);
}
#[test]
fn anychar_star_fallback_attempts_follow_the_regset_newline_rule() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let input = b"abx";
for (start, expected) in [
(0, (ONIG_MISMATCH, 0)),
(1, (ONIG_MISMATCH, 0)),
(2, (0, 2)),
] {
let (set, result) = onig_regset_new(vec![compile(br"(?<=b).*x"), compile(b"q")]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
assert_eq!(fallback_indices(&set), [0]);
for identity in [None, Some(FallbackMemoIdentity::Caller(18))] {
let found = match identity {
None => onig_regset_search(
&mut set,
input,
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
Some(identity) => onig_regset_search_fast_with_id(
&mut set,
input,
input.len(),
start,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
identity,
),
};
assert_eq!(found, expected, "start {start}, memo {identity:?}");
}
}
}
#[test]
fn regset_position_lead_attempts_the_range_position() {
let (set, r) = onig_regset_new(vec![compile(b"(?=b)")]);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
assert_eq!(
onig_regset_search(
&mut set,
b"abc",
3,
0,
1,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
(0, 1)
);
}
#[test]
fn regset_position_lead_finds_zero_width_matches_at_end() {
for pattern in [b"$".as_slice(), b"\\z".as_slice(), b"a*".as_slice()] {
let (set, r) = onig_regset_new(vec![compile(pattern)]);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
assert_eq!(
onig_regset_search(
&mut set,
b"abc",
3,
3,
3,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
(0, 3),
"pattern {:?}",
std::str::from_utf8(pattern).unwrap()
);
}
}
#[test]
fn regset_nonempty_eos_keeps_regex_lead_semantics() {
for lead in [
OnigRegSetLead::RegexLead,
OnigRegSetLead::PriorityToRegexOrder,
] {
let (set, r) = onig_regset_new(vec![compile(b"a*")]);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
assert_eq!(
onig_regset_search(&mut set, b"\na", 2, 2, 2, lead, ONIG_OPTION_NONE),
(ONIG_MISMATCH, 0),
"lead {lead:?}"
);
}
}
#[test]
fn regset_empty_set() {
let (set, r) = onig_regset_new(vec![]);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"abc";
let (idx, _) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!(idx, ONIG_MISMATCH);
}
#[test]
fn regset_search_normalizes_out_of_range_endpoints() {
let (set, r) = onig_regset_new(vec![compile(b"(?=b)")]);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"abc";
for (end, start, range, expected) in [
(input.len(), 0, 100, (0, 1)),
(100, 0, 100, (0, 1)),
(input.len(), 100, 0, (ONIG_MISMATCH, 0)),
] {
assert_eq!(
onig_regset_search(
&mut set,
input,
end,
start,
range,
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
),
expected,
"end={end}, start={start}, range={range}"
);
}
}
#[test]
fn regset_add_and_replace() {
let (set, r) = onig_regset_new(vec![compile(b"abc")]);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
assert_eq!(onig_regset_number_of_regex(&set), 1);
let r = onig_regset_add(&mut set, compile(b"def"));
assert_eq!(r, ONIG_NORMAL);
assert_eq!(onig_regset_number_of_regex(&set), 2);
let r = onig_regset_replace(&mut set, 0, None);
assert_eq!(r, ONIG_NORMAL);
assert_eq!(onig_regset_number_of_regex(&set), 1);
let input = b"def";
let (idx, pos) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!(idx, 0);
assert_eq!(pos, 0);
}
#[test]
fn regset_captures() {
let regs = vec![compile(b"a(b)c"), compile(b"(d)(e)f")];
let (set, r) = onig_regset_new(regs);
assert_eq!(r, ONIG_NORMAL);
let mut set = set.unwrap();
let input = b"xdefx";
let (idx, pos) = onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
);
assert_eq!(idx, 1);
assert_eq!(pos, 1);
let region = onig_regset_get_region(&set, 1).unwrap();
assert_eq!(region.beg[0], 1); assert_eq!(region.end[0], 4); assert_eq!(region.beg[1], 1); assert_eq!(region.end[1], 2);
assert_eq!(region.beg[2], 2); assert_eq!(region.end[2], 3);
}
#[test]
fn keep_patterns_report_attempt_relative_positions_and_lengths() {
let _lock = LIMIT_TEST_LOCK.lock().unwrap();
let input = b"xxabxx";
for (pattern, position, match_len, region) in [
(br"a\Kb".as_slice(), 2, 2, (3, 4)),
(br".*\Kb".as_slice(), 0, 4, (3, 4)),
] {
for eager in [false, true] {
let (set, result) = onig_regset_new(vec![compile(pattern)]);
assert_eq!(result, ONIG_NORMAL);
let mut set = set.expect("regset");
let where_ = format!("{} eager={eager}", String::from_utf8_lossy(pattern));
let found = if eager {
onig_regset_search(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
)
} else {
onig_regset_search_fast(
&mut set,
input,
input.len(),
0,
input.len(),
OnigRegSetLead::PositionLead,
ONIG_OPTION_NONE,
)
};
assert_eq!(found, (0, position), "{where_}");
assert_eq!(onig_regset_last_match_len(&set), match_len, "{where_}");
let found_region = onig_regset_get_region(&set, 0).expect("region");
assert_eq!(
(found_region.beg[0], found_region.end[0]),
region,
"{where_}"
);
assert_eq!(position + match_len, found_region.end[0], "{where_}");
assert_ne!(position, found_region.beg[0], "{where_}");
}
}
}
}