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::{xmlRegFreeRegexp, 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    /// List of all states.
104    states: Vec<*mut XmlAutomataState>,
105    /// The initial state.
106    init_state: Option<*mut XmlAutomataState>,
107    /// Last error code.
108    error: c_int,
109}
110
111/// UPSTREAM-PARITY: Corresponds to `_xmlAutomataState` in libxml2.
112#[derive(Debug)]
113#[repr(C)]
114pub struct XmlAutomataState {
115    /// Transitions from this state.
116    transitions: Vec<AutomataTransition>,
117}
118
119/// A transition in the automata.
120#[derive(Debug)]
121#[repr(C)]
122pub struct AutomataTransition {
123    /// Token to match (null means epsilon/any).
124    token: Option<u8>,
125    /// Minimum count (for counted transitions).
126    min: c_int,
127    /// Maximum count (for counted transitions).
128    max: c_int,
129    /// Target state.
130    to: Option<*mut XmlAutomataState>,
131    /// Whether this is a "once" (consuming) transition.
132    once: bool,
133    /// Whether this is an "all" (any character) transition.
134    all: bool,
135    /// Whether this is an epsilon transition.
136    epsilon: bool,
137    /// Counter ID for counted transitions.
138    counter: c_int,
139    /// User data.
140    data: *mut core::ffi::c_void,
141}
142
143// SAFETY: These types are only accessed through C-compatible raw pointers
144// in the automata API. The internal Vecs are properly managed.
145unsafe impl Send for XmlAutomata {}
146unsafe impl Sync for XmlAutomata {}
147unsafe impl Send for XmlAutomataState {}
148unsafe impl Sync for XmlAutomataState {}
149
150/// Create a new automata.
151///
152/// UPSTREAM-PARITY: `xmlNewAutomata()`
153///
154/// # SAFETY
155///
156/// The function touches crate-global state only; it is safe
157/// as long as the caller respects the library's global
158/// initialization/cleanup ordering (xmlInitParser before use,
159/// xmlCleanupParser only after all users are done).
160///
161/// Violating the global lifecycle ordering, or calling this after
162/// teardown or from a signal handler, is undefined behavior.
163#[no_mangle]
164pub unsafe extern "C" fn xmlNewAutomata() -> XmlAutomataPtr {
165    let am = xmlMallocImpl(core::mem::size_of::<XmlAutomata>()) as XmlAutomataPtr;
166    if am.is_null() {
167        return ptr::null_mut();
168    }
169    unsafe {
170        core::ptr::write(&mut (*am).states, Vec::new());
171        (*am).init_state = None;
172        (*am).error = 0;
173    }
174    am
175}
176
177/// Free an automata.
178///
179/// UPSTREAM-PARITY: `xmlFreeAutomata()`
180///
181/// # SAFETY
182///
183/// - `am` must be valid pointers (or NULL
184///   where the upstream C contract allows), obtained from the
185///   matching constructor/owner and not yet freed; the callee may
186///   take or keep ownership exactly as the C API specifies.
187///
188/// The caller must not race this call with concurrent mutation of the
189/// same objects from other threads (per-object state is not internally
190/// synchronized). Violating any of the above is undefined behavior.
191///
192/// Exercised by the C-API differential courts
193/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
194/// courts; those pass byte-for-byte against the upstream oracle.
195#[no_mangle]
196pub unsafe extern "C" fn xmlFreeAutomata(am: XmlAutomataPtr) {
197    if am.is_null() {
198        return;
199    }
200    unsafe {
201        // Free all states
202        for &state in &(*am).states {
203            if !state.is_null() {
204                core::ptr::drop_in_place(&mut (*state).transitions);
205                xmlFreeImpl(state as *mut core::ffi::c_void);
206            }
207        }
208        // Drop the states Vec
209        core::ptr::drop_in_place(&mut (*am).states);
210        // Note: since 11.1-Z.2 (R-000176) the compiled regexp is returned
211        // caller-owned by xmlAutomataCompile (upstream 2.15 xmlregexp.c), so
212        // the automata no longer owns or frees it.
213        xmlFreeImpl(am as *mut core::ffi::c_void);
214    }
215}
216
217/// Create a new automata state.
218///
219/// UPSTREAM-PARITY: `xmlAutomataNewState()`
220///
221/// # SAFETY
222///
223/// - `am` must be valid pointers (or NULL
224///   where the upstream C contract allows), obtained from the
225///   matching constructor/owner and not yet freed; the callee may
226///   take or keep ownership exactly as the C API specifies.
227///
228/// The caller must not race this call with concurrent mutation of the
229/// same objects from other threads (per-object state is not internally
230/// synchronized). Violating any of the above is undefined behavior.
231///
232/// Exercised by the C-API differential courts
233/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
234/// courts; those pass byte-for-byte against the upstream oracle.
235#[no_mangle]
236pub unsafe extern "C" fn xmlAutomataNewState(am: XmlAutomataPtr) -> XmlAutomataStatePtr {
237    if am.is_null() {
238        return ptr::null_mut();
239    }
240    let state = xmlMallocImpl(core::mem::size_of::<XmlAutomataState>()) as XmlAutomataStatePtr;
241    if state.is_null() {
242        return ptr::null_mut();
243    }
244    unsafe {
245        core::ptr::write(&mut (*state).transitions, Vec::new());
246        // Add to the automata's state list
247        (*am).states.push(state);
248        // Set as init state if first
249        if (*am).init_state.is_none() {
250            (*am).init_state = Some(state);
251        }
252    }
253    state
254}
255
256/// Set a state as the final (accepting) state.
257///
258/// UPSTREAM-PARITY: `xmlAutomataSetFinalState()`
259///
260/// # SAFETY
261///
262/// - `_am`, `_state` must be valid pointers (or NULL
263///   where the upstream C contract allows), obtained from the
264///   matching constructor/owner and not yet freed; the callee may
265///   take or keep ownership exactly as the C API specifies.
266///
267/// The caller must not race this call with concurrent mutation of the
268/// same objects from other threads (per-object state is not internally
269/// synchronized). Violating any of the above is undefined behavior.
270///
271/// Exercised by the C-API differential courts
272/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
273/// courts; those pass byte-for-byte against the upstream oracle.
274#[no_mangle]
275pub const unsafe extern "C" fn xmlAutomataSetFinalState(
276    _am: XmlAutomataPtr,
277    _state: XmlAutomataStatePtr,
278) -> c_int {
279    // In our implementation, final states are determined by the compiled regex.
280    // This is a no-op for the automata builder; final states are handled during
281    // compilation.
282    0
283}
284
285/// Get the initial state of the automata.
286///
287/// UPSTREAM-PARITY: `xmlAutomataGetInitState()`
288///
289/// # SAFETY
290///
291/// - `am` must be valid pointers (or NULL
292///   where the upstream C contract allows), obtained from the
293///   matching constructor/owner and not yet freed; the callee may
294///   take or keep ownership exactly as the C API specifies.
295///
296/// The caller must not race this call with concurrent mutation of the
297/// same objects from other threads (per-object state is not internally
298/// synchronized). Violating any of the above is undefined behavior.
299///
300/// Exercised by the C-API differential courts
301/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
302/// courts; those pass byte-for-byte against the upstream oracle.
303#[no_mangle]
304pub unsafe extern "C" fn xmlAutomataGetInitState(am: XmlAutomataPtr) -> XmlAutomataStatePtr {
305    if am.is_null() {
306        return ptr::null_mut();
307    }
308    unsafe { (*am).init_state.unwrap_or(ptr::null_mut()) }
309}
310
311/// Add an epsilon (empty) transition between two states.
312///
313/// UPSTREAM-PARITY: `xmlAutomataNewEpsilon()`
314///
315/// # SAFETY
316///
317/// - `am`, `from`, `to` must be valid pointers (or NULL
318///   where the upstream C contract allows), obtained from the
319///   matching constructor/owner and not yet freed; the callee may
320///   take or keep ownership exactly as the C API specifies.
321///
322/// The caller must not race this call with concurrent mutation of the
323/// same objects from other threads (per-object state is not internally
324/// synchronized). Violating any of the above is undefined behavior.
325///
326/// Exercised by the C-API differential courts
327/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
328/// courts; those pass byte-for-byte against the upstream oracle.
329#[no_mangle]
330pub unsafe extern "C" fn xmlAutomataNewEpsilon(
331    am: XmlAutomataPtr,
332    from: XmlAutomataStatePtr,
333    to: XmlAutomataStatePtr,
334) -> XmlAutomataStatePtr {
335    if am.is_null() || from.is_null() || to.is_null() {
336        return ptr::null_mut();
337    }
338    unsafe {
339        (*from).transitions.push(AutomataTransition {
340            token: None,
341            min: 0,
342            max: 0,
343            to: Some(to),
344            once: false,
345            all: false,
346            epsilon: true,
347            counter: -1,
348            data: ptr::null_mut(),
349        });
350    }
351    from
352}
353
354/// Add a character transition between two states.
355///
356/// UPSTREAM-PARITY: `xmlAutomataNewTransition()`
357///
358/// # SAFETY
359///
360/// - `am`, `from`, `to`, `token`, `_data` must be valid pointers (or NULL
361///   where the upstream C contract allows), obtained from the
362///   matching constructor/owner and not yet freed; the callee may
363///   take or keep ownership exactly as the C API specifies.
364///
365/// The caller must not race this call with concurrent mutation of the
366/// same objects from other threads (per-object state is not internally
367/// synchronized). Violating any of the above is undefined behavior.
368///
369/// Exercised by the C-API differential courts
370/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
371/// courts; those pass byte-for-byte against the upstream oracle.
372#[no_mangle]
373pub unsafe extern "C" fn xmlAutomataNewTransition(
374    am: XmlAutomataPtr,
375    from: XmlAutomataStatePtr,
376    to: XmlAutomataStatePtr,
377    token: *const core::ffi::c_char,
378    _data: *mut core::ffi::c_void,
379) -> XmlAutomataStatePtr {
380    if am.is_null() || from.is_null() || to.is_null() {
381        return ptr::null_mut();
382    }
383    let tok = if token.is_null() {
384        None
385    } else {
386        // Take the first byte of the token string
387        unsafe { Some(*token as u8) }
388    };
389    unsafe {
390        (*from).transitions.push(AutomataTransition {
391            token: tok,
392            min: 0,
393            max: 0,
394            to: Some(to),
395            once: false,
396            all: false,
397            epsilon: false,
398            counter: -1,
399            data: ptr::null_mut(),
400        });
401    }
402    from
403}
404
405/// Add a counted transition with min/max bounds (upstream xmlautomata.h:
406/// `(am, from, to, token, min, max, data)` — the candidate previously
407/// swapped `min`/`max` with `data`, misreading five registers; R-000176).
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    min: c_int,
432    max: c_int,
433    _data: *mut core::ffi::c_void,
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; upstream
460/// order `(am, from, to, token, min, max, data)` — R-000176).
461///
462/// UPSTREAM-PARITY: `xmlAutomataNewOnceTrans()`
463///
464/// # SAFETY
465///
466/// - `am`, `from`, `to`, `token`, `data` must be valid pointers (or NULL
467///   where the upstream C contract allows), obtained from the
468///   matching constructor/owner and not yet freed; the callee may
469///   take or keep ownership exactly as the C API specifies.
470///
471/// The caller must not race this call with concurrent mutation of the
472/// same objects from other threads (per-object state is not internally
473/// synchronized). Violating any of the above is undefined behavior.
474///
475/// Exercised by the C-API differential courts
476/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
477/// courts; those pass byte-for-byte against the upstream oracle.
478#[no_mangle]
479pub unsafe extern "C" fn xmlAutomataNewOnceTrans(
480    am: XmlAutomataPtr,
481    from: XmlAutomataStatePtr,
482    to: XmlAutomataStatePtr,
483    token: *const core::ffi::c_char,
484    min: c_int,
485    max: c_int,
486    _data: *mut core::ffi::c_void,
487) -> XmlAutomataStatePtr {
488    if am.is_null() || from.is_null() || to.is_null() {
489        return ptr::null_mut();
490    }
491    let tok = if token.is_null() {
492        None
493    } else {
494        unsafe { Some(*token as u8) }
495    };
496    unsafe {
497        (*from).transitions.push(AutomataTransition {
498            token: tok,
499            min,
500            max,
501            to: Some(to),
502            once: true,
503            all: false,
504            epsilon: false,
505            counter: -1,
506            data: ptr::null_mut(),
507        });
508    }
509    from
510}
511
512/// Add a transition that matches any character.
513///
514/// UPSTREAM-PARITY: `xmlAutomataNewAllTrans()`
515///
516/// # SAFETY
517///
518/// - `am`, `from`, `to` must be valid pointers (or NULL
519///   where the upstream C contract allows), obtained from the
520///   matching constructor/owner and not yet freed; the callee may
521///   take or keep ownership exactly as the C API specifies.
522///
523/// The caller must not race this call with concurrent mutation of the
524/// same objects from other threads (per-object state is not internally
525/// synchronized). Violating any of the above is undefined behavior.
526///
527/// Exercised by the C-API differential courts
528/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
529/// courts; those pass byte-for-byte against the upstream oracle.
530#[no_mangle]
531pub unsafe extern "C" fn xmlAutomataNewAllTrans(
532    am: XmlAutomataPtr,
533    from: XmlAutomataStatePtr,
534    to: XmlAutomataStatePtr,
535    _lax: c_int,
536) -> XmlAutomataStatePtr {
537    if am.is_null() || from.is_null() || to.is_null() {
538        return ptr::null_mut();
539    }
540    unsafe {
541        (*from).transitions.push(AutomataTransition {
542            token: None,
543            min: 0,
544            max: 0,
545            to: Some(to),
546            once: false,
547            all: true,
548            epsilon: false,
549            counter: -1,
550            data: ptr::null_mut(),
551        });
552    }
553    from
554}
555
556/// Add a transition associated with a counter.
557///
558/// UPSTREAM-PARITY: `xmlAutomataNewCountedTrans()`
559///
560/// # SAFETY
561///
562/// - `am`, `from`, `to` must be valid pointers (or NULL
563///   where the upstream C contract allows), obtained from the
564///   matching constructor/owner and not yet freed; the callee may
565///   take or keep ownership exactly as the C API specifies.
566///
567/// The caller must not race this call with concurrent mutation of the
568/// same objects from other threads (per-object state is not internally
569/// synchronized). Violating any of the above is undefined behavior.
570///
571/// Exercised by the C-API differential courts
572/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
573/// courts; those pass byte-for-byte against the upstream oracle.
574#[no_mangle]
575pub unsafe extern "C" fn xmlAutomataNewCountedTrans(
576    am: XmlAutomataPtr,
577    from: XmlAutomataStatePtr,
578    to: XmlAutomataStatePtr,
579    counter: c_int,
580) -> XmlAutomataStatePtr {
581    if am.is_null() || from.is_null() || to.is_null() {
582        return ptr::null_mut();
583    }
584    unsafe {
585        (*from).transitions.push(AutomataTransition {
586            token: None,
587            min: 0,
588            max: 0,
589            to: Some(to),
590            once: false,
591            all: false,
592            epsilon: false,
593            counter,
594            data: ptr::null_mut(),
595        });
596    }
597    from
598}
599
600/// Add a transition gated by a counter value.
601///
602/// UPSTREAM-PARITY: `xmlAutomataNewCounterTrans()`
603///
604/// # SAFETY
605///
606/// - `am`, `from`, `to` must be valid pointers (or NULL
607///   where the upstream C contract allows), obtained from the
608///   matching constructor/owner and not yet freed; the callee may
609///   take or keep ownership exactly as the C API specifies.
610///
611/// The caller must not race this call with concurrent mutation of the
612/// same objects from other threads (per-object state is not internally
613/// synchronized). Violating any of the above is undefined behavior.
614///
615/// Exercised by the C-API differential courts
616/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
617/// courts; those pass byte-for-byte against the upstream oracle.
618#[no_mangle]
619pub unsafe extern "C" fn xmlAutomataNewCounterTrans(
620    am: XmlAutomataPtr,
621    from: XmlAutomataStatePtr,
622    to: XmlAutomataStatePtr,
623    counter: c_int,
624) -> XmlAutomataStatePtr {
625    if am.is_null() || from.is_null() || to.is_null() {
626        return ptr::null_mut();
627    }
628    unsafe {
629        (*from).transitions.push(AutomataTransition {
630            token: None,
631            min: 0,
632            max: 0,
633            to: Some(to),
634            once: false,
635            all: false,
636            epsilon: false,
637            counter,
638            data: ptr::null_mut(),
639        });
640    }
641    from
642}
643
644/// Create a new counter with min/max bounds.
645///
646/// UPSTREAM-PARITY: `xmlAutomataNewCounter()`
647///
648/// # SAFETY
649///
650/// - `_am` must be valid pointers (or NULL
651///   where the upstream C contract allows), obtained from the
652///   matching constructor/owner and not yet freed; the callee may
653///   take or keep ownership exactly as the C API specifies.
654///
655/// The caller must not race this call with concurrent mutation of the
656/// same objects from other threads (per-object state is not internally
657/// synchronized). Violating any of the above is undefined behavior.
658///
659/// Exercised by the C-API differential courts
660/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
661/// courts; those pass byte-for-byte against the upstream oracle.
662#[no_mangle]
663pub const unsafe extern "C" fn xmlAutomataNewCounter(
664    _am: XmlAutomataPtr,
665    _min: c_int,
666    _max: c_int,
667) -> c_int {
668    // Counters are tracked by the automata; return a simple counter ID.
669    // In our simplified implementation, return 0 to indicate the first counter.
670    0
671}
672
673/// Compile the automata into a regex (upstream xmlregexp.c 2.15:
674/// caller-owned `xmlRegexp *` return — R-000176, the candidate previously
675/// returned an int error code and boxed the regexp into the automata).
676///
677/// UPSTREAM-PARITY: `xmlAutomataCompile()`
678///
679/// This builds a regex pattern string from the automata's state machine and
680/// compiles it using the regex engine. The returned regexp is owned by the
681/// caller (free with `xmlRegFreeRegexp`), exactly as upstream.
682///
683/// # SAFETY
684///
685/// - `am` must be valid pointers (or NULL
686///   where the upstream C contract allows), obtained from the
687///   matching constructor/owner and not yet freed; the callee may
688///   take or keep ownership exactly as the C API specifies.
689///
690/// The caller must not race this call with concurrent mutation of the
691/// same objects from other threads (per-object state is not internally
692/// synchronized). Violating any of the above is undefined behavior.
693///
694/// Exercised by the C-API differential courts
695/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
696/// courts; those pass byte-for-byte against the upstream oracle.
697#[no_mangle]
698pub unsafe extern "C" fn xmlAutomataCompile(am: XmlAutomataPtr) -> *mut XmlRegexp {
699    if am.is_null() {
700        return ptr::null_mut();
701    }
702    unsafe {
703        // Build a regex pattern from the automata transitions.
704        // This is a simplified implementation that handles linear chains
705        // of character transitions.
706        let mut pattern = Vec::new();
707        let init = match (*am).init_state {
708            Some(s) => s,
709            None => return ptr::null_mut(), // Empty automata — nothing to compile
710        };
711
712        // Walk the state machine to build a pattern.
713        build_pattern_from_automata(init, &mut pattern);
714
715        if pattern.is_empty() {
716            return ptr::null_mut();
717        }
718
719        // Compile the pattern
720        pattern.push(0); // null-terminate
721        let compiled = xmlRegexpCompile(pattern.as_ptr());
722        if compiled.is_null() {
723            (*am).error = -1;
724            return ptr::null_mut();
725        }
726
727        compiled
728    }
729}
730
731/// Build a regex pattern string from the automata state machine.
732///
733/// This walks the states starting from `state` and emits regex tokens
734/// for each transition.
735unsafe fn build_pattern_from_automata(state: XmlAutomataStatePtr, pattern: &mut Vec<u8>) {
736    if state.is_null() {
737        return;
738    }
739
740    let transitions = &(*state).transitions;
741    if transitions.is_empty() {
742        return;
743    }
744
745    if transitions.len() == 1 {
746        let t = &transitions[0];
747        if t.epsilon {
748            // Follow epsilon transition
749            if let Some(to) = t.to {
750                build_pattern_from_automata(to, pattern);
751            }
752        } else if t.all {
753            pattern.push(b'.');
754            if let Some(to) = t.to {
755                build_pattern_from_automata(to, pattern);
756            }
757        } else if let Some(tok) = t.token {
758            pattern.push(tok);
759            if let Some(to) = t.to {
760                build_pattern_from_automata(to, pattern);
761            }
762        }
763    } else {
764        // Multiple transitions — this is an alternation
765        pattern.push(b'(');
766        for (i, t) in transitions.iter().enumerate() {
767            if i > 0 {
768                pattern.push(b'|');
769            }
770            if let Some(tok) = t.token {
771                pattern.push(tok);
772            } else if t.all {
773                pattern.push(b'.');
774            }
775            if let Some(to) = t.to {
776                // Check if target has further transitions
777                if !(*to).transitions.is_empty() {
778                    // Follow the chain
779                    let mut sub = Vec::new();
780                    build_pattern_from_automata(to, &mut sub);
781                    pattern.extend(sub);
782                }
783            }
784        }
785        pattern.push(b')');
786    }
787}
788
789/// Check if the compiled automata is deterministic.
790///
791/// Report whether the automata's language is deterministic (upstream
792/// xmlregexp.c 2.15 `xmlAutomataIsDeterminist` — computed on the automata,
793/// independent of a stored compiled regexp; the candidate compiles a
794/// throwaway regexp and checks its determinism).
795///
796/// UPSTREAM-PARITY: `xmlAutomataIsDeterminist()`
797///
798/// Returns 1 if deterministic, 0 if not, -1 for a NULL automata (upstream
799/// xmlregexp.c: `if (am == NULL) return(-1);`).
800///
801/// # SAFETY
802///
803/// - `am` must be valid pointers (or NULL
804///   where the upstream C contract allows), obtained from the
805///   matching constructor/owner and not yet freed; the callee may
806///   take or keep ownership exactly as the C API specifies.
807///
808/// The caller must not race this call with concurrent mutation of the
809/// same objects from other threads (per-object state is not internally
810/// synchronized). Violating any of the above is undefined behavior.
811///
812/// Exercised by the C-API differential courts
813/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
814/// courts; those pass byte-for-byte against the upstream oracle.
815#[no_mangle]
816pub unsafe extern "C" fn xmlAutomataIsDeterministic(am: XmlAutomataPtr) -> c_int {
817    if am.is_null() {
818        return -1;
819    }
820    unsafe {
821        let compiled = xmlAutomataCompile(am);
822        if compiled.is_null() {
823            // No accepting path — the (empty) language is trivially
824            // deterministic.
825            return 1;
826        }
827        let ret = xmlRegexpIsDeterministic(compiled as *const XmlRegexp);
828        xmlRegFreeRegexp(compiled);
829        ret
830    }
831}
832
833// ═══════════════════════════════════════════════════════════════════════════════
834// Tests
835// ═══════════════════════════════════════════════════════════════════════════════
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840    use core::ptr;
841
842    #[test]
843    fn test_new_automata() {
844        unsafe {
845            let am = xmlNewAutomata();
846            assert!(!am.is_null());
847            xmlFreeAutomata(am);
848        }
849    }
850
851    #[test]
852    fn test_new_automata_null_safety() {
853        unsafe {
854            xmlFreeAutomata(ptr::null_mut());
855            assert!(xmlAutomataGetInitState(ptr::null_mut()).is_null());
856            assert!(xmlAutomataCompile(ptr::null_mut()).is_null());
857        }
858    }
859
860    #[test]
861    fn test_new_state() {
862        unsafe {
863            let am = xmlNewAutomata();
864            let state = xmlAutomataNewState(am);
865            assert!(!state.is_null());
866            let init = xmlAutomataGetInitState(am);
867            assert_eq!(init, state);
868            xmlFreeAutomata(am);
869        }
870    }
871
872    #[test]
873    fn test_epsilon_transition() {
874        unsafe {
875            let am = xmlNewAutomata();
876            let s1 = xmlAutomataNewState(am);
877            let s2 = xmlAutomataNewState(am);
878            let result = xmlAutomataNewEpsilon(am, s1, s2);
879            assert!(!result.is_null());
880            assert_eq!(result, s1);
881            xmlFreeAutomata(am);
882        }
883    }
884
885    #[test]
886    fn test_char_transition() {
887        unsafe {
888            let am = xmlNewAutomata();
889            let s1 = xmlAutomataNewState(am);
890            let s2 = xmlAutomataNewState(am);
891            let token = c"a".as_ptr() as *const core::ffi::c_char;
892            let result = xmlAutomataNewTransition(am, s1, s2, token, ptr::null_mut());
893            assert!(!result.is_null());
894            assert_eq!(result, s1);
895            xmlFreeAutomata(am);
896        }
897    }
898
899    #[test]
900    fn test_count_transition() {
901        unsafe {
902            let am = xmlNewAutomata();
903            let s1 = xmlAutomataNewState(am);
904            let s2 = xmlAutomataNewState(am);
905            let token = c"a".as_ptr() as *const core::ffi::c_char;
906            let result = xmlAutomataNewCountTrans(am, s1, s2, token, 1, 5, ptr::null_mut());
907            assert!(!result.is_null());
908            xmlFreeAutomata(am);
909        }
910    }
911
912    #[test]
913    fn test_all_transition() {
914        unsafe {
915            let am = xmlNewAutomata();
916            let s1 = xmlAutomataNewState(am);
917            let s2 = xmlAutomataNewState(am);
918            let result = xmlAutomataNewAllTrans(am, s1, s2, 0);
919            assert!(!result.is_null());
920            xmlFreeAutomata(am);
921        }
922    }
923
924    #[test]
925    fn test_once_transition() {
926        unsafe {
927            let am = xmlNewAutomata();
928            let s1 = xmlAutomataNewState(am);
929            let s2 = xmlAutomataNewState(am);
930            let token = c"x".as_ptr() as *const core::ffi::c_char;
931            let result = xmlAutomataNewOnceTrans(am, s1, s2, token, 0, 1, ptr::null_mut());
932            assert!(!result.is_null());
933            xmlFreeAutomata(am);
934        }
935    }
936
937    #[test]
938    fn test_counter_transition() {
939        unsafe {
940            let am = xmlNewAutomata();
941            let s1 = xmlAutomataNewState(am);
942            let s2 = xmlAutomataNewState(am);
943            let cid = xmlAutomataNewCounter(am, 0, 10);
944            let r1 = xmlAutomataNewCountedTrans(am, s1, s2, cid);
945            assert!(!r1.is_null());
946            let r2 = xmlAutomataNewCounterTrans(am, s2, s1, cid);
947            assert!(!r2.is_null());
948            xmlFreeAutomata(am);
949        }
950    }
951
952    #[test]
953    fn test_compile_empty() {
954        unsafe {
955            let am = xmlNewAutomata();
956            let result = xmlAutomataCompile(am);
957            // No accepting path — NULL (upstream xmlRegEpxFromParse has no
958            // final-state path to compile).
959            assert!(result.is_null());
960            xmlFreeAutomata(am);
961        }
962    }
963
964    #[test]
965    fn test_set_final_state() {
966        unsafe {
967            let am = xmlNewAutomata();
968            let state = xmlAutomataNewState(am);
969            let result = xmlAutomataSetFinalState(am, state);
970            assert_eq!(result, 0);
971            xmlFreeAutomata(am);
972        }
973    }
974
975    #[test]
976    fn test_is_deterministic_not_compiled() {
977        unsafe {
978            let am = xmlNewAutomata();
979            // Before compilation, should return 1 (assumed deterministic)
980            assert_eq!(xmlAutomataIsDeterministic(am), 1);
981            xmlFreeAutomata(am);
982        }
983    }
984
985    #[test]
986    fn test_null_automata_returns_null_state() {
987        unsafe {
988            let state = xmlAutomataNewState(ptr::null_mut());
989            assert!(state.is_null());
990        }
991    }
992
993    #[test]
994    fn test_null_automata_returns_null_init() {
995        unsafe {
996            assert!(xmlAutomataGetInitState(ptr::null_mut()).is_null());
997        }
998    }
999
1000    #[test]
1001    fn test_new_state_adds_to_list() {
1002        unsafe {
1003            let am = xmlNewAutomata();
1004            let s1 = xmlAutomataNewState(am);
1005            let s2 = xmlAutomataNewState(am);
1006            assert!(!s1.is_null());
1007            assert!(!s2.is_null());
1008            assert_ne!(s1, s2);
1009            assert_eq!((*am).states.len(), 2);
1010            xmlFreeAutomata(am);
1011        }
1012    }
1013
1014    #[test]
1015    fn test_compile_simple_chain() {
1016        unsafe {
1017            let am = xmlNewAutomata();
1018            let s1 = xmlAutomataNewState(am);
1019            let s2 = xmlAutomataNewState(am);
1020            let s3 = xmlAutomataNewState(am);
1021            let token_a = c"a".as_ptr() as *const core::ffi::c_char;
1022            let token_b = c"b".as_ptr() as *const core::ffi::c_char;
1023            xmlAutomataNewTransition(am, s1, s2, token_a, ptr::null_mut());
1024            xmlAutomataNewTransition(am, s2, s3, token_b, ptr::null_mut());
1025            let result = xmlAutomataCompile(am);
1026            // Caller-owned regexp (upstream 2.15): the chain compiles to a
1027            // non-NULL regexp.
1028            assert!(!result.is_null());
1029            xmlRegFreeRegexp(result);
1030            xmlFreeAutomata(am);
1031        }
1032    }
1033}