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