Skip to main content

libxml_rs/xml/regex/
mod.rs

1//! libxml2's internal regex engine (§85 Phase 7).
2//!
3//! libxml2 uses its own regex engine (xmlregexp.c) for XML Schema pattern
4//! facets, XSLT template match patterns, and other internal uses.
5//! Must match upstream behavior exactly.
6//!
7//! Implements an NFA-based regex engine using Thompson's construction:
8//! - Compilation: regex pattern → NFA
9//! - Execution: NFA simulation with state-set tracking
10//! - Incremental matching: push strings into an execution context
11//! - Determinism check
12//!
13//! # Upstream contract
14//!
15//! Mirrors upstream `xmlregexp.c` / `xmlregexp.h`
16//! (`SRC-LIBXML2-2.15.0-XMLREGEXP-C`, parity target libxml2 2.15.3
17//! oracle): `xmlRegexpCompile`, `xmlRegexpExec`, `xmlRegexpPrint`,
18//! `xmlRegFreeRegexp`, `xmlRegNewExecCtxt`, `xmlRegExecPushString` and the
19//! determinism check — the engine behind XML Schema pattern facets, RELAX
20//! NG and XSLT match patterns.
21//!
22//! # Conceptual behavior
23//!
24//! Implements an NFA-based regex engine using Thompson construction:
25//! compilation lowers the pattern to an NFA with epsilon transitions and
26//! character classes, execution simulates the state set, and the
27//! incremental context (`xmlRegExecCtxt`) accepts pushed string fragments
28//! — the automata module compiles its state machines down to this engine.
29//!
30//! # Ownership & safety invariants
31//!
32//! A compiled `XmlRegexp` is owned by its caller (freed with
33//! `xmlRegFreeRegexp`); execution contexts are owned separately and freed
34//! with `xmlRegFreeExecCtxt`. The automata builder stores the compiled
35//! regexp inside the automata and frees it with the automata. No global
36//! mutable state — compiled regexps are shareable across threads for
37//! execution.
38//!
39//! # Historical quirks & epochs
40//!
41//! The engine carries the CVE-2021-3541 fix lineage (SEC-0010) and is
42//! stable across the oracle matrix; the crate targets the 2.15.3 epoch,
43//! where schema-facet compilation and matching are byte-identical.
44//!
45//! # Deliberate oddities
46//!
47//! The determinism check is exposed as an API (xmlRegIsDeterministic /
48//! xmlAutomataIsDeterministic) and reports `not compiled = deterministic`
49//! in the automata layer — a faithful reproduction of the upstream return
50//! contract.
51//!
52//! # Proving courts
53//!
54//! Schema/RELAX NG differential courts compile and execute facets through
55//! this engine byte-identical against the oracle; cargo test runs the
56//! regexp unit suites.
57//!
58//! # Tempting simplifications that would break parity
59//!
60//! Do not replace the engine with the Rust `regex` crate: the XML Schema
61//! pattern syntax (including the limits and error returns) is not the
62//! regex-crate grammar, and incremental push matching has no counterpart.
63//! Do not drop the determinism API — schema compilation calls it.
64
65#![allow(
66    missing_docs,
67    non_snake_case,
68    non_camel_case_types,
69    non_upper_case_globals
70)]
71
72use core::ffi::c_void;
73use core::ptr;
74use std::os::raw::{c_char, c_int};
75
76use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl};
77use crate::abi::types::xmlChar;
78
79// ═══════════════════════════════════════════════════════════════════════════════
80// Constants
81// ═══════════════════════════════════════════════════════════════════════════════
82
83/// Maximum number of states in an NFA.
84#[allow(dead_code)]
85const MAX_NFA_STATES: usize = 1024;
86
87/// Maximum recursion depth for parsing.
88#[allow(dead_code)]
89const MAX_PARSE_DEPTH: usize = 256;
90
91/// Return value for a successful match.
92const REGEXP_MATCH: c_int = 1;
93
94/// Return value for no match.
95const REGEXP_NOMATCH: c_int = 0;
96
97/// Return value for an error.
98const REGEXP_ERROR: c_int = -1;
99
100// ═══════════════════════════════════════════════════════════════════════════════
101// Transition Types
102// ═══════════════════════════════════════════════════════════════════════════════
103
104/// Type of anchor in a transition.
105#[derive(Debug, Clone, Copy, PartialEq)]
106enum AnchorType {
107    /// Start-of-string anchor `^`
108    Start,
109    /// End-of-string anchor `$`
110    End,
111}
112
113/// Character class categories for predefined character classes.
114#[derive(Debug, Clone, Copy, PartialEq)]
115enum PredefinedClass {
116    /// `\d` — digit [0-9]
117    Digit,
118    /// `\D` — non-digit
119    NotDigit,
120    /// `\s` — whitespace
121    Space,
122    /// `\S` — non-whitespace
123    NotSpace,
124    /// `\w` — word character [A-Za-z0-9_]
125    Word,
126    /// `\W` — non-word character
127    NotWord,
128}
129
130/// A transition in the NFA.
131#[derive(Debug, Clone, PartialEq)]
132enum Transition {
133    /// Match a specific character.
134    Char(u8),
135    /// Match any character in a range [lo, hi].
136    Range(u8, u8),
137    /// Match any character in a set.
138    #[allow(dead_code)]
139    Set(Vec<u8>),
140    /// Match any character NOT in a range.
141    NotRange(u8, u8),
142    /// Match any character NOT in a set.
143    NotSet(Vec<u8>),
144    /// Match any character (`.` wildcard).
145    Wildcard,
146    /// Match a predefined character class.
147    Predefined(PredefinedClass),
148    /// Epsilon transition (consumes no input).
149    Epsilon,
150    /// Anchor transition (^ or $).
151    Anchor(AnchorType),
152}
153
154// ═══════════════════════════════════════════════════════════════════════════════
155// NFA Types
156// ═══════════════════════════════════════════════════════════════════════════════
157
158/// A single state in the NFA.
159#[derive(Debug, Clone)]
160struct NfaState {
161    /// Transitions from this state.
162    transitions: Vec<(Transition, usize)>,
163    /// Whether this is an accepting state.
164    is_accept: bool,
165}
166
167impl NfaState {
168    const fn new() -> Self {
169        NfaState {
170            transitions: Vec::new(),
171            is_accept: false,
172        }
173    }
174}
175
176/// A non-deterministic finite automaton.
177#[derive(Debug, Clone)]
178struct Nfa {
179    /// All states in the NFA.
180    states: Vec<NfaState>,
181    /// The start state index.
182    start: usize,
183}
184
185impl Nfa {
186    fn new() -> Self {
187        let start = 0;
188        Nfa {
189            states: vec![NfaState::new()],
190            start,
191        }
192    }
193
194    /// Add a new state to the NFA and return its index.
195    fn add_state(&mut self) -> usize {
196        let index = self.states.len();
197        self.states.push(NfaState::new());
198        index
199    }
200
201    /// Add a transition between two states.
202    fn add_transition(&mut self, from: usize, to: usize, trans: Transition) {
203        if from < self.states.len() && to < self.states.len() {
204            self.states[from].transitions.push((trans, to));
205        }
206    }
207
208    /// Set a state as accepting.
209    fn set_accept(&mut self, state: usize) {
210        if state < self.states.len() {
211            self.states[state].is_accept = true;
212        }
213    }
214}
215
216/// A fragment of an NFA during Thompson construction.
217///
218/// Tracks the fragment's NFA, its start state, and its "dangling" out states
219/// that need to be connected to the next fragment.
220struct NfaFragment {
221    nfa: Nfa,
222    /// The start state of this fragment.
223    start: usize,
224    /// Set of states that are "dangling" — they should be connected to the
225    /// next fragment in a concatenation, or become accepting in the final NFA.
226    /// These are the states that currently have no outgoing transitions to
227    /// the rest of the NFA.
228    out: Vec<usize>,
229}
230
231impl NfaFragment {
232    const fn new(nfa: Nfa, start: usize, out: Vec<usize>) -> Self {
233        NfaFragment { nfa, start, out }
234    }
235}
236
237impl Clone for NfaFragment {
238    fn clone(&self) -> Self {
239        NfaFragment {
240            nfa: self.nfa.clone(),
241            start: self.start,
242            out: self.out.clone(),
243        }
244    }
245}
246
247// ═══════════════════════════════════════════════════════════════════════════════
248// NFA Construction Primitives
249// ═══════════════════════════════════════════════════════════════════════════════
250
251/// Create an NFA fragment matching a single character.
252fn nfa_char(c: u8) -> NfaFragment {
253    let mut nfa = Nfa::new();
254    let start = nfa.start;
255    let accept = nfa.add_state();
256    nfa.set_accept(accept);
257    nfa.add_transition(start, accept, Transition::Char(c));
258    NfaFragment::new(nfa, start, vec![accept])
259}
260
261/// Create an NFA fragment matching an epsilon (empty string).
262fn nfa_epsilon() -> NfaFragment {
263    let mut nfa = Nfa::new();
264    let start = nfa.start;
265    nfa.set_accept(start);
266    NfaFragment::new(nfa, start, vec![start])
267}
268
269/// Create an NFA fragment matching a character range.
270fn nfa_range(lo: u8, hi: u8) -> NfaFragment {
271    let mut nfa = Nfa::new();
272    let start = nfa.start;
273    let accept = nfa.add_state();
274    nfa.set_accept(accept);
275    nfa.add_transition(start, accept, Transition::Range(lo, hi));
276    NfaFragment::new(nfa, start, vec![accept])
277}
278
279/// Create an NFA fragment matching a character set.
280#[allow(dead_code)]
281fn nfa_set(chars: Vec<u8>) -> NfaFragment {
282    let mut nfa = Nfa::new();
283    let start = nfa.start;
284    let accept = nfa.add_state();
285    nfa.set_accept(accept);
286    nfa.add_transition(start, accept, Transition::Set(chars));
287    NfaFragment::new(nfa, start, vec![accept])
288}
289
290/// Create an NFA fragment matching a NOT range.
291fn nfa_not_range(lo: u8, hi: u8) -> NfaFragment {
292    let mut nfa = Nfa::new();
293    let start = nfa.start;
294    let accept = nfa.add_state();
295    nfa.set_accept(accept);
296    nfa.add_transition(start, accept, Transition::NotRange(lo, hi));
297    NfaFragment::new(nfa, start, vec![accept])
298}
299
300/// Create an NFA fragment matching a NOT set.
301#[allow(dead_code)]
302fn nfa_not_set(chars: Vec<u8>) -> NfaFragment {
303    let mut nfa = Nfa::new();
304    let start = nfa.start;
305    let accept = nfa.add_state();
306    nfa.set_accept(accept);
307    nfa.add_transition(start, accept, Transition::NotSet(chars));
308    NfaFragment::new(nfa, start, vec![accept])
309}
310
311/// Create an NFA fragment matching a wildcard (`.`).
312fn nfa_wildcard() -> NfaFragment {
313    let mut nfa = Nfa::new();
314    let start = nfa.start;
315    let accept = nfa.add_state();
316    nfa.set_accept(accept);
317    nfa.add_transition(start, accept, Transition::Wildcard);
318    NfaFragment::new(nfa, start, vec![accept])
319}
320
321/// Create an NFA fragment matching a predefined class.
322fn nfa_predefined(class: PredefinedClass) -> NfaFragment {
323    let mut nfa = Nfa::new();
324    let start = nfa.start;
325    let accept = nfa.add_state();
326    nfa.set_accept(accept);
327    nfa.add_transition(start, accept, Transition::Predefined(class));
328    NfaFragment::new(nfa, start, vec![accept])
329}
330
331/// Create an NFA fragment for a start anchor `^`.
332fn nfa_start_anchor() -> NfaFragment {
333    let mut nfa = Nfa::new();
334    let start = nfa.start;
335    let accept = nfa.add_state();
336    nfa.set_accept(accept);
337    nfa.add_transition(start, accept, Transition::Anchor(AnchorType::Start));
338    NfaFragment::new(nfa, start, vec![accept])
339}
340
341/// Create an NFA fragment for an end anchor `$`.
342fn nfa_end_anchor() -> NfaFragment {
343    let mut nfa = Nfa::new();
344    let start = nfa.start;
345    let accept = nfa.add_state();
346    nfa.set_accept(accept);
347    nfa.add_transition(start, accept, Transition::Anchor(AnchorType::End));
348    NfaFragment::new(nfa, start, vec![accept])
349}
350
351/// Concatenate two NFA fragments: `a` followed by `b`.
352///
353/// Connects all dangling out states of `a` to the start state of `b`
354/// via epsilon transitions.
355fn concat(a: NfaFragment, b: NfaFragment) -> NfaFragment {
356    let a_out = a.out.clone();
357    let a_start = a.start;
358    let a_size = a.nfa.states.len();
359
360    let mut nfa = a.nfa;
361    let b_start = b.nfa.start + a_size;
362
363    // Adjust state indices in b's transitions and add b's states
364    for mut state in b.nfa.states {
365        for (_, target) in &mut state.transitions {
366            *target += a_size;
367        }
368        nfa.states.push(state);
369    }
370
371    // Connect a's out states to b's start via epsilon
372    for &out_state in &a_out {
373        nfa.add_transition(out_state, b_start, Transition::Epsilon);
374    }
375
376    // The out states of the concatenation are b's out states (with adjusted indices)
377    let b_out: Vec<usize> = b.out.iter().map(|&s| s + a_size).collect();
378
379    NfaFragment::new(nfa, a_start, b_out)
380}
381
382/// Union (alternation) of two NFA fragments: `a | b`.
383///
384/// Creates a new start state with epsilon transitions to both a and b.
385fn union(a: NfaFragment, b: NfaFragment) -> NfaFragment {
386    let mut nfa = Nfa::new();
387    let new_start = nfa.start;
388
389    // Add all states from a (adjusting indices)
390    let a_start = nfa.states.len();
391    let _a_size = a.nfa.states.len();
392    for mut state in a.nfa.states {
393        for (_, target) in &mut state.transitions {
394            *target += a_start;
395        }
396        nfa.states.push(state);
397    }
398
399    // Add all states from b (adjusting indices)
400    let b_start = nfa.states.len();
401    for mut state in b.nfa.states {
402        for (_, target) in &mut state.transitions {
403            *target += b_start;
404        }
405        nfa.states.push(state);
406    }
407
408    // Connect new start to a and b starts
409    nfa.add_transition(new_start, a_start, Transition::Epsilon);
410    nfa.add_transition(new_start, b_start, Transition::Epsilon);
411
412    // Out states are the out states of both a and b (adjusted)
413    let mut out = Vec::new();
414    for &s in &a.out {
415        out.push(s + a_start);
416    }
417    for &s in &b.out {
418        out.push(s + b_start);
419    }
420
421    NfaFragment::new(nfa, new_start, out)
422}
423
424/// Kleene star (zero or more repetitions): `a*`.
425fn kleene_star(frag: NfaFragment) -> NfaFragment {
426    let mut nfa = Nfa::new();
427    let new_start = nfa.start;
428    let new_accept = nfa.add_state();
429
430    // Add fragment states (adjusted)
431    let frag_start = nfa.states.len();
432    let _frag_size = frag.nfa.states.len();
433    for mut state in frag.nfa.states {
434        for (_, target) in &mut state.transitions {
435            *target += frag_start;
436        }
437        nfa.states.push(state);
438    }
439
440    // Epsilon from new_start to both new_accept (zero repetitions) and frag_start
441    nfa.add_transition(new_start, new_accept, Transition::Epsilon);
442    nfa.add_transition(new_start, frag_start, Transition::Epsilon);
443
444    // Epsilon from frag's out states to both frag_start (loop) and new_accept
445    for &s in &frag.out {
446        nfa.add_transition(s + frag_start, frag_start, Transition::Epsilon);
447        nfa.add_transition(s + frag_start, new_accept, Transition::Epsilon);
448    }
449
450    nfa.set_accept(new_accept);
451    NfaFragment::new(nfa, new_start, vec![new_accept])
452}
453
454/// One or more repetitions: `a+`.
455fn plus(frag: NfaFragment) -> NfaFragment {
456    let out_orig = frag.out.clone();
457    let _frag_start_orig = frag.start;
458
459    let mut nfa = Nfa::new();
460    let new_start = nfa.start;
461
462    // Add fragment states (adjusted)
463    let frag_start = nfa.states.len();
464    for mut state in frag.nfa.states {
465        for (_, target) in &mut state.transitions {
466            *target += frag_start;
467        }
468        nfa.states.push(state);
469    }
470
471    // Epsilon from new_start to frag_start (must match at least once)
472    nfa.add_transition(new_start, frag_start, Transition::Epsilon);
473
474    // Connect frag's out states back to frag's start for additional repetitions
475    for &s in &out_orig {
476        let adjusted = s + frag_start;
477        nfa.add_transition(adjusted, frag_start, Transition::Epsilon);
478    }
479
480    // Out states are the adjusted original out states
481    let out: Vec<usize> = out_orig.iter().map(|&s| s + frag_start).collect();
482
483    NfaFragment::new(nfa, new_start, out)
484}
485
486/// Optional: `a?`.
487fn optional(frag: NfaFragment) -> NfaFragment {
488    let mut nfa = Nfa::new();
489    let new_start = nfa.start;
490    let new_accept = nfa.add_state();
491
492    let frag_start = nfa.states.len();
493    for mut state in frag.nfa.states {
494        for (_, target) in &mut state.transitions {
495            *target += frag_start;
496        }
497        nfa.states.push(state);
498    }
499
500    // Epsilon from new_start to both new_accept (skip) and frag_start
501    nfa.add_transition(new_start, new_accept, Transition::Epsilon);
502    nfa.add_transition(new_start, frag_start, Transition::Epsilon);
503
504    // Epsilon from frag's out to new_accept
505    for &s in &frag.out {
506        nfa.add_transition(s + frag_start, new_accept, Transition::Epsilon);
507    }
508
509    nfa.set_accept(new_accept);
510    NfaFragment::new(nfa, new_start, vec![new_accept])
511}
512
513// ═══════════════════════════════════════════════════════════════════════════════
514// Regex Parser
515// ═══════════════════════════════════════════════════════════════════════════════
516
517/// Token types for the regex parser.
518#[derive(Debug, Clone, PartialEq)]
519#[allow(dead_code)]
520enum RegexToken {
521    /// A literal character
522    Char(u8),
523    /// Wildcard (`.`)
524    Dot,
525    /// Start anchor (`^`)
526    AnchorStart,
527    /// End anchor (`$`)
528    AnchorEnd,
529    /// Left parenthesis
530    LParen,
531    /// Right parenthesis
532    RParen,
533    /// Alternation (`|`)
534    Pipe,
535    /// Zero or more (`*`)
536    Star,
537    /// One or more (`+`)
538    Plus,
539    /// Optional (`?`)
540    Question,
541    /// Left brace for quantifier
542    LBrace,
543    /// Right brace for quantifier
544    RBrace,
545    /// Comma in quantifier
546    Comma,
547    /// Number in quantifier
548    #[allow(dead_code)]
549    Number(u32),
550    /// Escape sequence
551    Escape(u8),
552    /// Character class start `[`
553    ClassStart,
554    /// Character class end `]`
555    ClassEnd,
556    /// Negation in character class `^`
557    ClassNegate,
558    /// Range in character class `-`
559    ClassRange,
560}
561
562/// Regex parser state.
563struct RegexParser<'a> {
564    /// Input pattern bytes.
565    input: &'a [u8],
566    /// Current position in input.
567    pos: usize,
568    /// Lookahead token.
569    lookahead: Option<RegexToken>,
570}
571
572impl<'a> RegexParser<'a> {
573    const fn new(input: &'a [u8]) -> Self {
574        RegexParser {
575            input,
576            pos: 0,
577            lookahead: None,
578        }
579    }
580
581    /// Peek at the next character without consuming it.
582    fn peek(&self) -> Option<u8> {
583        self.input.get(self.pos).copied()
584    }
585
586    /// Advance and return the next character.
587    fn advance(&mut self) -> Option<u8> {
588        let ch = self.input.get(self.pos).copied();
589        if ch.is_some() {
590            self.pos += 1;
591        }
592        ch
593    }
594
595    /// Skip whitespace in the pattern.
596    #[allow(dead_code)]
597    fn skip_whitespace(&mut self) {
598        while let Some(ch) = self.peek() {
599            if ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r' {
600                self.advance();
601            } else {
602                break;
603            }
604        }
605    }
606
607    /// Parse the next token and store it as lookahead.
608    fn scan_token(&mut self) -> Option<RegexToken> {
609        let ch = self.advance()?;
610        match ch {
611            b'.' => Some(RegexToken::Dot),
612            b'^' => Some(RegexToken::AnchorStart),
613            b'$' => Some(RegexToken::AnchorEnd),
614            b'(' => Some(RegexToken::LParen),
615            b')' => Some(RegexToken::RParen),
616            b'|' => Some(RegexToken::Pipe),
617            b'*' => Some(RegexToken::Star),
618            b'+' => Some(RegexToken::Plus),
619            b'?' => Some(RegexToken::Question),
620            b'{' => Some(RegexToken::LBrace),
621            b'}' => Some(RegexToken::RBrace),
622            b',' => Some(RegexToken::Comma),
623            b'[' => Some(RegexToken::ClassStart),
624            b']' => Some(RegexToken::ClassEnd),
625            b'\\' => {
626                // Escape sequence
627                let next = self.advance()?;
628                Some(RegexToken::Escape(next))
629            }
630            _ => Some(RegexToken::Char(ch)),
631        }
632    }
633
634    /// Get the next token (from lookahead or by scanning).
635    fn next_token(&mut self) -> Option<RegexToken> {
636        if let Some(token) = self.lookahead.take() {
637            Some(token)
638        } else {
639            self.scan_token()
640        }
641    }
642
643    /// Push back a token as lookahead.
644    const fn unscan(&mut self, token: RegexToken) {
645        self.lookahead = Some(token);
646    }
647
648    // ── Parsing ──────────────────────────────────────────────────────────
649
650    /// Parse the entire pattern into an NFA fragment.
651    fn parse(&mut self) -> Result<NfaFragment, String> {
652        let frag = self.parse_alternation()?;
653        Ok(frag)
654    }
655
656    /// Parse alternation: `expr | expr | ...`
657    fn parse_alternation(&mut self) -> Result<NfaFragment, String> {
658        let mut frag = self.parse_sequence()?;
659
660        loop {
661            match self.next_token() {
662                Some(RegexToken::Pipe) => {
663                    let rhs = self.parse_sequence()?;
664                    frag = union(frag, rhs);
665                }
666                Some(other) => {
667                    self.unscan(other);
668                    break;
669                }
670                None => break,
671            }
672        }
673
674        Ok(frag)
675    }
676
677    /// Parse a sequence of quantified atoms.
678    fn parse_sequence(&mut self) -> Result<NfaFragment, String> {
679        let mut fragments: Vec<NfaFragment> = Vec::new();
680
681        loop {
682            match self.peek() {
683                None => break,
684                Some(b'|') | Some(b')') => break,
685                _ => {}
686            }
687
688            let atom = self.parse_atom()?;
689            fragments.push(atom);
690        }
691
692        if fragments.is_empty() {
693            return Ok(nfa_epsilon());
694        }
695
696        let mut result = fragments.remove(0);
697        for frag in fragments {
698            result = concat(result, frag);
699        }
700
701        Ok(result)
702    }
703
704    /// Parse an atom (possibly with quantifier).
705    fn parse_atom(&mut self) -> Result<NfaFragment, String> {
706        let token = self
707            .next_token()
708            .ok_or_else(|| "Unexpected end of pattern".to_string())?;
709
710        let base = match token {
711            RegexToken::Char(c) => nfa_char(c),
712            RegexToken::Dot => nfa_wildcard(),
713            RegexToken::AnchorStart => nfa_start_anchor(),
714            RegexToken::AnchorEnd => nfa_end_anchor(),
715            RegexToken::LParen => {
716                let inner = self.parse_alternation()?;
717                match self.next_token() {
718                    Some(RegexToken::RParen) => inner,
719                    Some(t) => return Err(format!("Expected ')', got {:?}", t)),
720                    None => return Err("Unterminated group".to_string()),
721                }
722            }
723            RegexToken::Escape(c) => {
724                match c {
725                    b'd' => nfa_predefined(PredefinedClass::Digit),
726                    b'D' => nfa_predefined(PredefinedClass::NotDigit),
727                    b's' => nfa_predefined(PredefinedClass::Space),
728                    b'S' => nfa_predefined(PredefinedClass::NotSpace),
729                    b'w' => nfa_predefined(PredefinedClass::Word),
730                    b'W' => nfa_predefined(PredefinedClass::NotWord),
731                    b'n' => nfa_char(b'\n'),
732                    b'r' => nfa_char(b'\r'),
733                    b't' => nfa_char(b'\t'),
734                    b'\\' => nfa_char(b'\\'),
735                    b'.' => nfa_char(b'.'),
736                    b'^' => nfa_char(b'^'),
737                    b'$' => nfa_char(b'$'),
738                    b'|' => nfa_char(b'|'),
739                    b'*' => nfa_char(b'*'),
740                    b'+' => nfa_char(b'+'),
741                    b'?' => nfa_char(b'?'),
742                    b'(' => nfa_char(b'('),
743                    b')' => nfa_char(b')'),
744                    b'[' => nfa_char(b'['),
745                    b']' => nfa_char(b']'),
746                    b'{' => nfa_char(b'{'),
747                    b'}' => nfa_char(b'}'),
748                    b'-' => nfa_char(b'-'),
749                    b'0'..=b'9' => {
750                        // Backreference or octal - treat as literal for now
751                        nfa_char(c)
752                    }
753                    _ => nfa_char(c),
754                }
755            }
756            RegexToken::ClassStart => self.parse_char_class()?,
757            _ => return Err(format!("Unexpected token: {:?}", token)),
758        };
759
760        // Check for quantifier
761        self.parse_quantifier(base)
762    }
763
764    /// Parse a quantifier after an atom.
765    fn parse_quantifier(&mut self, frag: NfaFragment) -> Result<NfaFragment, String> {
766        match self.peek() {
767            Some(b'*') => {
768                self.advance();
769                Ok(kleene_star(frag))
770            }
771            Some(b'+') => {
772                self.advance();
773                Ok(plus(frag))
774            }
775            Some(b'?') => {
776                self.advance();
777                Ok(optional(frag))
778            }
779            Some(b'{') => {
780                self.advance();
781                self.parse_brace_quantifier(frag)
782            }
783            _ => Ok(frag),
784        }
785    }
786
787    /// Parse a brace quantifier `{n}`, `{n,}`, or `{n,m}`.
788    fn parse_brace_quantifier(&mut self, frag: NfaFragment) -> Result<NfaFragment, String> {
789        // Parse minimum
790        let mut min: u32 = 0;
791        while let Some(b'0'..=b'9') = self.peek() {
792            let d = self.advance().unwrap() - b'0';
793            min = min * 10 + d as u32;
794        }
795
796        let mut max: Option<u32> = None;
797
798        match self.peek() {
799            Some(b',') => {
800                self.advance();
801                // Parse maximum
802                let mut max_val: u32 = 0;
803                let mut has_max = false;
804                while let Some(b'0'..=b'9') = self.peek() {
805                    let d = self.advance().unwrap() - b'0';
806                    max_val = max_val * 10 + d as u32;
807                    has_max = true;
808                }
809                if has_max {
810                    max = Some(max_val);
811                }
812            }
813            Some(b'}') => {
814                max = Some(min);
815            }
816            _ => return Err("Expected '}' in quantifier".to_string()),
817        }
818
819        // Expect closing brace
820        match self.peek() {
821            Some(b'}') => {
822                self.advance();
823            }
824            _ => return Err("Expected '}' in quantifier".to_string()),
825        }
826
827        // Build the quantifier NFA
828        // {n} = exactly n repetitions
829        // {n,} = at least n repetitions
830        // {n,m} = between n and m repetitions
831        if min == 0 && max.is_none() {
832            // {0,} = *
833            Ok(kleene_star(frag))
834        } else if min == 0 && max == Some(0) {
835            // {0} = empty
836            Ok(nfa_epsilon())
837        } else if min == 1 && max.is_none() {
838            // {1,} = +
839            Ok(plus(frag))
840        } else if min == 0 && max == Some(1) {
841            // {0,1} = ?
842            Ok(optional(frag))
843        } else {
844            // General case: build concatenation of min repetitions,
845            // plus optional repetitions for max
846            let mut result = nfa_epsilon();
847            for _ in 0..min {
848                result = concat(result, frag.clone());
849            }
850            if let Some(max_val) = max {
851                for _ in min..max_val {
852                    result = concat(result, optional(frag.clone()));
853                }
854            } else {
855                // At least min, then zero or more
856                result = concat(result, kleene_star(frag.clone()));
857            }
858            Ok(result)
859        }
860    }
861
862    /// Parse a character class `[...]` or `[^...]`.
863    fn parse_char_class(&mut self) -> Result<NfaFragment, String> {
864        let mut chars: Vec<u8> = Vec::new();
865        let mut ranges: Vec<(u8, u8)> = Vec::new();
866        let mut negated = false;
867
868        // Check for negation
869        if let Some(b'^') = self.peek() {
870            negated = true;
871            self.advance();
872        }
873
874        // Parse character class contents
875        let mut prev: Option<u8> = None;
876
877        loop {
878            match self.peek() {
879                None => return Err("Unterminated character class".to_string()),
880                Some(b']') => {
881                    if prev.is_some() {
882                        // Treat '-' before ']' as literal
883                        if let Some(c) = prev {
884                            chars.push(c);
885                        }
886                        prev = None;
887                    }
888                    self.advance();
889                    break;
890                }
891                Some(b'-') if prev.is_some() => {
892                    // Range operator
893                    self.advance();
894                    let lo = prev.take().unwrap();
895                    match self.peek() {
896                        Some(b']') => {
897                            // '-' before ']' is literal
898                            chars.push(lo);
899                            chars.push(b'-');
900                            prev = None;
901                        }
902                        Some(ch) => {
903                            self.advance();
904                            if lo <= ch {
905                                ranges.push((lo, ch));
906                            }
907                            prev = None;
908                        }
909                        None => {
910                            chars.push(lo);
911                            chars.push(b'-');
912                            prev = None;
913                        }
914                    }
915                }
916                Some(b'\\') => {
917                    self.advance();
918                    if let Some(ch) = self.advance() {
919                        // UPSTREAM-PARITY: Inside character classes, \d, \D, \s, \S,
920                        // \w, \W create predefined class transitions rather than
921                        // literal character matches.
922                        match ch {
923                            b'd' | b'D' | b's' | b'S' | b'w' | b'W' => {
924                                // Push any pending prev first
925                                if let Some(p) = prev.take() {
926                                    chars.push(p);
927                                }
928                                // We can't directly return a predefined fragment here
929                                // because we're in the middle of parsing. Instead, convert
930                                // the predefined class to its equivalent range/chars.
931                                let class = match ch {
932                                    b'd' => PredefinedClass::Digit,
933                                    b'D' => PredefinedClass::NotDigit,
934                                    b's' => PredefinedClass::Space,
935                                    b'S' => PredefinedClass::NotSpace,
936                                    b'w' => PredefinedClass::Word,
937                                    b'W' => PredefinedClass::NotWord,
938                                    _ => unreachable!(),
939                                };
940                                // Add the predefined class equivalent ranges
941                                // We'll handle this after the loop
942                                // Store a sentinel: we'll push special entries
943                                // For now, just add digit ranges
944                                match class {
945                                    PredefinedClass::Digit => {
946                                        ranges.push((b'0', b'9'));
947                                    }
948                                    PredefinedClass::NotDigit => {
949                                        ranges.push((0x00, b'/' - 1));
950                                        ranges.push((b':', 0xFF));
951                                    }
952                                    PredefinedClass::Space => {
953                                        chars.push(b' ');
954                                        chars.push(b'\t');
955                                        chars.push(b'\n');
956                                        chars.push(b'\r');
957                                    }
958                                    PredefinedClass::NotSpace => {
959                                        // Everything except space, tab, newline, carriage return
960                                        ranges.push((0x00, b' ' - 1));
961                                        ranges.push((b'!' + 1, b'\t' - 1));
962                                        ranges.push((b'\t' + 1, b'\n' - 1));
963                                        ranges.push((b'\n' + 1, b'\r' - 1));
964                                        ranges.push((b'\r' + 1, 0xFF));
965                                    }
966                                    PredefinedClass::Word => {
967                                        ranges.push((b'0', b'9'));
968                                        ranges.push((b'A', b'Z'));
969                                        ranges.push((b'a', b'z'));
970                                        chars.push(b'_');
971                                    }
972                                    PredefinedClass::NotWord => {
973                                        ranges.push((0x00, b'0' - 1));
974                                        ranges.push((b'9' + 1, b'A' - 1));
975                                        ranges.push((b'Z' + 1, b'_' - 1));
976                                        ranges.push((b'_' + 1, b'a' - 1));
977                                        ranges.push((b'z' + 1, 0xFF));
978                                    }
979                                }
980                            }
981                            _ => {
982                                let c = match ch {
983                                    b'n' => b'\n',
984                                    b'r' => b'\r',
985                                    b't' => b'\t',
986                                    b'\\' => b'\\',
987                                    b'0'..=b'9' => ch, // treat as literal
988                                    _ => ch,
989                                };
990                                if let Some(p) = prev.take() {
991                                    chars.push(p);
992                                }
993                                prev = Some(c);
994                            }
995                        }
996                    }
997                }
998                Some(ch) => {
999                    self.advance();
1000                    if let Some(p) = prev.take() {
1001                        chars.push(p);
1002                    }
1003                    prev = Some(ch);
1004                }
1005            }
1006        }
1007
1008        // Push any remaining prev
1009        if let Some(c) = prev {
1010            chars.push(c);
1011        }
1012
1013        // Build the NFA fragment
1014        let mut combined_ranges: Vec<(u8, u8)> = ranges;
1015        for &c in &chars {
1016            combined_ranges.push((c, c));
1017        }
1018
1019        if combined_ranges.is_empty() {
1020            return if negated {
1021                // [^] matches nothing? Actually [^] is invalid in XML regex
1022                Ok(nfa_epsilon())
1023            } else {
1024                // [] is invalid, treat as empty
1025                Ok(nfa_epsilon())
1026            };
1027        }
1028
1029        // Merge overlapping/consecutive ranges
1030        combined_ranges.sort_by_key(|a| a.0);
1031        let mut merged: Vec<(u8, u8)> = Vec::new();
1032        for (lo, hi) in combined_ranges {
1033            if let Some(last) = merged.last_mut() {
1034                if lo <= last.1 + 1 {
1035                    last.1 = last.1.max(hi);
1036                    continue;
1037                }
1038            }
1039            merged.push((lo, hi));
1040        }
1041
1042        // Build fragment from merged ranges
1043        if negated {
1044            if merged.len() == 1 && merged[0].0 == 0 && merged[0].1 == 255 {
1045                // [^...] with everything is impossible — empty
1046                Ok(nfa_epsilon())
1047            } else {
1048                Ok(nfa_not_range(0, 255))
1049            }
1050        } else if merged.len() == 1 {
1051            let (lo, hi) = merged[0];
1052            if lo == hi {
1053                Ok(nfa_char(lo))
1054            } else {
1055                Ok(nfa_range(lo, hi))
1056            }
1057        } else {
1058            // Multiple ranges: union them
1059            let mut result = nfa_range(merged[0].0, merged[0].1);
1060            for &(lo, hi) in &merged[1..] {
1061                result = union(result, nfa_range(lo, hi));
1062            }
1063            Ok(result)
1064        }
1065    }
1066}
1067
1068// ═══════════════════════════════════════════════════════════════════════════════
1069// NFA Simulation — Character Class Matching
1070// ═══════════════════════════════════════════════════════════════════════════════
1071
1072/// Check if a byte matches a given transition.
1073fn matches_transition(c: u8, trans: &Transition) -> bool {
1074    match trans {
1075        Transition::Char(ch) => c == *ch,
1076        Transition::Range(lo, hi) => c >= *lo && c <= *hi,
1077        Transition::Set(chars) => chars.contains(&c),
1078        Transition::NotRange(lo, hi) => c < *lo || c > *hi,
1079        Transition::NotSet(chars) => !chars.contains(&c),
1080        Transition::Wildcard => true,
1081        Transition::Predefined(class) => matches_predefined(c, *class),
1082        Transition::Epsilon => false,   // Epsilon handled separately
1083        Transition::Anchor(_) => false, // Anchors handled separately
1084    }
1085}
1086
1087/// Check if a character matches a predefined class.
1088const fn matches_predefined(c: u8, class: PredefinedClass) -> bool {
1089    match class {
1090        PredefinedClass::Digit => c >= b'0' && c <= b'9',
1091        PredefinedClass::NotDigit => c < b'0' || c > b'9',
1092        PredefinedClass::Space => {
1093            c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' || c == 0x0b || c == 0x0c
1094        }
1095        PredefinedClass::NotSpace => {
1096            !(c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' || c == 0x0b || c == 0x0c)
1097        }
1098        PredefinedClass::Word => {
1099            (c >= b'0' && c <= b'9')
1100                || (c >= b'A' && c <= b'Z')
1101                || (c >= b'a' && c <= b'z')
1102                || c == b'_'
1103        }
1104        PredefinedClass::NotWord => {
1105            !((c >= b'0' && c <= b'9')
1106                || (c >= b'A' && c <= b'Z')
1107                || (c >= b'a' && c <= b'z')
1108                || c == b'_')
1109        }
1110    }
1111}
1112
1113/// Compute the epsilon closure of a set of states.
1114fn epsilon_closure(nfa: &Nfa, states: &[usize]) -> Vec<usize> {
1115    let mut visited = vec![false; nfa.states.len()];
1116    let mut result = Vec::new();
1117    let mut stack: Vec<usize> = states.to_vec();
1118
1119    while let Some(s) = stack.pop() {
1120        if s >= nfa.states.len() || visited[s] {
1121            continue;
1122        }
1123        visited[s] = true;
1124        result.push(s);
1125
1126        for (cond, target) in &nfa.states[s].transitions {
1127            if matches!(cond, Transition::Epsilon)
1128                && *target < nfa.states.len()
1129                && !visited[*target]
1130            {
1131                stack.push(*target);
1132            }
1133        }
1134    }
1135
1136    result
1137}
1138
1139/// Move from a set of states on a single character.
1140///
1141/// Returns all states reachable from any state in `states` by consuming `c`.
1142/// First follows non-consuming transitions (anchors), then character transitions.
1143fn move_on_char(nfa: &Nfa, states: &[usize], c: u8, is_start: bool, is_end: bool) -> Vec<usize> {
1144    // Step 1: Follow non-consuming transitions (anchors) to expand
1145    // the set of states, without consuming the character.
1146    let mut expanded = states.to_vec();
1147    let mut more = true;
1148    while more {
1149        more = false;
1150        let mut new_states = Vec::new();
1151        for &s in &expanded {
1152            if s >= nfa.states.len() {
1153                continue;
1154            }
1155            for (cond, target) in &nfa.states[s].transitions {
1156                if *target >= nfa.states.len() {
1157                    continue;
1158                }
1159                let should_follow = match cond {
1160                    Transition::Anchor(AnchorType::Start) => {
1161                        is_start && !expanded.contains(target) && !new_states.contains(target)
1162                    }
1163                    Transition::Anchor(AnchorType::End) => {
1164                        is_end && !expanded.contains(target) && !new_states.contains(target)
1165                    }
1166                    _ => false,
1167                };
1168                if should_follow {
1169                    new_states.push(*target);
1170                    more = true;
1171                }
1172            }
1173        }
1174        expanded.extend(new_states);
1175    }
1176
1177    // Step 1.5: Follow epsilon transitions from the expanded set.
1178    // Anchor states may connect to character-consuming states via epsilon
1179    // (introduced by concat()). We must traverse these before consuming input.
1180    let mut eps_expanded = expanded.clone();
1181    let mut more_eps = true;
1182    while more_eps {
1183        more_eps = false;
1184        let mut new_eps = Vec::new();
1185        for &s in &eps_expanded {
1186            if s >= nfa.states.len() {
1187                continue;
1188            }
1189            for (cond, target) in &nfa.states[s].transitions {
1190                if let Transition::Epsilon = cond {
1191                    if *target < nfa.states.len()
1192                        && !eps_expanded.contains(target)
1193                        && !new_eps.contains(target)
1194                    {
1195                        new_eps.push(*target);
1196                        more_eps = true;
1197                    }
1198                }
1199            }
1200        }
1201        eps_expanded.extend(new_eps);
1202    }
1203
1204    // Step 2: From the fully expanded set, follow character-consuming transitions.
1205    let mut next = Vec::new();
1206    for &s in &eps_expanded {
1207        if s >= nfa.states.len() {
1208            continue;
1209        }
1210        for (cond, target) in &nfa.states[s].transitions {
1211            if *target >= nfa.states.len() {
1212                continue;
1213            }
1214            match cond {
1215                Transition::Epsilon => continue,
1216                Transition::Anchor(_) => continue, // already handled above
1217                _ => {
1218                    if matches_transition(c, cond) && !next.contains(target) {
1219                        next.push(*target);
1220                    }
1221                }
1222            }
1223        }
1224    }
1225
1226    next
1227}
1228
1229/// Check if any state in the set is an accepting state.
1230fn has_accept_state(nfa: &Nfa, states: &[usize]) -> bool {
1231    states
1232        .iter()
1233        .any(|&s| s < nfa.states.len() && nfa.states[s].is_accept)
1234}
1235
1236/// Execute an NFA against a byte string (full match).
1237///
1238/// Returns `REGEXP_MATCH` (1) if the entire string matches,
1239/// `REGEXP_NOMATCH` (0) if it doesn't, or `REGEXP_ERROR` (-1) on error.
1240fn nfa_exec(nfa: &Nfa, input: &[u8]) -> c_int {
1241    if nfa.states.is_empty() {
1242        return REGEXP_ERROR;
1243    }
1244
1245    // Start with epsilon closure of the start state.
1246    let mut current = epsilon_closure(nfa, &[nfa.start]);
1247
1248    // If input is empty, check immediately if we're in an accept state.
1249    // Also handle end anchor ($), which matches the end of input (empty string at end).
1250    if input.is_empty() {
1251        let mut final_states = current.clone();
1252        for &s in &current {
1253            for (cond, target) in &nfa.states[s].transitions {
1254                if let Transition::Anchor(AnchorType::End) = cond {
1255                    if *target < nfa.states.len() {
1256                        let ec = epsilon_closure(nfa, &[*target]);
1257                        final_states.extend(ec);
1258                    }
1259                }
1260            }
1261        }
1262        final_states = epsilon_closure(nfa, &final_states);
1263        return if has_accept_state(nfa, &final_states) {
1264            REGEXP_MATCH
1265        } else {
1266            REGEXP_NOMATCH
1267        };
1268    }
1269
1270    for (i, &c) in input.iter().enumerate() {
1271        let is_start = i == 0;
1272        // Move on this character. move_on_char handles both character
1273        // transitions and anchor transitions (^ at start, $ at end).
1274        let next_states = move_on_char(nfa, &current, c, is_start, false);
1275        if next_states.is_empty() {
1276            return REGEXP_NOMATCH;
1277        }
1278
1279        current = epsilon_closure(nfa, &next_states);
1280        if current.is_empty() {
1281            return REGEXP_NOMATCH;
1282        }
1283    }
1284
1285    // After consuming all input, check if we can reach an accept state.
1286    // Also handle end anchor ($) by checking if any state can transition
1287    // to an accept state via end anchor.
1288    let mut final_states = current.clone();
1289    for &s in &current {
1290        for (cond, target) in &nfa.states[s].transitions {
1291            if let Transition::Anchor(AnchorType::End) = cond {
1292                if *target < nfa.states.len() {
1293                    let ec = epsilon_closure(nfa, &[*target]);
1294                    final_states.extend(ec);
1295                }
1296            }
1297        }
1298    }
1299    final_states = epsilon_closure(nfa, &final_states);
1300
1301    if has_accept_state(nfa, &final_states) {
1302        REGEXP_MATCH
1303    } else {
1304        REGEXP_NOMATCH
1305    }
1306}
1307
1308// ═══════════════════════════════════════════════════════════════════════════════
1309// XmlRegexp — Compiled Regex Type
1310// ═══════════════════════════════════════════════════════════════════════════════
1311
1312/// Compiled regular expression.
1313///
1314/// # UPSTREAM-PARITY
1315///
1316/// Corresponds to `xmlRegexpPtr` / `_xmlRegexp` in libxml2.
1317#[derive(Debug)]
1318#[repr(C)]
1319pub struct XmlRegexp {
1320    /// The original pattern string (null-terminated xmlChar*).
1321    pattern: *mut xmlChar,
1322    /// The internal NFA representation.
1323    nfa: Option<Box<Nfa>>,
1324    /// Whether the regex is deterministic.
1325    is_deterministic: c_int,
1326}
1327
1328/// Compile a regex pattern into an NFA.
1329fn compile_pattern(pattern: &[u8]) -> Option<Box<Nfa>> {
1330    if pattern.is_empty() {
1331        // Empty pattern matches empty string
1332        let mut nfa = Nfa::new();
1333        nfa.set_accept(nfa.start);
1334        return Some(Box::new(nfa));
1335    }
1336
1337    let mut parser = RegexParser::new(pattern);
1338    match parser.parse() {
1339        Ok(fragment) => {
1340            // UPSTREAM-PARITY: Clear ALL accept flags first, then set only the
1341            // fragment's final out states as accepting. This ensures that inner
1342            // fragment accept states (from kleene_star, plus, etc.) are not
1343            // incorrectly treated as accepting when they are connected to
1344            // subsequent fragments via concatenation.
1345            let mut nfa = fragment.nfa;
1346            for state in &mut nfa.states {
1347                state.is_accept = false;
1348            }
1349            for &s in &fragment.out {
1350                if s < nfa.states.len() {
1351                    nfa.set_accept(s);
1352                }
1353            }
1354            Some(Box::new(nfa))
1355        }
1356        Err(_) => None,
1357    }
1358}
1359
1360/// Check if an NFA is deterministic.
1361fn is_deterministic(nfa: &Nfa) -> bool {
1362    // UPSTREAM-PARITY: An NFA is deterministic if, for every state, there is
1363    // at most one transition that can match any given input character.
1364    //
1365    // Key rules:
1366    // 1. Epsilon transitions from concatenation are fine (they chain linearly).
1367    // 2. Multiple epsilon transitions from the same state (alternation) make
1368    //    it non-deterministic.
1369    // 3. An epsilon + character-consuming transition from the same state
1370    //    makes it non-deterministic (engine must choose).
1371    // 4. Overlapping character-consuming transitions (e.g., 'a' and range('a','z'))
1372    //    make it non-deterministic.
1373    //
1374    // We check each state individually. For states with only epsilon transitions,
1375    // we check if there's exactly one (which is fine, it's a pass-through from
1376    // concatenation) or multiple (which is non-deterministic from alternation).
1377    //
1378    // For states with non-epsilon transitions, we check for overlap.
1379    // We also consider the epsilon-closure: if a state has an epsilon to another
1380    // state, the character-consuming transitions of both states must not overlap.
1381
1382    for (state_idx, state) in nfa.states.iter().enumerate() {
1383        // Count epsilon transitions from this state directly.
1384        let epsilon_count = state
1385            .transitions
1386            .iter()
1387            .filter(|(cond, _)| matches!(cond, Transition::Epsilon))
1388            .count();
1389
1390        // Count non-epsilon, non-anchor transitions.
1391        let consuming_count = state
1392            .transitions
1393            .iter()
1394            .filter(|(cond, _)| !matches!(cond, Transition::Epsilon | Transition::Anchor(_)))
1395            .count();
1396
1397        // Rule 2: Multiple epsilon transitions from same state = non-deterministic.
1398        if epsilon_count > 1 {
1399            return false;
1400        }
1401
1402        // Rule 3: Epsilon + consuming from same state = non-deterministic.
1403        if epsilon_count > 0 && consuming_count > 0 {
1404            return false;
1405        }
1406
1407        // Collect all consuming transitions (direct + via epsilon closure).
1408        // But only if there are no epsilon transitions (otherwise already caught).
1409        let closure = if epsilon_count == 0 {
1410            epsilon_closure(nfa, &[state_idx])
1411        } else {
1412            // Already handled above; skip detailed check.
1413            continue;
1414        };
1415
1416        let mut has_wildcard = false;
1417        let mut chars = Vec::new();
1418        let mut has_range = false;
1419        let mut has_not = false;
1420        let mut has_predefined = false;
1421
1422        for &s_idx in &closure {
1423            if s_idx >= nfa.states.len() {
1424                continue;
1425            }
1426            for (cond, _) in &nfa.states[s_idx].transitions {
1427                match cond {
1428                    Transition::Epsilon | Transition::Anchor(_) => {}
1429                    Transition::Wildcard => {
1430                        if has_wildcard {
1431                            return false;
1432                        }
1433                        has_wildcard = true;
1434                    }
1435                    Transition::Char(c) => {
1436                        chars.push(*c);
1437                    }
1438                    Transition::Range(_, _) => {
1439                        has_range = true;
1440                    }
1441                    Transition::Set(_) => {}
1442                    Transition::NotRange(_, _) | Transition::NotSet(_) => {
1443                        has_not = true;
1444                    }
1445                    Transition::Predefined(_) => {
1446                        has_predefined = true;
1447                    }
1448                }
1449            }
1450        }
1451
1452        // Rule 4: Check for overlapping character-consuming transitions.
1453        if has_wildcard && (!chars.is_empty() || has_range || has_not || has_predefined) {
1454            return false;
1455        }
1456
1457        if has_range && (has_wildcard || has_not || has_predefined) {
1458            return false;
1459        }
1460
1461        if has_not && (has_wildcard || has_range || has_predefined) {
1462            return false;
1463        }
1464
1465        if has_predefined && (has_wildcard || has_range || has_not) {
1466            return false;
1467        }
1468
1469        // Check for duplicate characters.
1470        chars.sort();
1471        let dedup_len = {
1472            chars.dedup();
1473            chars.len()
1474        };
1475        // If we had chars and they deduplicated to fewer, there were duplicates.
1476        // But actually the issue is: multiple chars on same state are fine
1477        // (each is a distinct transition), but a single char appearing twice
1478        // would indicate non-determinism. Since we already dedup'd, if length
1479        // before dedup > length after dedup, there were duplicates.
1480        // We don't have the original length here easily.
1481        // Instead: if we have multiple distinct chars AND any other type,
1482        // it's non-deterministic.
1483        if dedup_len > 1 && (has_range || has_wildcard || has_not || has_predefined) {
1484            return false;
1485        }
1486    }
1487
1488    true
1489}
1490
1491// ═══════════════════════════════════════════════════════════════════════════════
1492// C ABI Functions
1493// ═══════════════════════════════════════════════════════════════════════════════
1494
1495/// xmlChar* helper: get length of null-terminated xmlChar string.
1496const unsafe fn xml_strlen(s: *const xmlChar) -> usize {
1497    if s.is_null() {
1498        return 0;
1499    }
1500    let mut len: usize = 0;
1501    while *s.add(len) != 0 {
1502        len += 1;
1503    }
1504    len
1505}
1506
1507/// xmlChar* helper: duplicate a null-terminated xmlChar string.
1508unsafe fn xml_strdup(s: *const xmlChar) -> *mut xmlChar {
1509    if s.is_null() {
1510        return ptr::null_mut();
1511    }
1512    let len = xml_strlen(s);
1513    let new_ptr = xmlMallocImpl((len + 1) * core::mem::size_of::<xmlChar>()) as *mut xmlChar;
1514    if new_ptr.is_null() {
1515        return ptr::null_mut();
1516    }
1517    for i in 0..=len {
1518        unsafe { *new_ptr.add(i) = *s.add(i) };
1519    }
1520    new_ptr
1521}
1522
1523/// Compile a regex pattern.
1524///
1525/// # UPSTREAM-PARITY
1526///
1527/// ```c
1528/// xmlRegexpPtr xmlRegexpCompile(const xmlChar *pattern);
1529/// ```
1530///
1531/// Returns a compiled regex or NULL on error.
1532///
1533/// # SAFETY
1534///
1535/// - `pattern` must be a valid null-terminated xmlChar string, or NULL.
1536#[no_mangle]
1537pub unsafe extern "C" fn xmlRegexpCompile(pattern: *const xmlChar) -> *mut XmlRegexp {
1538    if pattern.is_null() {
1539        return ptr::null_mut();
1540    }
1541
1542    let len = xml_strlen(pattern);
1543    let pattern_bytes = unsafe { core::slice::from_raw_parts(pattern, len) };
1544
1545    let nfa = compile_pattern(pattern_bytes);
1546    let pattern_copy = xml_strdup(pattern);
1547
1548    let compiled = xmlMallocImpl(core::mem::size_of::<XmlRegexp>()) as *mut XmlRegexp;
1549    if compiled.is_null() {
1550        if !pattern_copy.is_null() {
1551            xmlFreeImpl(pattern_copy as *mut c_void);
1552        }
1553        return ptr::null_mut();
1554    }
1555
1556    let det = nfa
1557        .as_ref()
1558        .map_or(0, |n| if is_deterministic(n) { 1 } else { 0 });
1559
1560    unsafe {
1561        (*compiled).pattern = pattern_copy;
1562        // Use ptr::write to avoid dropping uninitialized memory.
1563        // xmlMalloc returns uninitialized memory; Rust's assignment operator
1564        // would try to drop the old (garbage) value for fields with Drop.
1565        core::ptr::write(&mut (*compiled).nfa, nfa);
1566        (*compiled).is_deterministic = det;
1567    }
1568
1569    compiled
1570}
1571
1572/// Execute a compiled regex against a string.
1573///
1574/// # UPSTREAM-PARITY
1575///
1576/// ```c
1577/// int xmlRegexpExec(const xmlRegexpPtr compiled, const xmlChar *value);
1578/// ```
1579///
1580/// Returns 1 if the value matches, 0 if not, -1 on error.
1581///
1582/// # SAFETY
1583///
1584/// - `compiled` must be a valid pointer to an XmlRegexp, or NULL.
1585/// - `value` must be a valid null-terminated xmlChar string, or NULL.
1586#[no_mangle]
1587pub unsafe extern "C" fn xmlRegexpExec(compiled: *const XmlRegexp, value: *const xmlChar) -> c_int {
1588    if compiled.is_null() || value.is_null() {
1589        return REGEXP_ERROR;
1590    }
1591
1592    let regex = unsafe { &*compiled };
1593    let nfa = match &regex.nfa {
1594        Some(nfa) => nfa,
1595        None => return REGEXP_ERROR,
1596    };
1597
1598    let len = xml_strlen(value);
1599    let input = unsafe { core::slice::from_raw_parts(value, len) };
1600
1601    nfa_exec(nfa, input)
1602}
1603
1604/// Check if a compiled regex is deterministic.
1605///
1606/// # UPSTREAM-PARITY
1607///
1608/// ```c
1609/// int xmlRegexpIsDeterministic(const xmlRegexpPtr compiled);
1610/// ```
1611///
1612/// Returns 1 if deterministic, 0 otherwise.
1613///
1614/// # SAFETY
1615///
1616/// - `compiled` must be a valid pointer to an XmlRegexp, or NULL.
1617#[no_mangle]
1618pub const unsafe extern "C" fn xmlRegexpIsDeterministic(compiled: *const XmlRegexp) -> c_int {
1619    if compiled.is_null() {
1620        return 0;
1621    }
1622    unsafe { (*compiled).is_deterministic }
1623}
1624
1625/// Print a compiled regex for debugging (upstream xmlregexp.h: FILE* first
1626/// argument — R-000176, the candidate previously dropped it).
1627///
1628/// # UPSTREAM-PARITY
1629///
1630/// ```c
1631/// void xmlRegexpPrint(FILE *output, xmlRegexpPtr compiled);
1632/// ```
1633///
1634/// # SAFETY
1635///
1636/// - `output` must be a valid FILE* or NULL (stderr is used)
1637/// - `compiled` must be a valid pointer to an XmlRegexp, or NULL
1638#[no_mangle]
1639pub unsafe extern "C" fn xmlRegexpPrint(output: *mut c_void, compiled: *const XmlRegexp) {
1640    let mut msg = String::new();
1641    if compiled.is_null() {
1642        msg.push_str("(null regex)\n");
1643    } else {
1644        let regex = unsafe { &*compiled };
1645        let pattern_str = if regex.pattern.is_null() {
1646            "(null)".to_string()
1647        } else {
1648            let len = xml_strlen(regex.pattern);
1649            let slice = unsafe { core::slice::from_raw_parts(regex.pattern, len) };
1650            core::str::from_utf8(slice)
1651                .unwrap_or("(invalid utf-8)")
1652                .to_string()
1653        };
1654        msg.push_str(&format!("Regex: /{}/\n", pattern_str));
1655        msg.push_str(&format!("  deterministic: {}\n", regex.is_deterministic));
1656
1657        if let Some(ref nfa) = regex.nfa {
1658            msg.push_str(&format!("  states: {}\n", nfa.states.len()));
1659            msg.push_str(&format!("  start state: {}\n", nfa.start));
1660            for (i, state) in nfa.states.iter().enumerate() {
1661                msg.push_str(&format!("    state[{}]: ", i));
1662                if state.is_accept {
1663                    msg.push_str("(accept) ");
1664                }
1665                for (j, (cond, target)) in state.transitions.iter().enumerate() {
1666                    if j > 0 {
1667                        msg.push_str(", ");
1668                    }
1669                    match cond {
1670                        Transition::Epsilon => msg.push_str(&format!("ε->{}", target)),
1671                        Transition::Char(c) => {
1672                            if *c >= 0x20 && *c <= 0x7e {
1673                                msg.push_str(&format!("'{}'->{}", *c as char, target));
1674                            } else {
1675                                msg.push_str(&format!("0x{:02x}->{}", c, target));
1676                            }
1677                        }
1678                        Transition::Range(lo, hi) => {
1679                            msg.push_str(&format!("[{:02x}-{:02x}]->{}", lo, hi, target));
1680                        }
1681                        Transition::Set(chars) => {
1682                            msg.push('{');
1683                            for (k, c) in chars.iter().enumerate() {
1684                                if k > 0 {
1685                                    msg.push(',');
1686                                }
1687                                msg.push_str(&format!("0x{:02x}", c));
1688                            }
1689                            msg.push_str("}->");
1690                            msg.push_str(&target.to_string());
1691                        }
1692                        Transition::NotRange(lo, hi) => {
1693                            msg.push_str(&format!("[^{:02x}-{:02x}]->{}", lo, hi, target));
1694                        }
1695                        Transition::NotSet(chars) => {
1696                            msg.push_str("^{{");
1697                            for (k, c) in chars.iter().enumerate() {
1698                                if k > 0 {
1699                                    msg.push(',');
1700                                }
1701                                msg.push_str(&format!("0x{:02x}", c));
1702                            }
1703                            msg.push_str("}}->");
1704                            msg.push_str(&target.to_string());
1705                        }
1706                        Transition::Wildcard => msg.push_str(&format!(".*->{}", target)),
1707                        Transition::Predefined(class) => {
1708                            let name = match class {
1709                                PredefinedClass::Digit => "\\d",
1710                                PredefinedClass::NotDigit => "\\D",
1711                                PredefinedClass::Space => "\\s",
1712                                PredefinedClass::NotSpace => "\\S",
1713                                PredefinedClass::Word => "\\w",
1714                                PredefinedClass::NotWord => "\\W",
1715                            };
1716                            msg.push_str(&format!("{}->{}", name, target));
1717                        }
1718                        Transition::Anchor(at) => match at {
1719                            AnchorType::Start => msg.push_str(&format!("^->{}", target)),
1720                            AnchorType::End => msg.push_str(&format!("$->{}", target)),
1721                        },
1722                    }
1723                }
1724                msg.push('\n');
1725            }
1726        }
1727    }
1728    unsafe {
1729        let out = if output.is_null() {
1730            libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut c_void
1731        } else {
1732            output
1733        };
1734        if out.is_null() {
1735            return;
1736        }
1737        libc::fwrite(
1738            msg.as_ptr() as *const c_void,
1739            1,
1740            msg.len(),
1741            out as *mut libc::FILE,
1742        );
1743    }
1744}
1745
1746/// Free a compiled regex.
1747///
1748/// # UPSTREAM-PARITY
1749///
1750/// ```c
1751/// void xmlRegFreeRegexp(xmlRegexpPtr regexp);
1752/// ```
1753///
1754/// # SAFETY
1755///
1756/// - `regexp` must be a valid pointer to an XmlRegexp previously returned
1757///   by `xmlRegexpCompile`, or NULL.
1758#[no_mangle]
1759pub unsafe extern "C" fn xmlRegFreeRegexp(regexp: *mut XmlRegexp) {
1760    if regexp.is_null() {
1761        return;
1762    }
1763    unsafe {
1764        if !(*regexp).pattern.is_null() {
1765            xmlFreeImpl((*regexp).pattern as *mut c_void);
1766        }
1767        // Drop the NFA box
1768        let _ = (*regexp).nfa.take();
1769        xmlFreeImpl(regexp as *mut c_void);
1770    }
1771}
1772
1773// ═══════════════════════════════════════════════════════════════════════════════
1774// RegExecCtxt — Incremental Regex Execution Context
1775// ═══════════════════════════════════════════════════════════════════════════════
1776
1777/// Incremental regex execution context.
1778///
1779/// # UPSTREAM-PARITY
1780///
1781/// Corresponds to `xmlRegExecCtxtPtr` / `_xmlRegExecCtxt` in libxml2.
1782#[derive(Debug)]
1783#[repr(C)]
1784pub struct RegExecCtxt {
1785    /// The compiled regex being executed.
1786    compiled: *mut XmlRegexp,
1787    /// Current set of NFA states (indices into the NFA).
1788    current_states: Vec<usize>,
1789    /// Whether we've started matching.
1790    started: bool,
1791    /// Progress callback (upstream xmlRegExecCallbacks; retained for ABI
1792    /// parity — the candidate NFA engine has no transition-data concept, so
1793    /// it is not invoked; documented divergence, R-000176).
1794    callback: Option<crate::abi::callbacks::xmlRegExecCallbacks>,
1795    /// Opaque context for the callback (upstream `exec->data`).
1796    data: *mut c_void,
1797}
1798
1799/// Create an incremental regex execution context.
1800///
1801/// # UPSTREAM-PARITY
1802///
1803/// ```c
1804/// xmlRegExecCtxtPtr xmlRegNewExecCtxt(xmlRegexpPtr compiled,
1805///                                     xmlRegExecCallbacks callback,
1806///                                     void *data);
1807/// ```
1808///
1809/// # SAFETY
1810///
1811/// - `compiled` must be a valid pointer to an XmlRegexp, or NULL.
1812/// - `callback` may be NULL; when non-NULL it is retained in the context
1813///   (upstream invokes it on transitions with attached data — not wired in
1814///   the candidate NFA engine, R-000176).
1815#[no_mangle]
1816pub unsafe extern "C" fn xmlRegNewExecCtxt(
1817    compiled: *mut XmlRegexp,
1818    callback: Option<crate::abi::callbacks::xmlRegExecCallbacks>,
1819    data: *mut c_void,
1820) -> *mut RegExecCtxt {
1821    if compiled.is_null() {
1822        return ptr::null_mut();
1823    }
1824
1825    let ctxt = xmlMallocImpl(core::mem::size_of::<RegExecCtxt>()) as *mut RegExecCtxt;
1826    if ctxt.is_null() {
1827        return ptr::null_mut();
1828    }
1829
1830    unsafe {
1831        (*ctxt).compiled = compiled;
1832        // Use ptr::write to avoid dropping uninitialized memory.
1833        // xmlMalloc returns uninitialized memory; Rust's assignment operator
1834        // would try to drop the old (garbage) value for fields with Drop.
1835        core::ptr::write(&mut (*ctxt).current_states, Vec::new());
1836        (*ctxt).started = false;
1837        (*ctxt).callback = callback;
1838        (*ctxt).data = data;
1839    }
1840
1841    ctxt
1842}
1843
1844/// Push a string into the incremental regex execution context.
1845///
1846/// # UPSTREAM-PARITY
1847///
1848/// ```c
1849/// int xmlRegExecPushString(xmlRegExecCtxtPtr ctxt, const xmlChar *value,
1850///                          void *data);
1851/// ```
1852///
1853/// Returns 1 if the pushed data completes a match, 0 if more data is needed,
1854/// -1 on error.
1855///
1856/// # SAFETY
1857///
1858/// - `ctxt` must be a valid pointer to a RegExecCtxt, or NULL.
1859/// - `value` must be a valid null-terminated xmlChar string, or NULL.
1860/// - `data` is the per-push token data passed to the progress callback
1861///   (retained for ABI parity; not invoked, R-000176).
1862#[no_mangle]
1863pub unsafe extern "C" fn xmlRegExecPushString(
1864    ctxt: *mut RegExecCtxt,
1865    value: *const xmlChar,
1866    _data: *mut c_void,
1867) -> c_int {
1868    if ctxt.is_null() {
1869        return REGEXP_ERROR;
1870    }
1871
1872    let exec_ctxt = unsafe { &mut *ctxt };
1873    let regex = match unsafe { exec_ctxt.compiled.as_mut() } {
1874        Some(r) => r,
1875        None => return REGEXP_ERROR,
1876    };
1877
1878    let nfa = match &regex.nfa {
1879        Some(nfa) => nfa,
1880        None => return REGEXP_ERROR,
1881    };
1882
1883    if value.is_null() {
1884        // NULL means end of input — check if current state is accepting.
1885        // If not started yet, initialize from start state first.
1886        if !exec_ctxt.started {
1887            exec_ctxt.current_states = epsilon_closure(nfa, &[nfa.start]);
1888            exec_ctxt.started = true;
1889        }
1890        return if has_accept_state(nfa, &exec_ctxt.current_states) {
1891            REGEXP_MATCH
1892        } else {
1893            REGEXP_NOMATCH
1894        };
1895    }
1896
1897    let len = xml_strlen(value);
1898    let input = unsafe { core::slice::from_raw_parts(value, len) };
1899
1900    if input.is_empty() {
1901        return REGEXP_NOMATCH;
1902    }
1903
1904    if !exec_ctxt.started {
1905        // Initialize with epsilon closure of start state
1906        exec_ctxt.current_states = epsilon_closure(nfa, &[nfa.start]);
1907        exec_ctxt.started = true;
1908    }
1909
1910    for (i, &c) in input.iter().enumerate() {
1911        let is_end = i == input.len() - 1 && true; // end of this push, but not necessarily end of all input
1912        let next_states = move_on_char(nfa, &exec_ctxt.current_states, c, i == 0, is_end);
1913        if next_states.is_empty() {
1914            exec_ctxt.current_states = Vec::new();
1915            return REGEXP_NOMATCH;
1916        }
1917        exec_ctxt.current_states = epsilon_closure(nfa, &next_states);
1918    }
1919
1920    // Check if current state set contains an accept state
1921    if has_accept_state(nfa, &exec_ctxt.current_states) {
1922        REGEXP_MATCH
1923    } else {
1924        REGEXP_NOMATCH
1925    }
1926}
1927
1928/// Free an incremental regex execution context.
1929///
1930/// # UPSTREAM-PARITY
1931///
1932/// ```c
1933/// void xmlRegFreeExecCtxt(xmlRegExecCtxtPtr ctxt);
1934/// ```
1935///
1936/// # SAFETY
1937///
1938/// - `ctxt` must be a valid pointer to a RegExecCtxt previously returned
1939///   by `xmlRegNewExecCtxt`, or NULL.
1940#[no_mangle]
1941pub unsafe extern "C" fn xmlRegFreeExecCtxt(ctxt: *mut RegExecCtxt) {
1942    if ctxt.is_null() {
1943        return;
1944    }
1945    unsafe {
1946        // SAFETY: The Vec inside RegExecCtxt was allocated by Rust's allocator
1947        // and must be dropped before freeing the struct memory via libc::free.
1948        core::ptr::drop_in_place(&mut (*ctxt).current_states);
1949        xmlFreeImpl(ctxt as *mut c_void);
1950    }
1951}
1952
1953// ═══════════════════════════════════════════════════════════════════════════════
1954// Tests
1955// ═══════════════════════════════════════════════════════════════════════════════
1956
1957#[cfg(test)]
1958mod tests {
1959    use super::*;
1960    use core::ptr;
1961
1962    /// Helper: create a null-terminated xmlChar* from a byte slice using xmlMalloc.
1963    ///
1964    /// This ensures the returned pointer uses the same allocator as xmlFreeImpl,
1965    /// preventing allocator mismatch crashes.
1966    ///
1967    /// # Safety
1968    ///
1969    /// - `s` must be a valid slice; the returned pointer is allocator-owned,
1970    ///   valid for `len + 1` bytes, and must be freed with `xmlFreeImpl`, or
1971    ///   is NULL when the allocation fails.
1972    fn to_xml_str(s: &[u8]) -> *mut xmlChar {
1973        let len = s.len();
1974        let ptr =
1975            unsafe { xmlMallocImpl((len + 1) * core::mem::size_of::<xmlChar>()) } as *mut xmlChar;
1976        if ptr.is_null() {
1977            return ptr::null_mut();
1978        }
1979        unsafe {
1980            core::ptr::copy_nonoverlapping(s.as_ptr(), ptr, len);
1981            *ptr.add(len) = 0;
1982        }
1983        ptr
1984    }
1985
1986    /// Helper: match a regex pattern against an input string.
1987    ///
1988    /// # Safety
1989    ///
1990    /// - The temporary `pat`/`val` NUL-terminated strings must stay valid
1991    ///   for the compile/exec calls; `compiled` is freed with
1992    ///   `xmlRegFreeRegexp` before return.
1993    fn match_regex(pattern: &[u8], input: &[u8]) -> c_int {
1994        let pat = to_xml_str(pattern);
1995        let val = to_xml_str(input);
1996        unsafe {
1997            let compiled = xmlRegexpCompile(pat);
1998            if compiled.is_null() {
1999                return REGEXP_ERROR;
2000            }
2001            let ret = xmlRegexpExec(compiled, val);
2002            xmlRegFreeRegexp(compiled);
2003            ret
2004        }
2005    }
2006
2007    /// Helper to compile a pattern and return the compiled regex.
2008    ///
2009    /// # Safety
2010    ///
2011    /// - `pattern` must be a valid slice; the returned `XmlRegexp` pointer
2012    ///   is non-NULL only on success and must be freed with
2013    ///   `xmlRegFreeRegexp` by the caller.
2014    fn compile(pattern: &[u8]) -> *mut XmlRegexp {
2015        let pat = to_xml_str(pattern);
2016        unsafe { xmlRegexpCompile(pat) }
2017    }
2018
2019    // ── Simple Literal Matching ───────────────────────────────────────────
2020
2021    #[test]
2022    fn test_literal_exact() {
2023        assert_eq!(match_regex(b"hello", b"hello"), REGEXP_MATCH);
2024    }
2025
2026    #[test]
2027    fn test_literal_no_match() {
2028        assert_eq!(match_regex(b"hello", b"world"), REGEXP_NOMATCH);
2029    }
2030
2031    #[test]
2032    fn test_literal_partial_prefix() {
2033        // Full match required - "hel" is only a prefix
2034        assert_eq!(match_regex(b"hello", b"hel"), REGEXP_NOMATCH);
2035    }
2036
2037    #[test]
2038    fn test_literal_empty_pattern() {
2039        // Empty pattern matches empty string
2040        assert_eq!(match_regex(b"", b""), REGEXP_MATCH);
2041    }
2042
2043    #[test]
2044    fn test_literal_empty_pattern_nonempty() {
2045        // Empty pattern does not match non-empty string
2046        assert_eq!(match_regex(b"", b"a"), REGEXP_NOMATCH);
2047    }
2048
2049    #[test]
2050    fn test_literal_single_char() {
2051        assert_eq!(match_regex(b"a", b"a"), REGEXP_MATCH);
2052        assert_eq!(match_regex(b"a", b"b"), REGEXP_NOMATCH);
2053    }
2054
2055    // ── Alternation ──────────────────────────────────────────────────────
2056
2057    #[test]
2058    fn test_alternation_simple() {
2059        assert_eq!(match_regex(b"a|b", b"a"), REGEXP_MATCH);
2060        assert_eq!(match_regex(b"a|b", b"b"), REGEXP_MATCH);
2061        assert_eq!(match_regex(b"a|b", b"c"), REGEXP_NOMATCH);
2062    }
2063
2064    #[test]
2065    fn test_alternation_three() {
2066        assert_eq!(match_regex(b"a|b|c", b"a"), REGEXP_MATCH);
2067        assert_eq!(match_regex(b"a|b|c", b"b"), REGEXP_MATCH);
2068        assert_eq!(match_regex(b"a|b|c", b"c"), REGEXP_MATCH);
2069        assert_eq!(match_regex(b"a|b|c", b"d"), REGEXP_NOMATCH);
2070    }
2071
2072    // ── Quantifiers ──────────────────────────────────────────────────────
2073
2074    #[test]
2075    fn test_zero_or_more() {
2076        assert_eq!(match_regex(b"a*", b""), REGEXP_MATCH);
2077        assert_eq!(match_regex(b"a*", b"a"), REGEXP_MATCH);
2078        assert_eq!(match_regex(b"a*", b"aaa"), REGEXP_MATCH);
2079    }
2080
2081    #[test]
2082    fn test_one_or_more() {
2083        assert_eq!(match_regex(b"a+", b"a"), REGEXP_MATCH);
2084        assert_eq!(match_regex(b"a+", b"aaa"), REGEXP_MATCH);
2085        assert_eq!(match_regex(b"a+", b""), REGEXP_NOMATCH);
2086    }
2087
2088    #[test]
2089    fn test_zero_or_one() {
2090        assert_eq!(match_regex(b"a?", b""), REGEXP_MATCH);
2091        assert_eq!(match_regex(b"a?", b"a"), REGEXP_MATCH);
2092        assert_eq!(match_regex(b"a?", b"aa"), REGEXP_NOMATCH);
2093    }
2094
2095    #[test]
2096    fn test_zero_or_more_middle() {
2097        // a*b matches "b", "ab", "aaab"
2098        assert_eq!(match_regex(b"a*b", b"b"), REGEXP_MATCH);
2099        assert_eq!(match_regex(b"a*b", b"ab"), REGEXP_MATCH);
2100        assert_eq!(match_regex(b"a*b", b"aaab"), REGEXP_MATCH);
2101        assert_eq!(match_regex(b"a*b", b"a"), REGEXP_NOMATCH);
2102    }
2103
2104    // ── Grouping ─────────────────────────────────────────────────────────
2105
2106    #[test]
2107    fn test_grouping() {
2108        assert_eq!(match_regex(b"(a)", b"a"), REGEXP_MATCH);
2109        assert_eq!(match_regex(b"(a)", b"b"), REGEXP_NOMATCH);
2110    }
2111
2112    #[test]
2113    fn test_grouping_with_quantifier() {
2114        assert_eq!(match_regex(b"(ab)+", b"ab"), REGEXP_MATCH);
2115        assert_eq!(match_regex(b"(ab)+", b"abab"), REGEXP_MATCH);
2116        assert_eq!(match_regex(b"(ab)+", b"a"), REGEXP_NOMATCH);
2117    }
2118
2119    // ── Anchors ──────────────────────────────────────────────────────────
2120
2121    #[test]
2122    fn test_start_anchor() {
2123        assert_eq!(match_regex(b"^a", b"a"), REGEXP_MATCH);
2124        assert_eq!(match_regex(b"^a", b"ba"), REGEXP_NOMATCH);
2125    }
2126
2127    #[test]
2128    fn test_end_anchor() {
2129        assert_eq!(match_regex(b"a$", b"a"), REGEXP_MATCH);
2130        assert_eq!(match_regex(b"a$", b"ba"), REGEXP_NOMATCH);
2131    }
2132
2133    #[test]
2134    fn test_both_anchors() {
2135        assert_eq!(match_regex(b"^a$", b"a"), REGEXP_MATCH);
2136        assert_eq!(match_regex(b"^a$", b"ab"), REGEXP_NOMATCH);
2137        assert_eq!(match_regex(b"^a$", b"ba"), REGEXP_NOMATCH);
2138    }
2139
2140    // ── Wildcard ─────────────────────────────────────────────────────────
2141
2142    #[test]
2143    fn test_wildcard() {
2144        assert_eq!(match_regex(b".", b"a"), REGEXP_MATCH);
2145        assert_eq!(match_regex(b".", b"1"), REGEXP_MATCH);
2146        assert_eq!(match_regex(b"...", b"abc"), REGEXP_MATCH);
2147        assert_eq!(match_regex(b"...", b"ab"), REGEXP_NOMATCH);
2148    }
2149
2150    #[test]
2151    fn test_wildcard_with_literal() {
2152        assert_eq!(match_regex(b"a.b", b"axb"), REGEXP_MATCH);
2153        assert_eq!(match_regex(b"a.b", b"azb"), REGEXP_MATCH);
2154        assert_eq!(match_regex(b"a.b", b"ab"), REGEXP_NOMATCH);
2155    }
2156
2157    // ── Escaped Characters ───────────────────────────────────────────────
2158
2159    #[test]
2160    fn test_escaped_newline() {
2161        assert_eq!(match_regex(b"a\\nb", b"a\nb"), REGEXP_MATCH);
2162        assert_eq!(match_regex(b"a\\nb", b"ab"), REGEXP_NOMATCH);
2163    }
2164
2165    #[test]
2166    fn test_escaped_tab() {
2167        assert_eq!(match_regex(b"a\\tb", b"a\tb"), REGEXP_MATCH);
2168    }
2169
2170    #[test]
2171    fn test_escaped_metachar() {
2172        // Escaped dot matches literal dot
2173        assert_eq!(match_regex(b"\\.", b"."), REGEXP_MATCH);
2174        assert_eq!(match_regex(b"\\.", b"a"), REGEXP_NOMATCH);
2175    }
2176
2177    // ── Character Classes ────────────────────────────────────────────────
2178
2179    #[test]
2180    fn test_char_class_single() {
2181        assert_eq!(match_regex(b"[a]", b"a"), REGEXP_MATCH);
2182        assert_eq!(match_regex(b"[a]", b"b"), REGEXP_NOMATCH);
2183    }
2184
2185    #[test]
2186    fn test_char_class_multiple_chars() {
2187        assert_eq!(match_regex(b"[abc]", b"a"), REGEXP_MATCH);
2188        assert_eq!(match_regex(b"[abc]", b"b"), REGEXP_MATCH);
2189        assert_eq!(match_regex(b"[abc]", b"c"), REGEXP_MATCH);
2190        assert_eq!(match_regex(b"[abc]", b"d"), REGEXP_NOMATCH);
2191    }
2192
2193    #[test]
2194    fn test_char_class_range() {
2195        assert_eq!(match_regex(b"[a-z]", b"a"), REGEXP_MATCH);
2196        assert_eq!(match_regex(b"[a-z]", b"m"), REGEXP_MATCH);
2197        assert_eq!(match_regex(b"[a-z]", b"z"), REGEXP_MATCH);
2198        assert_eq!(match_regex(b"[a-z]", b"1"), REGEXP_NOMATCH);
2199    }
2200
2201    #[test]
2202    fn test_char_class_range_with_escape() {
2203        assert_eq!(match_regex(b"[\\d]", b"5"), REGEXP_MATCH);
2204        assert_eq!(match_regex(b"[\\d]", b"a"), REGEXP_NOMATCH);
2205    }
2206
2207    #[test]
2208    fn test_multiple_char_classes() {
2209        assert_eq!(match_regex(b"[a-z0-9]+", b"abc123"), REGEXP_MATCH);
2210        assert_eq!(match_regex(b"[a-z0-9]+", b"ABC"), REGEXP_NOMATCH);
2211    }
2212
2213    // ── Predefined Classes ───────────────────────────────────────────────
2214
2215    #[test]
2216    fn test_digit_class() {
2217        assert_eq!(match_regex(b"\\d", b"5"), REGEXP_MATCH);
2218        assert_eq!(match_regex(b"\\d", b"a"), REGEXP_NOMATCH);
2219    }
2220
2221    #[test]
2222    fn test_word_class() {
2223        assert_eq!(match_regex(b"\\w+", b"hello"), REGEXP_MATCH);
2224        assert_eq!(match_regex(b"\\w+", b"hello123"), REGEXP_MATCH);
2225        assert_eq!(match_regex(b"\\w+", b""), REGEXP_NOMATCH);
2226    }
2227
2228    #[test]
2229    fn test_space_class() {
2230        assert_eq!(match_regex(b"\\s", b" "), REGEXP_MATCH);
2231        assert_eq!(match_regex(b"\\s", b"\t"), REGEXP_MATCH);
2232        assert_eq!(match_regex(b"\\s", b"a"), REGEXP_NOMATCH);
2233    }
2234
2235    // ── Complex Patterns ─────────────────────────────────────────────────
2236
2237    #[test]
2238    fn test_complex_email_like() {
2239        // Simple email-like pattern: \w+@\w+\.\w+
2240        assert_eq!(
2241            match_regex(b"\\w+@\\w+\\.\\w+", b"user@example.com"),
2242            REGEXP_MATCH
2243        );
2244        assert_eq!(match_regex(b"\\w+@\\w+\\.\\w+", b"invalid"), REGEXP_NOMATCH);
2245    }
2246
2247    #[test]
2248    fn test_complex_phone_like() {
2249        // Simple phone-like: \d{3}-\d{3}-\d{4}
2250        assert_eq!(
2251            match_regex(b"\\d{3}-\\d{3}-\\d{4}", b"555-123-4567"),
2252            REGEXP_MATCH
2253        );
2254        assert_eq!(
2255            match_regex(b"\\d{3}-\\d{3}-\\d{4}", b"555-123-456"),
2256            REGEXP_NOMATCH
2257        );
2258    }
2259
2260    #[test]
2261    fn test_pattern_with_all_features() {
2262        // Pattern using alternation, grouping, quantifiers, anchors
2263        assert_eq!(match_regex(b"^(a|b)+c$", b"ac"), REGEXP_MATCH);
2264        assert_eq!(match_regex(b"^(a|b)+c$", b"bc"), REGEXP_MATCH);
2265        assert_eq!(match_regex(b"^(a|b)+c$", b"ababc"), REGEXP_MATCH);
2266        assert_eq!(match_regex(b"^(a|b)+c$", b"abd"), REGEXP_NOMATCH);
2267    }
2268
2269    // ── Exact Quantifiers ────────────────────────────────────────────────
2270
2271    #[test]
2272    fn test_exact_quantifier() {
2273        assert_eq!(match_regex(b"a{3}", b"aaa"), REGEXP_MATCH);
2274        assert_eq!(match_regex(b"a{3}", b"aa"), REGEXP_NOMATCH);
2275        assert_eq!(match_regex(b"a{3}", b"aaaa"), REGEXP_NOMATCH);
2276    }
2277
2278    #[test]
2279    fn test_between_quantifier() {
2280        assert_eq!(match_regex(b"a{2,4}", b"aa"), REGEXP_MATCH);
2281        assert_eq!(match_regex(b"a{2,4}", b"aaa"), REGEXP_MATCH);
2282        assert_eq!(match_regex(b"a{2,4}", b"aaaa"), REGEXP_MATCH);
2283        assert_eq!(match_regex(b"a{2,4}", b"a"), REGEXP_NOMATCH);
2284        assert_eq!(match_regex(b"a{2,4}", b"aaaaa"), REGEXP_NOMATCH);
2285    }
2286
2287    #[test]
2288    fn test_at_least_quantifier() {
2289        assert_eq!(match_regex(b"a{2,}", b"aa"), REGEXP_MATCH);
2290        assert_eq!(match_regex(b"a{2,}", b"aaaa"), REGEXP_MATCH);
2291        assert_eq!(match_regex(b"a{2,}", b"a"), REGEXP_NOMATCH);
2292    }
2293
2294    // ── Determinism ──────────────────────────────────────────────────────
2295
2296    /// A deterministic literal pattern is reported as deterministic.
2297    ///
2298    /// # Safety
2299    ///
2300    /// - `compiled` is non-NULL (asserted) and valid until freed with
2301    ///   `xmlRegFreeRegexp`; `xmlRegexpIsDeterministic` only reads it.
2302    #[test]
2303    fn test_deterministic_literal() {
2304        let compiled = compile(b"hello");
2305        assert!(!compiled.is_null());
2306        assert_eq!(unsafe { xmlRegexpIsDeterministic(compiled) }, 1);
2307        unsafe { xmlRegFreeRegexp(compiled) };
2308    }
2309
2310    /// An alternation pattern is reported as non-deterministic.
2311    ///
2312    /// # Safety
2313    ///
2314    /// - `compiled` is non-NULL (asserted) and valid until freed with
2315    ///   `xmlRegFreeRegexp`; `xmlRegexpIsDeterministic` only reads it.
2316    #[test]
2317    fn test_non_deterministic() {
2318        // Alternation is non-deterministic in NFA form
2319        let compiled = compile(b"a|b");
2320        assert!(!compiled.is_null());
2321        assert_eq!(unsafe { xmlRegexpIsDeterministic(compiled) }, 0);
2322        unsafe { xmlRegFreeRegexp(compiled) };
2323    }
2324
2325    // ── Incremental Execution ────────────────────────────────────────────
2326
2327    /// Push a NULL terminator into an incremental execution context.
2328    ///
2329    /// # Safety
2330    ///
2331    /// - `compiled` and `ctxt` are non-NULL (asserted) and valid until
2332    ///   their frees; a NULL push string is accepted as end-of-input by
2333    ///   `xmlRegExecPushString`.
2334    #[test]
2335    fn test_incremental_empty_input() {
2336        let compiled = compile(b"a*");
2337        assert!(!compiled.is_null());
2338        let ctxt = unsafe { xmlRegNewExecCtxt(compiled, None, ptr::null_mut()) };
2339        assert!(!ctxt.is_null());
2340        // Push empty string should not match a* (no input yet)
2341        let ret = unsafe { xmlRegExecPushString(ctxt, ptr::null_mut(), ptr::null_mut()) };
2342        // NULL terminates input — a* matches empty
2343        assert_eq!(ret, REGEXP_MATCH);
2344        unsafe { xmlRegFreeExecCtxt(ctxt) };
2345        unsafe { xmlRegFreeRegexp(compiled) };
2346    }
2347
2348    /// Push a complete string into an incremental execution context.
2349    ///
2350    /// # Safety
2351    ///
2352    /// - `compiled` and `ctxt` are non-NULL (asserted) and valid until
2353    ///   freed; `val` is an allocator-owned NUL-terminated string valid
2354    ///   for the push call.
2355    #[test]
2356    fn test_incremental_simple_match() {
2357        let compiled = compile(b"abc");
2358        assert!(!compiled.is_null());
2359        let ctxt = unsafe { xmlRegNewExecCtxt(compiled, None, ptr::null_mut()) };
2360        assert!(!ctxt.is_null());
2361        let val = to_xml_str(b"abc");
2362        let ret = unsafe { xmlRegExecPushString(ctxt, val, ptr::null_mut()) };
2363        assert_eq!(ret, REGEXP_MATCH);
2364        unsafe { xmlRegFreeExecCtxt(ctxt) };
2365        unsafe { xmlRegFreeRegexp(compiled) };
2366    }
2367
2368    // ── Edge Cases ───────────────────────────────────────────────────────
2369
2370    /// Compiling a NULL pattern returns NULL without dereferencing it.
2371    ///
2372    /// # Safety
2373    ///
2374    /// - `xmlRegexpCompile` accepts a NULL pattern and returns NULL; no
2375    ///   pointer is dereferenced.
2376    #[test]
2377    fn test_null_pattern() {
2378        let compiled = unsafe { xmlRegexpCompile(ptr::null()) };
2379        assert!(compiled.is_null());
2380    }
2381
2382    /// Executing with a NULL input string reports an error.
2383    ///
2384    /// # Safety
2385    ///
2386    /// - `compiled` is non-NULL (asserted) and valid until freed with
2387    ///   `xmlRegFreeRegexp`; the NULL input is accepted by
2388    ///   `xmlRegexpExec` as an invalid input.
2389    #[test]
2390    fn test_null_input() {
2391        let compiled = compile(b"a");
2392        assert!(!compiled.is_null());
2393        let ret = unsafe { xmlRegexpExec(compiled, ptr::null()) };
2394        assert_eq!(ret, REGEXP_ERROR);
2395        unsafe { xmlRegFreeRegexp(compiled) };
2396    }
2397
2398    /// Freeing a compiled regexp must not crash.
2399    ///
2400    /// # Safety
2401    ///
2402    /// - `compiled` is non-NULL (asserted) and is freed exactly once with
2403    ///   `xmlRegFreeRegexp`; the object must not be used afterwards.
2404    #[test]
2405    fn test_double_free() {
2406        let compiled = compile(b"test");
2407        assert!(!compiled.is_null());
2408        unsafe { xmlRegFreeRegexp(compiled) };
2409        // Freeing again is safe (pattern is null after free)
2410        // We just test that it doesn't crash
2411    }
2412
2413    /// Print a compiled regexp to a NULL output stream.
2414    ///
2415    /// # Safety
2416    ///
2417    /// - `compiled` is non-NULL (asserted) and valid until freed with
2418    ///   `xmlRegFreeRegexp`; the NULL FILE* output is accepted by
2419    ///   `xmlRegexpPrint`.
2420    #[test]
2421    fn test_print() {
2422        let compiled = compile(b"hello");
2423        assert!(!compiled.is_null());
2424        unsafe { xmlRegexpPrint(ptr::null_mut(), compiled) };
2425        unsafe { xmlRegFreeRegexp(compiled) };
2426    }
2427}