use {
super::backtrack::deterministic,
fancy_regex::{Assertion, Expr, LookAround},
regex::{Error as RegexError, RegexBuilder},
};
const MAX_REPS: i32 = i32::MAX;
const LEAST_INPUT_LIMIT: i32 = 1 << 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Refusal {
NoObviousMaximum,
MaximumOverflows,
TooLarge,
}
impl Refusal {
pub(super) const fn reason(self) -> &'static str {
match self {
Self::NoObviousMaximum => "has a look-behind group with no obvious maximum length",
Self::MaximumOverflows => {
"has a look-behind group whose maximum length overflows Java's int, which krabka \
cannot read as Java does"
}
Self::TooLarge => "has a look-behind group too large to compile",
}
}
}
#[derive(Debug, Clone, Copy)]
struct TreeInfo {
min_length: i32,
max_length: i32,
exact_min_length: i128,
exact_max_length: i128,
max_valid: bool,
}
impl TreeInfo {
const fn new() -> Self {
Self {
min_length: 0,
max_length: 0,
exact_min_length: 0,
exact_max_length: 0,
max_valid: true,
}
}
fn add_min(&mut self, length: i32, exact: i128) {
self.min_length = self.min_length.wrapping_add(length);
self.exact_min_length = self.exact_min_length.saturating_add(exact);
}
fn add_max(&mut self, length: i32, exact: i128) {
self.max_length = self.max_length.wrapping_add(length);
self.exact_max_length = self.exact_max_length.saturating_add(exact);
}
}
pub(super) fn check_look_behinds(
expr: &Expr,
code_points: &[bool],
) -> Result<LookBehinds, Refusal> {
let mut bodies = Vec::new();
look_behind_bodies(expr, &mut bodies);
let studied: Vec<(&Expr, TreeInfo)> = bodies
.into_iter()
.map(|body| {
let mut info = TreeInfo::new();
study(&[body], &mut info);
(body, info)
})
.collect();
if studied.iter().any(|(_, info)| !info.max_valid) {
return Err(Refusal::NoObviousMaximum);
}
let windows: Vec<JavaWindow> = studied
.iter()
.enumerate()
.map(|(index, (_, info))| JavaWindow {
min: info.min_length,
max: info.max_length,
code_points: code_points.get(index).copied().unwrap_or(false),
})
.collect();
let mut input_limit: Option<usize> = None;
for ((body, info), window) in studied.iter().zip(&windows) {
if let Some(limit) = wrapped_window(body, info, window.code_points)? {
input_limit = Some(input_limit.map_or(limit, |least| least.min(limit)));
}
}
Ok(LookBehinds {
input_limit,
too_large: studied.iter().any(|(body, _)| too_large(body)),
windows,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct JavaWindow {
pub(crate) min: i32,
pub(crate) max: i32,
pub(crate) code_points: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct LookBehinds {
pub(super) windows: Vec<JavaWindow>,
pub(super) input_limit: Option<usize>,
pub(super) too_large: bool,
}
fn look_behind_bodies<'a>(expr: &'a Expr, bodies: &mut Vec<&'a Expr>) {
match expr {
Expr::LookAround(child, kind) => {
if matches!(kind, LookAround::LookBehind | LookAround::LookBehindNeg) {
bodies.push(child);
}
look_behind_bodies(child, bodies);
}
Expr::Repeat { child, .. } | Expr::AtomicGroup(child) => look_behind_bodies(child, bodies),
Expr::Group(child) => look_behind_bodies(child, bodies),
Expr::Concat(children) | Expr::Alt(children) => {
for child in children {
look_behind_bodies(child, bodies);
}
}
_ => {}
}
}
fn wrapped_window(
body: &Expr,
info: &TreeInfo,
code_points: bool,
) -> Result<Option<usize>, Refusal> {
let rmax = info.max_length;
if i128::from(rmax) == info.exact_max_length {
return Ok(None);
}
if code_points {
return Err(Refusal::MaximumOverflows);
}
if rmax >= 0 {
if matches_empty_anywhere(body) {
return Ok(None);
}
if rmax >= LEAST_INPUT_LIMIT {
return Ok(usize::try_from(rmax).ok());
}
return Err(Refusal::MaximumOverflows);
}
let last_empty = i32::MAX + rmax;
if info.exact_min_length > i128::from(last_empty) {
Ok(None)
} else {
Err(Refusal::MaximumOverflows)
}
}
fn matches_empty_anywhere(expr: &Expr) -> bool {
match expr {
Expr::Empty | Expr::Repeat { lo: 0, .. } => true,
Expr::Literal { val, .. } => val.is_empty(),
Expr::Repeat { child, .. } => matches_empty_anywhere(child),
Expr::Concat(children) => children.iter().all(matches_empty_anywhere),
Expr::Alt(children) => children.iter().any(matches_empty_anywhere),
Expr::Group(child) => matches_empty_anywhere(child),
_ => false,
}
}
fn too_large(body: &Expr) -> bool {
let alternatives = match body {
Expr::Alt(children) => children.iter().collect(),
_ => vec![body],
};
let mut delegated: Vec<String> = Vec::new();
for alternative in alternatives {
if soft(alternative) {
let mut source = String::new();
alternative.to_str(&mut source, 1);
delegated.push(source);
} else if let Expr::Concat(children) = alternative {
for run in children.split(|child| !soft(child)) {
if !run.is_empty() {
let mut source = String::new();
for child in run {
child.to_str(&mut source, 1);
}
delegated.push(source);
}
}
}
}
delegated.iter().any(|source| {
matches!(
RegexBuilder::new(source).build(),
Err(RegexError::CompiledTooBig(_))
)
})
}
fn soft(expr: &Expr) -> bool {
match expr {
Expr::Empty
| Expr::Any { .. }
| Expr::Literal { .. }
| Expr::Delegate { .. }
| Expr::Assertion(
Assertion::StartText
| Assertion::EndText
| Assertion::StartLine { .. }
| Assertion::EndLine { .. },
) => true,
Expr::Concat(children) | Expr::Alt(children) => children.iter().all(soft),
Expr::Group(child) => soft(child),
Expr::Repeat { child, .. } => soft(child),
_ => false,
}
}
fn study(chain: &[&Expr], info: &mut TreeInfo) {
let Some((&first, rest)) = chain.split_first() else {
return;
};
match first {
Expr::Concat(children) => return inline(children.iter(), rest, info),
Expr::Group(child) => return inline(std::iter::once(&**child), rest, info),
Expr::AtomicGroup(child) => return inline(std::iter::once(&**child), rest, info),
Expr::Alt(children) => return branch(children.iter(), rest, info),
Expr::Repeat {
child,
lo,
hi,
greedy,
} => {
if (*lo, *hi) == (0, 1) && is_group(child) {
return branch([&**child, &Expr::Empty].into_iter(), rest, info);
}
if !repeat(child, *lo, *hi, *greedy, info) {
return;
}
}
Expr::Literal { val, .. } => add(info, val.chars().count()),
Expr::Any { .. } | Expr::Delegate { .. } => add(info, 1),
Expr::GeneralNewline { .. } => {
info.add_min(1, 1);
info.add_max(2, 2);
}
Expr::Backref { .. } => info.max_valid = false,
_ => {}
}
study(rest, info);
}
fn inline<'a>(children: impl Iterator<Item = &'a Expr>, rest: &[&'a Expr], info: &mut TreeInfo) {
let joined: Vec<&Expr> = children.chain(rest.iter().copied()).collect();
study(&joined, info);
}
fn add(info: &mut TreeInfo, length: usize) {
let exact = i128::try_from(length).unwrap_or(i128::MAX);
info.add_min(java_int(length), exact);
info.add_max(java_int(length), exact);
}
fn java_int(value: usize) -> i32 { i32::try_from(value).unwrap_or(MAX_REPS) }
fn branch<'a>(children: impl Iterator<Item = &'a Expr>, rest: &[&'a Expr], info: &mut TreeInfo) {
let mut branches = TreeInfo {
min_length: i32::MAX,
max_length: -1,
exact_min_length: i128::MAX,
exact_max_length: -1,
max_valid: info.max_valid,
};
for child in children {
let mut alternative = TreeInfo::new();
study(&[child], &mut alternative);
branches.min_length = branches.min_length.min(alternative.min_length);
branches.max_length = branches.max_length.max(alternative.max_length);
branches.exact_min_length = branches.exact_min_length.min(alternative.exact_min_length);
branches.exact_max_length = branches.exact_max_length.max(alternative.exact_max_length);
branches.max_valid &= alternative.max_valid;
}
let mut before = *info;
before.add_min(branches.min_length, branches.exact_min_length);
before.add_max(branches.max_length, branches.exact_max_length);
*info = TreeInfo::new();
study(rest, info);
info.add_min(before.min_length, before.exact_min_length);
info.add_max(before.max_length, before.exact_max_length);
info.max_valid &= branches.max_valid;
}
fn repeat(child: &Expr, lo: usize, hi: usize, greedy: bool, info: &mut TreeInfo) -> bool {
let unbounded = hi == usize::MAX;
if (lo, hi) == (0, 1) {
let (min_length, exact_min_length) = (info.min_length, info.exact_min_length);
study(&[child], info);
(info.min_length, info.exact_min_length) = (min_length, exact_min_length);
} else if greedy && unbounded && is_char_property(child) {
info.add_min(java_int(lo), i128::from(java_int(lo)));
if info.max_valid {
info.add_max(MAX_REPS, i128::from(MAX_REPS));
}
} else if is_group(child) && !deterministic(child) && !zero_width(child) {
info.max_valid = false;
return false;
} else {
let (cmin, cmax) = (java_int(lo), java_int(hi));
let mut atom = TreeInfo::new();
study(&[child], &mut atom);
let least = atom
.min_length
.wrapping_mul(cmin)
.wrapping_add(info.min_length);
info.min_length = if least < info.min_length {
0xFFF_FFFF
} else {
least
};
info.exact_min_length = atom
.exact_min_length
.saturating_mul(i128::from(cmin))
.saturating_add(info.exact_min_length);
if info.max_valid && atom.max_valid {
let total = atom
.max_length
.wrapping_mul(cmax)
.wrapping_add(info.max_length);
if total < info.max_length {
info.max_valid = false;
}
info.max_length = total;
info.exact_max_length = atom
.exact_max_length
.saturating_mul(i128::from(cmax))
.saturating_add(info.exact_max_length);
} else {
info.max_valid = false;
}
}
true
}
fn is_char_property(child: &Expr) -> bool {
match child {
Expr::Literal { val, .. } => val.chars().count() == 1,
Expr::Any { .. } | Expr::Delegate { .. } => true,
_ => false,
}
}
fn zero_width(expr: &Expr) -> bool {
match expr {
Expr::Empty
| Expr::Assertion(_)
| Expr::ContinueFromPreviousMatchEnd
| Expr::LookAround(..) => true,
Expr::Concat(children) | Expr::Alt(children) => children.iter().all(zero_width),
Expr::Group(child) => zero_width(child),
_ => false,
}
}
fn is_group(child: &Expr) -> bool {
match child {
Expr::Group(_) | Expr::Concat(_) | Expr::Alt(_) => true,
Expr::Literal { val, .. } => val.chars().count() > 1,
_ => false,
}
}
#[cfg(test)]
mod tests {
use {super::*, assert2::check};
fn look_behinds(pattern: &str, code_points: &[bool]) -> Result<LookBehinds, Refusal> {
let tree = Expr::parse_tree(pattern).unwrap_or_else(|_| panic!("{pattern:?} parses"));
check_look_behinds(&tree.expr, code_points)
}
const fn units(min: i32, max: i32) -> JavaWindow {
JavaWindow {
min,
max,
code_points: false,
}
}
#[yare::parameterized(
char_property = { "(?<=a)", &[units(1, 1)] },
ques = { "(?<=ab?)", &[units(1, 2)] },
curly = { "(?<=a{2,5})", &[units(2, 5)] },
line_ending = { r"(?<=.\R)", &[units(2, 3)] },
branch = { "(?<=a|bcd)", &[units(1, 3)] },
group_curly = { "(?<=(?:ab){2})", &[units(4, 4)] },
char_property_greedy = { "(?<=a*)", &[units(0, i32::MAX)] },
// Three greedy repeats wrap the maximum back past 0, and the body
// matches the empty string anywhere, so the window is accepted.
char_property_greedy_wrapped = { "(?<=a*b*c*)", &[units(0, 2_147_483_645)] },
char_property_greedy_wrapped_negative = { "(?<=a*bc)", &[units(2, i32::MIN + 1)] },
char_property_greedy_minimum = { "(?<=a{3,}b)", &[units(4, i32::MIN)] },
look_ahead = { "(?<=(?=x)y)", &[units(1, 1)] },
nested_look_behind = { "(?<=a(?<=b)c)", &[units(2, 2), units(1, 1)] },
negated_branch = { "(?<!ab|c)", &[units(1, 2)] },
atomic_group = { "(?<=(?>ab)c?)", &[units(2, 3)] },
group_ques = { "(?<=a(?:bc)?)", &[units(1, 3)] },
supplementary_char = { r"(?<=\x{1F600})", &[units(1, 1)] },
// `a{1073741824}` four times wraps the maximum to 0, and twice wraps
// the minimum, which Java then sets to `0xFFFFFFF`.
wrapped_minimum = { "(?<=(?:a{1073741824}){2,4}b*)", &[units(0xFFF_FFFF, i32::MAX)] },
)]
fn a_window_is_the_minimum_and_maximum_java_studies(pattern: &str, expected: &[JavaWindow]) {
let windows = look_behinds(pattern, &[]).map(|found| found.windows);
check!(windows.as_deref() == Ok(expected));
}
#[yare::parameterized(
// `rmax` is -2, and the window leaves out starts in either count.
wrapped_to_minus_two = { "(?<=a*b*)", &[false], Err(Refusal::MaximumOverflows) },
wrapped_in_code_points = { "(?<=a*b*c*x)", &[true], Err(Refusal::MaximumOverflows) },
wrapped_in_units = {
"(?<=a*b*c*x)",
&[false],
Ok(LookBehinds {
windows: vec![units(1, 2_147_483_646)],
input_limit: Some(2_147_483_646),
too_large: false,
}),
},
wrapped_with_no_flag = {
"(?<=a*b*c*x)",
&[],
Ok(LookBehinds {
windows: vec![units(1, 2_147_483_646)],
input_limit: Some(2_147_483_646),
too_large: false,
}),
},
nested_with_one_flag = {
"(?<=a(?<=b)c)",
&[true],
Ok(LookBehinds {
windows: vec![
JavaWindow {
min: 2,
max: 2,
code_points: true,
},
units(1, 1),
],
input_limit: None,
too_large: false,
}),
},
wrapped_minimum = {
"(?<=(?:a{1073741824}){2,4}b*)",
&[false],
Ok(LookBehinds {
windows: vec![units(0xFFF_FFFF, i32::MAX)],
input_limit: Some(2_147_483_647),
too_large: true,
}),
},
)]
fn a_window_counts_as_its_flag_says(
pattern: &str,
code_points: &[bool],
expected: Result<LookBehinds, Refusal>,
) {
check!(look_behinds(pattern, code_points) == expected);
}
}