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