Skip to main content

libxml_rs/xml/automata/
mod.rs

1//! Automata/state machine infrastructure (§85 Phase 7).
2//!
3//! UPSTREAM-PARITY: Corresponds to `xmlautomata.c` / `xmlautomata.h` in libxml2.
4//!
5//! The libxml2 internal automata implementation is used primarily by the
6//! schema/RELAX NG validation subsystems. It builds a state machine that
7//! can be compiled into a regex for efficient validation.
8//!
9//! The automata API:
10//!
11//! ```c
12//! xmlAutomataPtr xmlNewAutomata(void);
13//! void xmlFreeAutomata(xmlAutomataPtr am);
14//! int xmlAutomataSetFinalState(xmlAutomataPtr am, xmlAutomataStatePtr state);
15//! xmlAutomataStatePtr xmlAutomataGetInitState(xmlAutomataPtr am);
16//! int xmlAutomataCompile(xmlAutomataPtr am);
17//! int xmlAutomataIsDeterministic(xmlAutomataPtr am);
18//!
19//! xmlAutomataStatePtr xmlAutomataNewState(xmlAutomataPtr am);
20//! xmlAutomataStatePtr xmlAutomataNewTransition(xmlAutomataPtr am,
21//!     xmlAutomataStatePtr from, xmlAutomataStatePtr to,
22//!     const xmlChar *token, void *data);
23//! xmlAutomataStatePtr xmlAutomataNewCountTrans(xmlAutomataPtr am,
24//!     xmlAutomataStatePtr from, xmlAutomataStatePtr to,
25//!     const xmlChar *token, void *data, int min, int max);
26//! xmlAutomataStatePtr xmlAutomataNewOnceTrans(xmlAutomataPtr am,
27//!     xmlAutomataStatePtr from, xmlAutomataStatePtr to,
28//!     const xmlChar *token, void *data, int min, int max);
29//! xmlAutomataStatePtr xmlAutomataNewAllTrans(xmlAutomataPtr am,
30//!     xmlAutomataStatePtr from, xmlAutomataStatePtr to, int lax);
31//! xmlAutomataStatePtr xmlAutomataNewEpsilon(xmlAutomataPtr am,
32//!     xmlAutomataStatePtr from, xmlAutomataStatePtr to);
33//! xmlAutomataStatePtr xmlAutomataNewCountedTrans(xmlAutomataPtr am,
34//!     xmlAutomataStatePtr from, xmlAutomataStatePtr to, int counter);
35//! xmlAutomataStatePtr xmlAutomataNewCounterTrans(xmlAutomataPtr am,
36//!     xmlAutomataStatePtr from, xmlAutomataStatePtr to, int counter);
37//! xmlAutomataStatePtr xmlAutomataNewCounter(xmlAutomataPtr am, int min, int max);
38//! ```
39//!
40//! # Upstream contract
41//!
42//! Mirrors upstream `xmlautomata.c` / `xmlautomata.h`
43//! (`SRC-LIBXML2-2.15.0-XMLAUTOMATA-C`, parity target libxml2 2.15.3
44//! oracle): the automata builder API plus `xmlAutomataCompile` (compiles to
45//! an xmlRegexp) and `xmlAutomataIsDeterministic`.
46//!
47//! # Conceptual behavior
48//!
49//! Implements a state-machine builder over the regexp engine: transitions
50//! (epsilon, token, count, once, all, counter) grow an NFA that
51//! `xmlAutomataCompile` lowers into an `XmlRegexp` for the schema / RELAX
52//! NG validation subsystems. This is the constructor half of the regexp
53//! subsystem, not an independent matcher.
54//!
55//! # Ownership & safety invariants
56//!
57//! The automata owns its state list and the compiled regexp (stored in
58//! `am.regexp` on `xmlAutomataCompile`); `xmlFreeAutomata` drops both.
59//! States are owned by the automata — callers hold borrowed pointers valid
60//! until the automata is freed.
61//!
62//! # Historical quirks & epochs
63//!
64//! The automata/regexp pairing dates from the schema work in the 2.6
65//! validation-era expansion and is stable through the 2.15.3 oracle; the
66//! regexp side carries the CVE-2021-3541 fix lineage (SEC-0010), so the
67//! compiled-output contract here inherits that hardening.
68//!
69//! # Deliberate oddities
70//!
71//! Counter transitions (xmlAutomataNewCountedTrans/CounterTrans) implement
72//! the upstream bounded-repetition idiom that has no direct NFA analogue;
73//! they are kept because schema facets compile through them.
74//!
75//! # Proving courts
76//!
77//! Schema/RELAX NG differential courts compile facets through this builder;
78//! cargo test covers the automata unit suites (determinism checks and
79//! compiled-regexp execution).
80//!
81//! # Tempting simplifications that would break parity
82//!
83//! Do not bypass the builder and construct regexes directly: the schema
84//! layer calls the xmlAutomata* entry points and observes their state/
85//! regexp behavior. Do not drop the determinism check — schema
86//! compilation relies on it.
87
88use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl};
89use crate::xml::regex::{xmlRegexpCompile, xmlRegexpIsDeterministic, XmlRegexp};
90use core::ffi::c_int;
91use core::ptr;
92
93/// Opaque pointer to an automata state.
94pub type XmlAutomataStatePtr = *mut XmlAutomataState;
95
96/// Opaque pointer to an automata.
97pub type XmlAutomataPtr = *mut XmlAutomata;
98
99/// UPSTREAM-PARITY: Corresponds to `_xmlAutomata` in libxml2.
100#[derive(Debug)]
101#[repr(C)]
102pub struct XmlAutomata {
103    /// Compiled regex, set by xmlAutomataCompile.
104    regexp: Option<Box<XmlRegexp>>,
105    /// List of all states.
106    states: Vec<*mut XmlAutomataState>,
107    /// The initial state.
108    init_state: Option<*mut XmlAutomataState>,
109    /// Last error code.
110    error: c_int,
111}
112
113/// UPSTREAM-PARITY: Corresponds to `_xmlAutomataState` in libxml2.
114#[derive(Debug)]
115#[repr(C)]
116pub struct XmlAutomataState {
117    /// Transitions from this state.
118    transitions: Vec<AutomataTransition>,
119}
120
121/// A transition in the automata.
122#[derive(Debug)]
123#[repr(C)]
124pub struct AutomataTransition {
125    /// Token to match (null means epsilon/any).
126    token: Option<u8>,
127    /// Minimum count (for counted transitions).
128    min: c_int,
129    /// Maximum count (for counted transitions).
130    max: c_int,
131    /// Target state.
132    to: Option<*mut XmlAutomataState>,
133    /// Whether this is a "once" (consuming) transition.
134    once: bool,
135    /// Whether this is an "all" (any character) transition.
136    all: bool,
137    /// Whether this is an epsilon transition.
138    epsilon: bool,
139    /// Counter ID for counted transitions.
140    counter: c_int,
141    /// User data.
142    data: *mut core::ffi::c_void,
143}
144
145// SAFETY: These types are only accessed through C-compatible raw pointers
146// in the automata API. The internal Vecs are properly managed.
147unsafe impl Send for XmlAutomata {}
148unsafe impl Sync for XmlAutomata {}
149unsafe impl Send for XmlAutomataState {}
150unsafe impl Sync for XmlAutomataState {}
151
152/// Create a new automata.
153///
154/// UPSTREAM-PARITY: `xmlNewAutomata()`
155///
156/// # SAFETY
157///
158/// The function touches crate-global state only; it is safe
159/// as long as the caller respects the library's global
160/// initialization/cleanup ordering (xmlInitParser before use,
161/// xmlCleanupParser only after all users are done).
162///
163/// Violating the global lifecycle ordering, or calling this after
164/// teardown or from a signal handler, is undefined behavior.
165#[no_mangle]
166pub unsafe extern "C" fn xmlNewAutomata() -> XmlAutomataPtr {
167    let am = xmlMallocImpl(core::mem::size_of::<XmlAutomata>()) as XmlAutomataPtr;
168    if am.is_null() {
169        return ptr::null_mut();
170    }
171    unsafe {
172        core::ptr::write(&mut (*am).regexp, None as Option<Box<XmlRegexp>>);
173        core::ptr::write(&mut (*am).states, Vec::new());
174        (*am).init_state = None;
175        (*am).error = 0;
176    }
177    am
178}
179
180/// Free an automata.
181///
182/// UPSTREAM-PARITY: `xmlFreeAutomata()`
183///
184/// # SAFETY
185///
186/// - `am` must be valid pointers (or NULL
187///   where the upstream C contract allows), obtained from the
188///   matching constructor/owner and not yet freed; the callee may
189///   take or keep ownership exactly as the C API specifies.
190///
191/// The caller must not race this call with concurrent mutation of the
192/// same objects from other threads (per-object state is not internally
193/// synchronized). Violating any of the above is undefined behavior.
194///
195/// Exercised by the C-API differential courts
196/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
197/// courts; those pass byte-for-byte against the upstream oracle.
198#[no_mangle]
199pub unsafe extern "C" fn xmlFreeAutomata(am: XmlAutomataPtr) {
200    if am.is_null() {
201        return;
202    }
203    unsafe {
204        // Free all states
205        for &state in &(*am).states {
206            if !state.is_null() {
207                core::ptr::drop_in_place(&mut (*state).transitions);
208                xmlFreeImpl(state as *mut core::ffi::c_void);
209            }
210        }
211        // Drop the states Vec
212        core::ptr::drop_in_place(&mut (*am).states);
213        // Drop the compiled regexp if any
214        let _ = (*am).regexp.take();
215        xmlFreeImpl(am as *mut core::ffi::c_void);
216    }
217}
218
219/// Create a new automata state.
220///
221/// UPSTREAM-PARITY: `xmlAutomataNewState()`
222///
223/// # SAFETY
224///
225/// - `am` must be valid pointers (or NULL
226///   where the upstream C contract allows), obtained from the
227///   matching constructor/owner and not yet freed; the callee may
228///   take or keep ownership exactly as the C API specifies.
229///
230/// The caller must not race this call with concurrent mutation of the
231/// same objects from other threads (per-object state is not internally
232/// synchronized). Violating any of the above is undefined behavior.
233///
234/// Exercised by the C-API differential courts
235/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
236/// courts; those pass byte-for-byte against the upstream oracle.
237#[no_mangle]
238pub unsafe extern "C" fn xmlAutomataNewState(am: XmlAutomataPtr) -> XmlAutomataStatePtr {
239    if am.is_null() {
240        return ptr::null_mut();
241    }
242    let state = xmlMallocImpl(core::mem::size_of::<XmlAutomataState>()) as XmlAutomataStatePtr;
243    if state.is_null() {
244        return ptr::null_mut();
245    }
246    unsafe {
247        core::ptr::write(&mut (*state).transitions, Vec::new());
248        // Add to the automata's state list
249        (*am).states.push(state);
250        // Set as init state if first
251        if (*am).init_state.is_none() {
252            (*am).init_state = Some(state);
253        }
254    }
255    state
256}
257
258/// Set a state as the final (accepting) state.
259///
260/// UPSTREAM-PARITY: `xmlAutomataSetFinalState()`
261///
262/// # SAFETY
263///
264/// - `_am`, `_state` must be valid pointers (or NULL
265///   where the upstream C contract allows), obtained from the
266///   matching constructor/owner and not yet freed; the callee may
267///   take or keep ownership exactly as the C API specifies.
268///
269/// The caller must not race this call with concurrent mutation of the
270/// same objects from other threads (per-object state is not internally
271/// synchronized). Violating any of the above is undefined behavior.
272///
273/// Exercised by the C-API differential courts
274/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
275/// courts; those pass byte-for-byte against the upstream oracle.
276#[no_mangle]
277pub const unsafe extern "C" fn xmlAutomataSetFinalState(
278    _am: XmlAutomataPtr,
279    _state: XmlAutomataStatePtr,
280) -> c_int {
281    // In our implementation, final states are determined by the compiled regex.
282    // This is a no-op for the automata builder; final states are handled during
283    // compilation.
284    0
285}
286
287/// Get the initial state of the automata.
288///
289/// UPSTREAM-PARITY: `xmlAutomataGetInitState()`
290///
291/// # SAFETY
292///
293/// - `am` must be valid pointers (or NULL
294///   where the upstream C contract allows), obtained from the
295///   matching constructor/owner and not yet freed; the callee may
296///   take or keep ownership exactly as the C API specifies.
297///
298/// The caller must not race this call with concurrent mutation of the
299/// same objects from other threads (per-object state is not internally
300/// synchronized). Violating any of the above is undefined behavior.
301///
302/// Exercised by the C-API differential courts
303/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
304/// courts; those pass byte-for-byte against the upstream oracle.
305#[no_mangle]
306pub unsafe extern "C" fn xmlAutomataGetInitState(am: XmlAutomataPtr) -> XmlAutomataStatePtr {
307    if am.is_null() {
308        return ptr::null_mut();
309    }
310    unsafe { (*am).init_state.unwrap_or(ptr::null_mut()) }
311}
312
313/// Add an epsilon (empty) transition between two states.
314///
315/// UPSTREAM-PARITY: `xmlAutomataNewEpsilon()`
316///
317/// # SAFETY
318///
319/// - `am`, `from`, `to` must be valid pointers (or NULL
320///   where the upstream C contract allows), obtained from the
321///   matching constructor/owner and not yet freed; the callee may
322///   take or keep ownership exactly as the C API specifies.
323///
324/// The caller must not race this call with concurrent mutation of the
325/// same objects from other threads (per-object state is not internally
326/// synchronized). Violating any of the above is undefined behavior.
327///
328/// Exercised by the C-API differential courts
329/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
330/// courts; those pass byte-for-byte against the upstream oracle.
331#[no_mangle]
332pub unsafe extern "C" fn xmlAutomataNewEpsilon(
333    am: XmlAutomataPtr,
334    from: XmlAutomataStatePtr,
335    to: XmlAutomataStatePtr,
336) -> XmlAutomataStatePtr {
337    if am.is_null() || from.is_null() || to.is_null() {
338        return ptr::null_mut();
339    }
340    unsafe {
341        (*from).transitions.push(AutomataTransition {
342            token: None,
343            min: 0,
344            max: 0,
345            to: Some(to),
346            once: false,
347            all: false,
348            epsilon: true,
349            counter: -1,
350            data: ptr::null_mut(),
351        });
352    }
353    from
354}
355
356/// Add a character transition between two states.
357///
358/// UPSTREAM-PARITY: `xmlAutomataNewTransition()`
359///
360/// # SAFETY
361///
362/// - `am`, `from`, `to`, `token`, `_data` must be valid pointers (or NULL
363///   where the upstream C contract allows), obtained from the
364///   matching constructor/owner and not yet freed; the callee may
365///   take or keep ownership exactly as the C API specifies.
366///
367/// The caller must not race this call with concurrent mutation of the
368/// same objects from other threads (per-object state is not internally
369/// synchronized). Violating any of the above is undefined behavior.
370///
371/// Exercised by the C-API differential courts
372/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
373/// courts; those pass byte-for-byte against the upstream oracle.
374#[no_mangle]
375pub unsafe extern "C" fn xmlAutomataNewTransition(
376    am: XmlAutomataPtr,
377    from: XmlAutomataStatePtr,
378    to: XmlAutomataStatePtr,
379    token: *const core::ffi::c_char,
380    _data: *mut core::ffi::c_void,
381) -> XmlAutomataStatePtr {
382    if am.is_null() || from.is_null() || to.is_null() {
383        return ptr::null_mut();
384    }
385    let tok = if token.is_null() {
386        None
387    } else {
388        // Take the first byte of the token string
389        unsafe { Some(*token as u8) }
390    };
391    unsafe {
392        (*from).transitions.push(AutomataTransition {
393            token: tok,
394            min: 0,
395            max: 0,
396            to: Some(to),
397            once: false,
398            all: false,
399            epsilon: false,
400            counter: -1,
401            data: ptr::null_mut(),
402        });
403    }
404    from
405}
406
407/// Add a counted transition (with min/max bounds).
408///
409/// UPSTREAM-PARITY: `xmlAutomataNewCountTrans()`
410///
411/// # SAFETY
412///
413/// - `am`, `from`, `to`, `token`, `_data` must be valid pointers (or NULL
414///   where the upstream C contract allows), obtained from the
415///   matching constructor/owner and not yet freed; the callee may
416///   take or keep ownership exactly as the C API specifies.
417///
418/// The caller must not race this call with concurrent mutation of the
419/// same objects from other threads (per-object state is not internally
420/// synchronized). Violating any of the above is undefined behavior.
421///
422/// Exercised by the C-API differential courts
423/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
424/// courts; those pass byte-for-byte against the upstream oracle.
425#[no_mangle]
426pub unsafe extern "C" fn xmlAutomataNewCountTrans(
427    am: XmlAutomataPtr,
428    from: XmlAutomataStatePtr,
429    to: XmlAutomataStatePtr,
430    token: *const core::ffi::c_char,
431    _data: *mut core::ffi::c_void,
432    min: c_int,
433    max: c_int,
434) -> XmlAutomataStatePtr {
435    if am.is_null() || from.is_null() || to.is_null() {
436        return ptr::null_mut();
437    }
438    let tok = if token.is_null() {
439        None
440    } else {
441        unsafe { Some(*token as u8) }
442    };
443    unsafe {
444        (*from).transitions.push(AutomataTransition {
445            token: tok,
446            min,
447            max,
448            to: Some(to),
449            once: false,
450            all: false,
451            epsilon: false,
452            counter: -1,
453            data: ptr::null_mut(),
454        });
455    }
456    from
457}
458
459/// Add a "once" transition (consumes exactly once within bounds).
460///
461/// UPSTREAM-PARITY: `xmlAutomataNewOnceTrans()`
462///
463/// # SAFETY
464///
465/// - `am`, `from`, `to`, `token`, `_data` must be valid pointers (or NULL
466///   where the upstream C contract allows), obtained from the
467///   matching constructor/owner and not yet freed; the callee may
468///   take or keep ownership exactly as the C API specifies.
469///
470/// The caller must not race this call with concurrent mutation of the
471/// same objects from other threads (per-object state is not internally
472/// synchronized). Violating any of the above is undefined behavior.
473///
474/// Exercised by the C-API differential courts
475/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
476/// courts; those pass byte-for-byte against the upstream oracle.
477#[no_mangle]
478pub unsafe extern "C" fn xmlAutomataNewOnceTrans(
479    am: XmlAutomataPtr,
480    from: XmlAutomataStatePtr,
481    to: XmlAutomataStatePtr,
482    token: *const core::ffi::c_char,
483    _data: *mut core::ffi::c_void,
484    min: c_int,
485    max: c_int,
486) -> XmlAutomataStatePtr {
487    if am.is_null() || from.is_null() || to.is_null() {
488        return ptr::null_mut();
489    }
490    let tok = if token.is_null() {
491        None
492    } else {
493        unsafe { Some(*token as u8) }
494    };
495    unsafe {
496        (*from).transitions.push(AutomataTransition {
497            token: tok,
498            min,
499            max,
500            to: Some(to),
501            once: true,
502            all: false,
503            epsilon: false,
504            counter: -1,
505            data: ptr::null_mut(),
506        });
507    }
508    from
509}
510
511/// Add a transition that matches any character.
512///
513/// UPSTREAM-PARITY: `xmlAutomataNewAllTrans()`
514///
515/// # SAFETY
516///
517/// - `am`, `from`, `to` must be valid pointers (or NULL
518///   where the upstream C contract allows), obtained from the
519///   matching constructor/owner and not yet freed; the callee may
520///   take or keep ownership exactly as the C API specifies.
521///
522/// The caller must not race this call with concurrent mutation of the
523/// same objects from other threads (per-object state is not internally
524/// synchronized). Violating any of the above is undefined behavior.
525///
526/// Exercised by the C-API differential courts
527/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
528/// courts; those pass byte-for-byte against the upstream oracle.
529#[no_mangle]
530pub unsafe extern "C" fn xmlAutomataNewAllTrans(
531    am: XmlAutomataPtr,
532    from: XmlAutomataStatePtr,
533    to: XmlAutomataStatePtr,
534    _lax: c_int,
535) -> XmlAutomataStatePtr {
536    if am.is_null() || from.is_null() || to.is_null() {
537        return ptr::null_mut();
538    }
539    unsafe {
540        (*from).transitions.push(AutomataTransition {
541            token: None,
542            min: 0,
543            max: 0,
544            to: Some(to),
545            once: false,
546            all: true,
547            epsilon: false,
548            counter: -1,
549            data: ptr::null_mut(),
550        });
551    }
552    from
553}
554
555/// Add a transition associated with a counter.
556///
557/// UPSTREAM-PARITY: `xmlAutomataNewCountedTrans()`
558///
559/// # SAFETY
560///
561/// - `am`, `from`, `to` must be valid pointers (or NULL
562///   where the upstream C contract allows), obtained from the
563///   matching constructor/owner and not yet freed; the callee may
564///   take or keep ownership exactly as the C API specifies.
565///
566/// The caller must not race this call with concurrent mutation of the
567/// same objects from other threads (per-object state is not internally
568/// synchronized). Violating any of the above is undefined behavior.
569///
570/// Exercised by the C-API differential courts
571/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
572/// courts; those pass byte-for-byte against the upstream oracle.
573#[no_mangle]
574pub unsafe extern "C" fn xmlAutomataNewCountedTrans(
575    am: XmlAutomataPtr,
576    from: XmlAutomataStatePtr,
577    to: XmlAutomataStatePtr,
578    counter: c_int,
579) -> XmlAutomataStatePtr {
580    if am.is_null() || from.is_null() || to.is_null() {
581        return ptr::null_mut();
582    }
583    unsafe {
584        (*from).transitions.push(AutomataTransition {
585            token: None,
586            min: 0,
587            max: 0,
588            to: Some(to),
589            once: false,
590            all: false,
591            epsilon: false,
592            counter,
593            data: ptr::null_mut(),
594        });
595    }
596    from
597}
598
599/// Add a transition gated by a counter value.
600///
601/// UPSTREAM-PARITY: `xmlAutomataNewCounterTrans()`
602///
603/// # SAFETY
604///
605/// - `am`, `from`, `to` must be valid pointers (or NULL
606///   where the upstream C contract allows), obtained from the
607///   matching constructor/owner and not yet freed; the callee may
608///   take or keep ownership exactly as the C API specifies.
609///
610/// The caller must not race this call with concurrent mutation of the
611/// same objects from other threads (per-object state is not internally
612/// synchronized). Violating any of the above is undefined behavior.
613///
614/// Exercised by the C-API differential courts
615/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
616/// courts; those pass byte-for-byte against the upstream oracle.
617#[no_mangle]
618pub unsafe extern "C" fn xmlAutomataNewCounterTrans(
619    am: XmlAutomataPtr,
620    from: XmlAutomataStatePtr,
621    to: XmlAutomataStatePtr,
622    counter: c_int,
623) -> XmlAutomataStatePtr {
624    if am.is_null() || from.is_null() || to.is_null() {
625        return ptr::null_mut();
626    }
627    unsafe {
628        (*from).transitions.push(AutomataTransition {
629            token: None,
630            min: 0,
631            max: 0,
632            to: Some(to),
633            once: false,
634            all: false,
635            epsilon: false,
636            counter,
637            data: ptr::null_mut(),
638        });
639    }
640    from
641}
642
643/// Create a new counter with min/max bounds.
644///
645/// UPSTREAM-PARITY: `xmlAutomataNewCounter()`
646///
647/// # SAFETY
648///
649/// - `_am` must be valid pointers (or NULL
650///   where the upstream C contract allows), obtained from the
651///   matching constructor/owner and not yet freed; the callee may
652///   take or keep ownership exactly as the C API specifies.
653///
654/// The caller must not race this call with concurrent mutation of the
655/// same objects from other threads (per-object state is not internally
656/// synchronized). Violating any of the above is undefined behavior.
657///
658/// Exercised by the C-API differential courts
659/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
660/// courts; those pass byte-for-byte against the upstream oracle.
661#[no_mangle]
662pub const unsafe extern "C" fn xmlAutomataNewCounter(
663    _am: XmlAutomataPtr,
664    _min: c_int,
665    _max: c_int,
666) -> c_int {
667    // Counters are tracked by the automata; return a simple counter ID.
668    // In our simplified implementation, return 0 to indicate the first counter.
669    0
670}
671
672/// Compile the automata into a regex.
673///
674/// UPSTREAM-PARITY: `xmlAutomataCompile()`
675///
676/// This builds a regex pattern string from the automata's state machine and
677/// compiles it using the regex engine.
678///
679/// # SAFETY
680///
681/// - `am` must be valid pointers (or NULL
682///   where the upstream C contract allows), obtained from the
683///   matching constructor/owner and not yet freed; the callee may
684///   take or keep ownership exactly as the C API specifies.
685///
686/// The caller must not race this call with concurrent mutation of the
687/// same objects from other threads (per-object state is not internally
688/// synchronized). Violating any of the above is undefined behavior.
689///
690/// Exercised by the C-API differential courts
691/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
692/// courts; those pass byte-for-byte against the upstream oracle.
693#[no_mangle]
694pub unsafe extern "C" fn xmlAutomataCompile(am: XmlAutomataPtr) -> c_int {
695    if am.is_null() {
696        return -1;
697    }
698    unsafe {
699        // Build a regex pattern from the automata transitions.
700        // This is a simplified implementation that handles linear chains
701        // of character transitions.
702        let mut pattern = Vec::new();
703        let init = match (*am).init_state {
704            Some(s) => s,
705            None => return 0, // Empty automata — nothing to compile
706        };
707
708        // Walk the state machine to build a pattern.
709        // For now, build a simple pattern from the transition chain.
710        build_pattern_from_automata(init, &mut pattern);
711
712        if pattern.is_empty() {
713            return 0;
714        }
715
716        // Compile the pattern
717        pattern.push(0); // null-terminate
718        let compiled = xmlRegexpCompile(pattern.as_ptr());
719        if compiled.is_null() {
720            (*am).error = -1;
721            return -1;
722        }
723
724        (*am).regexp = Some(Box::from_raw(compiled));
725        0
726    }
727}
728
729/// Build a regex pattern string from the automata state machine.
730///
731/// This walks the states starting from `state` and emits regex tokens
732/// for each transition.
733unsafe fn build_pattern_from_automata(state: XmlAutomataStatePtr, pattern: &mut Vec<u8>) {
734    if state.is_null() {
735        return;
736    }
737
738    let transitions = &(*state).transitions;
739    if transitions.is_empty() {
740        return;
741    }
742
743    if transitions.len() == 1 {
744        let t = &transitions[0];
745        if t.epsilon {
746            // Follow epsilon transition
747            if let Some(to) = t.to {
748                build_pattern_from_automata(to, pattern);
749            }
750        } else if t.all {
751            pattern.push(b'.');
752            if let Some(to) = t.to {
753                build_pattern_from_automata(to, pattern);
754            }
755        } else if let Some(tok) = t.token {
756            pattern.push(tok);
757            if let Some(to) = t.to {
758                build_pattern_from_automata(to, pattern);
759            }
760        }
761    } else {
762        // Multiple transitions — this is an alternation
763        pattern.push(b'(');
764        for (i, t) in transitions.iter().enumerate() {
765            if i > 0 {
766                pattern.push(b'|');
767            }
768            if let Some(tok) = t.token {
769                pattern.push(tok);
770            } else if t.all {
771                pattern.push(b'.');
772            }
773            if let Some(to) = t.to {
774                // Check if target has further transitions
775                if !(*to).transitions.is_empty() {
776                    // Follow the chain
777                    let mut sub = Vec::new();
778                    build_pattern_from_automata(to, &mut sub);
779                    pattern.extend(sub);
780                }
781            }
782        }
783        pattern.push(b')');
784    }
785}
786
787/// Check if the compiled automata is deterministic.
788///
789/// UPSTREAM-PARITY: `xmlAutomataIsDeterministic()`
790///
791/// # SAFETY
792///
793/// - `am` must be valid pointers (or NULL
794///   where the upstream C contract allows), obtained from the
795///   matching constructor/owner and not yet freed; the callee may
796///   take or keep ownership exactly as the C API specifies.
797///
798/// The caller must not race this call with concurrent mutation of the
799/// same objects from other threads (per-object state is not internally
800/// synchronized). Violating any of the above is undefined behavior.
801///
802/// Exercised by the C-API differential courts
803/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
804/// courts; those pass byte-for-byte against the upstream oracle.
805#[no_mangle]
806pub unsafe extern "C" fn xmlAutomataIsDeterministic(am: XmlAutomataPtr) -> c_int {
807    if am.is_null() {
808        return 0;
809    }
810    unsafe {
811        match &(*am).regexp {
812            Some(regexp) => xmlRegexpIsDeterministic(&**regexp as *const XmlRegexp),
813            None => 1, // Not compiled yet — assume deterministic
814        }
815    }
816}
817
818// ═══════════════════════════════════════════════════════════════════════════════
819// Tests
820// ═══════════════════════════════════════════════════════════════════════════════
821
822#[cfg(test)]
823mod tests {
824    use super::*;
825    use core::ptr;
826
827    #[test]
828    fn test_new_automata() {
829        unsafe {
830            let am = xmlNewAutomata();
831            assert!(!am.is_null());
832            xmlFreeAutomata(am);
833        }
834    }
835
836    #[test]
837    fn test_new_automata_null_safety() {
838        unsafe {
839            xmlFreeAutomata(ptr::null_mut());
840            assert!(xmlAutomataGetInitState(ptr::null_mut()).is_null());
841            assert_eq!(xmlAutomataCompile(ptr::null_mut()), -1);
842        }
843    }
844
845    #[test]
846    fn test_new_state() {
847        unsafe {
848            let am = xmlNewAutomata();
849            let state = xmlAutomataNewState(am);
850            assert!(!state.is_null());
851            let init = xmlAutomataGetInitState(am);
852            assert_eq!(init, state);
853            xmlFreeAutomata(am);
854        }
855    }
856
857    #[test]
858    fn test_epsilon_transition() {
859        unsafe {
860            let am = xmlNewAutomata();
861            let s1 = xmlAutomataNewState(am);
862            let s2 = xmlAutomataNewState(am);
863            let result = xmlAutomataNewEpsilon(am, s1, s2);
864            assert!(!result.is_null());
865            assert_eq!(result, s1);
866            xmlFreeAutomata(am);
867        }
868    }
869
870    #[test]
871    fn test_char_transition() {
872        unsafe {
873            let am = xmlNewAutomata();
874            let s1 = xmlAutomataNewState(am);
875            let s2 = xmlAutomataNewState(am);
876            let token = c"a".as_ptr() as *const core::ffi::c_char;
877            let result = xmlAutomataNewTransition(am, s1, s2, token, ptr::null_mut());
878            assert!(!result.is_null());
879            assert_eq!(result, s1);
880            xmlFreeAutomata(am);
881        }
882    }
883
884    #[test]
885    fn test_count_transition() {
886        unsafe {
887            let am = xmlNewAutomata();
888            let s1 = xmlAutomataNewState(am);
889            let s2 = xmlAutomataNewState(am);
890            let token = c"a".as_ptr() as *const core::ffi::c_char;
891            let result = xmlAutomataNewCountTrans(am, s1, s2, token, ptr::null_mut(), 1, 5);
892            assert!(!result.is_null());
893            xmlFreeAutomata(am);
894        }
895    }
896
897    #[test]
898    fn test_all_transition() {
899        unsafe {
900            let am = xmlNewAutomata();
901            let s1 = xmlAutomataNewState(am);
902            let s2 = xmlAutomataNewState(am);
903            let result = xmlAutomataNewAllTrans(am, s1, s2, 0);
904            assert!(!result.is_null());
905            xmlFreeAutomata(am);
906        }
907    }
908
909    #[test]
910    fn test_once_transition() {
911        unsafe {
912            let am = xmlNewAutomata();
913            let s1 = xmlAutomataNewState(am);
914            let s2 = xmlAutomataNewState(am);
915            let token = c"x".as_ptr() as *const core::ffi::c_char;
916            let result = xmlAutomataNewOnceTrans(am, s1, s2, token, ptr::null_mut(), 0, 1);
917            assert!(!result.is_null());
918            xmlFreeAutomata(am);
919        }
920    }
921
922    #[test]
923    fn test_counter_transition() {
924        unsafe {
925            let am = xmlNewAutomata();
926            let s1 = xmlAutomataNewState(am);
927            let s2 = xmlAutomataNewState(am);
928            let cid = xmlAutomataNewCounter(am, 0, 10);
929            let r1 = xmlAutomataNewCountedTrans(am, s1, s2, cid);
930            assert!(!r1.is_null());
931            let r2 = xmlAutomataNewCounterTrans(am, s2, s1, cid);
932            assert!(!r2.is_null());
933            xmlFreeAutomata(am);
934        }
935    }
936
937    #[test]
938    fn test_compile_empty() {
939        unsafe {
940            let am = xmlNewAutomata();
941            let result = xmlAutomataCompile(am);
942            assert_eq!(result, 0);
943            xmlFreeAutomata(am);
944        }
945    }
946
947    #[test]
948    fn test_set_final_state() {
949        unsafe {
950            let am = xmlNewAutomata();
951            let state = xmlAutomataNewState(am);
952            let result = xmlAutomataSetFinalState(am, state);
953            assert_eq!(result, 0);
954            xmlFreeAutomata(am);
955        }
956    }
957
958    #[test]
959    fn test_is_deterministic_not_compiled() {
960        unsafe {
961            let am = xmlNewAutomata();
962            // Before compilation, should return 1 (assumed deterministic)
963            assert_eq!(xmlAutomataIsDeterministic(am), 1);
964            xmlFreeAutomata(am);
965        }
966    }
967
968    #[test]
969    fn test_null_automata_returns_null_state() {
970        unsafe {
971            let state = xmlAutomataNewState(ptr::null_mut());
972            assert!(state.is_null());
973        }
974    }
975
976    #[test]
977    fn test_null_automata_returns_null_init() {
978        unsafe {
979            assert!(xmlAutomataGetInitState(ptr::null_mut()).is_null());
980        }
981    }
982
983    #[test]
984    fn test_new_state_adds_to_list() {
985        unsafe {
986            let am = xmlNewAutomata();
987            let s1 = xmlAutomataNewState(am);
988            let s2 = xmlAutomataNewState(am);
989            assert!(!s1.is_null());
990            assert!(!s2.is_null());
991            assert_ne!(s1, s2);
992            assert_eq!((*am).states.len(), 2);
993            xmlFreeAutomata(am);
994        }
995    }
996
997    #[test]
998    fn test_compile_simple_chain() {
999        unsafe {
1000            let am = xmlNewAutomata();
1001            let s1 = xmlAutomataNewState(am);
1002            let s2 = xmlAutomataNewState(am);
1003            let s3 = xmlAutomataNewState(am);
1004            let token_a = c"a".as_ptr() as *const core::ffi::c_char;
1005            let token_b = c"b".as_ptr() as *const core::ffi::c_char;
1006            xmlAutomataNewTransition(am, s1, s2, token_a, ptr::null_mut());
1007            xmlAutomataNewTransition(am, s2, s3, token_b, ptr::null_mut());
1008            let result = xmlAutomataCompile(am);
1009            assert_eq!(result, 0);
1010            xmlFreeAutomata(am);
1011        }
1012    }
1013}