#[cfg(test)]
use std::cell::Cell;
use crate::oniguruma::{ONIGENC_CTYPE_WORD, OnigCodePoint};
use crate::regcomp::{compiles_to_plain_star, is_exclusive, onig_is_code_in_cc};
use crate::regenc::onigenc_is_ascii_compatible_encoding;
use crate::regint::*;
use crate::regparse_types::*;
const MAX_FOLLOW_STEPS: u32 = 256;
#[cfg(test)]
thread_local! {
pub(crate) static POSSESSIFY_DISABLED: Cell<bool> = const { Cell::new(false) };
}
enum Follow<'a> {
End,
Seq(&'a Node, &'a Follow<'a>),
Again(&'a Node, &'a Follow<'a>),
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Start {
Fails,
Empty,
}
pub(crate) fn apply(root: &mut Node, reg: &RegexType, env: &ParseEnv) {
#[cfg(test)]
if POSSESSIFY_DISABLED.with(Cell::get) {
return;
}
if !onigenc_is_ascii_compatible_encoding(reg.enc)
|| env.num_call > 0
|| env.backref_num > 0
|| reg.extp.as_ref().is_some_and(|ext| ext.callout_num != 0)
{
return;
}
let mut stars = Vec::new();
collect(root, &Follow::End, reg, &mut Vec::new(), &mut stars);
for path in &stars {
mark(root, path);
}
}
fn is_keep(gn: &GimmickNode) -> bool {
gn.gimmick_type == GimmickType::Save && gn.detail_type == SaveType::Keep as i32
}
fn collect<'a>(
node: &'a Node,
follow: &Follow<'a>,
reg: &RegexType,
path: &mut Vec<u32>,
stars: &mut Vec<Vec<u32>>,
) {
match &node.inner {
NodeInner::List(_) => {
let mut cell = node;
let mut index = 0;
while let NodeInner::List(cons) = &cell.inner {
path.push(index);
match cons.cdr.as_deref() {
Some(rest) => {
collect(&cons.car, &Follow::Seq(rest, follow), reg, path, stars);
cell = rest;
}
None => collect(&cons.car, follow, reg, path, stars),
}
path.pop();
if cons.cdr.is_none() {
break;
}
index += 1;
}
}
NodeInner::Alt(_) => {
let mut cell = node;
let mut index = 0;
while let NodeInner::Alt(cons) = &cell.inner {
path.push(index);
collect(&cons.car, follow, reg, path, stars);
path.pop();
match cons.cdr.as_deref() {
Some(rest) => cell = rest,
None => break,
}
index += 1;
}
}
NodeInner::Quant(qn) => {
if let Some(body) = star_body(qn) {
let mut steps = MAX_FOLLOW_STEPS;
if follow_fails(follow, body, reg, &mut steps) {
stars.push(path.clone());
}
return;
}
let Some(body) = qn.body.as_deref() else {
return;
};
path.push(0);
match qn.upper {
0 => {}
1 => collect(body, follow, reg, path, stars),
_ => collect(body, &Follow::Again(body, follow), reg, path, stars),
}
path.pop();
}
NodeInner::Bag(bn) => {
if matches!(bn.bag_type, BagType::Memory | BagType::Option) {
if let Some(body) = bn.body.as_deref() {
path.push(0);
collect(body, follow, reg, path, stars);
path.pop();
}
}
}
_ => {}
}
}
fn star_body(qn: &QuantNode) -> Option<&Node> {
if !compiles_to_plain_star(qn) {
return None;
}
let body = qn.body.as_deref()?;
match &body.inner {
NodeInner::CClass(_) | NodeInner::CType(_) => Some(body),
NodeInner::String(sn)
if sn.s.len() == 1
&& !body.has_status(ND_ST_LITERAL_ALT)
&& !is_real_ignorecase(body, sn) =>
{
Some(body)
}
_ => None,
}
}
fn is_real_ignorecase(node: &Node, sn: &StrNode) -> bool {
node.has_status(ND_ST_IGNORECASE) && !sn.is_crude()
}
fn follow_fails(follow: &Follow<'_>, body: &Node, reg: &RegexType, steps: &mut u32) -> bool {
match *follow {
Follow::End => false,
Follow::Seq(cells, rest) => {
let mut cell = cells;
while let NodeInner::List(cons) = &cell.inner {
match start(&cons.car, body, reg, steps) {
None => return false,
Some(Start::Fails) => return true,
Some(Start::Empty) => {}
}
match cons.cdr.as_deref() {
Some(next) => cell = next,
None => return follow_fails(rest, body, reg, steps),
}
}
false
}
Follow::Again(again, rest) => {
start(again, body, reg, steps).is_some() && follow_fails(rest, body, reg, steps)
}
}
}
fn start(node: &Node, body: &Node, reg: &RegexType, steps: &mut u32) -> Option<Start> {
*steps = steps.checked_sub(1)?;
match &node.inner {
NodeInner::String(sn) => {
if sn.s.is_empty() {
Some(Start::Empty)
} else if node.has_status(ND_ST_LITERAL_ALT) || is_real_ignorecase(node, sn) {
None
} else {
is_exclusive(body, node, reg).then_some(Start::Fails)
}
}
NodeInner::CClass(_) | NodeInner::CType(_) => {
is_exclusive(body, node, reg).then_some(Start::Fails)
}
NodeInner::List(_) => {
let mut cell = node;
while let NodeInner::List(cons) = &cell.inner {
if start(&cons.car, body, reg, steps)? == Start::Fails {
return Some(Start::Fails);
}
match cons.cdr.as_deref() {
Some(next) => cell = next,
None => break,
}
}
Some(Start::Empty)
}
NodeInner::Alt(_) => {
let mut all_fail = true;
let mut cell = node;
while let NodeInner::Alt(cons) = &cell.inner {
all_fail &= start(&cons.car, body, reg, steps)? == Start::Fails;
match cons.cdr.as_deref() {
Some(next) => cell = next,
None => break,
}
}
Some(if all_fail { Start::Fails } else { Start::Empty })
}
NodeInner::Quant(qn) => {
if qn.upper == 0 {
return Some(Start::Empty);
}
let inner = start(qn.body.as_deref()?, body, reg, steps)?;
Some(if qn.lower == 0 { Start::Empty } else { inner })
}
NodeInner::Bag(bn) => match bn.bag_type {
BagType::Memory | BagType::Option | BagType::StopBacktrack => {
start(bn.body.as_deref()?, body, reg, steps)
}
BagType::IfElse => None,
},
NodeInner::Anchor(an) => match an.anchor_type {
ANCR_END_BUF => Some(Start::Fails),
ANCR_SEMI_END_BUF | ANCR_END_LINE => {
(!matches_newline(body, reg)).then_some(Start::Fails)
}
ANCR_PREC_READ
| ANCR_PREC_READ_NOT
| ANCR_BEGIN_BUF
| ANCR_BEGIN_LINE
| ANCR_BEGIN_POSITION
| ANCR_WORD_BOUNDARY
| ANCR_NO_WORD_BOUNDARY
| ANCR_WORD_BEGIN
| ANCR_WORD_END
| ANCR_TEXT_SEGMENT_BOUNDARY
| ANCR_NO_TEXT_SEGMENT_BOUNDARY => Some(Start::Empty),
_ => None,
},
NodeInner::Gimmick(gn) if is_keep(gn) => Some(Start::Empty),
NodeInner::Gimmick(_) | NodeInner::BackRef(_) | NodeInner::Call(_) => None,
}
}
fn matches_newline(body: &Node, reg: &RegexType) -> bool {
const NEWLINE: OnigCodePoint = 0x0a;
match &body.inner {
NodeInner::CClass(cc) => onig_is_code_in_cc(reg.enc, NEWLINE, cc),
NodeInner::CType(ct) if ct.ctype == ONIGENC_CTYPE_WORD as i32 => ct.not,
NodeInner::String(sn) => sn.s.first() == Some(&(NEWLINE as u8)),
_ => true,
}
}
fn mark(root: &mut Node, path: &[u32]) {
const ON_PATH: &str = "a collected path follows the tree";
fn cell(node: &mut Node) -> &mut ConsAltNode {
match &mut node.inner {
NodeInner::List(cons) | NodeInner::Alt(cons) => cons,
_ => unreachable!("{ON_PATH}"),
}
}
let mut node = root;
for &step in path {
node = if matches!(node.inner, NodeInner::List(_) | NodeInner::Alt(_)) {
for _ in 0..step {
node = cell(node).cdr.as_deref_mut().expect(ON_PATH);
}
&mut cell(node).car
} else {
match &mut node.inner {
NodeInner::Quant(QuantNode { body, .. }) | NodeInner::Bag(BagNode { body, .. }) => {
body.as_deref_mut().expect(ON_PATH)
}
_ => unreachable!("{ON_PATH}"),
}
};
}
if let NodeInner::Quant(qn) = &mut node.inner {
qn.possessive_star = true;
}
}
#[cfg(test)]
mod tests;