#![cfg(feature = "ffi")]
use ferroni::encodings::utf8::ONIG_ENCODING_UTF8;
use ferroni::ffi::{CRegex, CRegion};
use ferroni::oniguruma::*;
use ferroni::regcomp::onig_new;
use ferroni::regexec::{
onig_new_match_param, onig_search_with_param, onig_set_match_stack_limit_size_of_match_param,
};
use ferroni::regsyntax::OnigSyntaxOniguruma;
type Outcome = (i32, Vec<(i32, i32)>);
fn ferroni_search(reg: &RegexType, subject: &[u8], limit: u32) -> Outcome {
let mut mp = onig_new_match_param();
onig_set_match_stack_limit_size_of_match_param(&mut mp, limit);
let (r, region) = onig_search_with_param(
reg,
subject,
subject.len(),
0,
subject.len(),
Some(OnigRegion::new()),
ONIG_OPTION_NONE,
&mp,
);
let spans = match region {
Some(region) if r >= 0 => region
.beg
.iter()
.zip(®ion.end)
.map(|(&beg, &end)| (beg, end))
.collect(),
_ => Vec::new(),
};
(r, spans)
}
fn c_search(c: &CRegex, subject: &[u8], limit: u32) -> Outcome {
let mut region = CRegion::new();
let r = c.search_with_match_stack_limit(
subject,
0,
subject.len(),
Some(&mut region),
ferroni::ffi::ONIG_OPTION_NONE,
limit,
);
let spans = if r >= 0 {
region.capture_ranges()
} else {
Vec::new()
};
(r, spans)
}
fn compile(pattern: &str) -> (RegexType, CRegex) {
let reg = onig_new(
pattern.as_bytes(),
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
)
.unwrap_or_else(|e| panic!("{pattern} compiles: {e:?}"));
let c = CRegex::new(pattern.as_bytes(), ferroni::ffi::ONIG_OPTION_NONE).unwrap();
(reg, c)
}
const CASES: &[(&str, &str)] = &[
(r"(?:a|b)*z", "a"),
(r"(a|b)*z", "a"),
(r"(?:\w+\s)*z", "ab "),
(r"(?:(a)|b)*c", "a"),
(r"(?:a|ab)*?z", "ab"),
(r"(?<=a|b)(?:x|y)*z", "x"),
(
r"(a)(b)?(c)?(d)?(e)?(f)?(g)?(h)?(i)?(j)?(k)?(l)?(m)?(n)?(o)?(p)?(q)?(r)?(s)?(t)?(u)?(v)?(w)?(x)?(y)?(?:a|b)*z",
"a",
),
];
const COUNTS: &[usize] = &[1, 100, 157, 158, 159, 160, 316, 317, 318, 319, 320, 400];
fn limits() -> impl Iterator<Item = u32> {
(1..=8)
.chain([100, 159, 160, 161, 162, 200])
.chain(310..=330)
.chain([400, 640, 641, 700])
}
#[test]
fn small_stack_limits_never_stop_ferroni_where_c_completes() {
for &(pattern, unit) in CASES {
let (reg, c) = compile(pattern);
for &count in COUNTS {
for tail in ["z", "!"] {
let subject = format!("{}{tail}", unit.repeat(count));
let subject = subject.as_bytes();
let unlimited = c_search(&c, subject, 0);
assert_eq!(
ferroni_search(®, subject, 0),
unlimited,
"{pattern} on {count} x {unit:?} + {tail:?} without a limit"
);
for limit in limits() {
let ferroni = ferroni_search(®, subject, limit);
let c = c_search(&c, subject, limit);
let context = format!(
"{pattern} on {count} x {unit:?} + {tail:?} at stack limit {limit}"
);
if c.0 == ONIGERR_MATCH_STACK_LIMIT_OVER {
assert!(
ferroni == c || ferroni == unlimited,
"{context}: C stops, Ferroni returns {ferroni:?}"
);
} else {
assert_eq!(ferroni, c, "{context}");
}
}
}
}
}
}
#[test]
fn shallow_searches_complete_under_a_limit_of_one() {
for (pattern, subject) in [(r"(?:[a-z]+;)*z", "a;z"), (r"(a|b)*z", "abz"), (r"a", "a")] {
let (reg, c) = compile(pattern);
let subject = subject.as_bytes();
let unlimited = c_search(&c, subject, 0);
assert!(unlimited.0 >= 0, "{pattern} matches {subject:?}");
assert_eq!(c_search(&c, subject, 1), unlimited, "C, {pattern}");
assert_eq!(
ferroni_search(®, subject, 1),
unlimited,
"Ferroni, {pattern}"
);
}
}
#[test]
fn limits_below_320_hold_321_entries_like_c() {
let (reg, c) = compile(r"(?:a|b)*z");
for (count, completes_below_320) in [(318, true), (319, false)] {
let subject = format!("{}z", "a".repeat(count));
let subject = subject.as_bytes();
for limit in [1, 2, 160, 319] {
let expected = if completes_below_320 {
0
} else {
ONIGERR_MATCH_STACK_LIMIT_OVER
};
assert_eq!(
c_search(&c, subject, limit).0,
expected,
"C, {count} at {limit}"
);
assert_eq!(
ferroni_search(®, subject, limit).0,
expected,
"Ferroni, {count} at {limit}"
);
}
assert_eq!(
c_search(&c, subject, 320).0,
ONIGERR_MATCH_STACK_LIMIT_OVER,
"C, {count} at 320"
);
assert_eq!(
ferroni_search(®, subject, 320).0,
ONIGERR_MATCH_STACK_LIMIT_OVER,
"Ferroni, {count} at 320"
);
}
}