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 that a fresh automata is created and released.
843    ///
844    /// # Safety
845    ///
846    /// - `am` returned by `xmlNewAutomata` must be non-NULL (asserted) and
847    ///   not yet freed; `xmlFreeAutomata` takes ownership, so it must be
848    ///   called exactly once and never concurrently with other uses of `am`.
849    #[test]
850    fn test_new_automata() {
851        unsafe {
852            let am = xmlNewAutomata();
853            assert!(!am.is_null());
854            xmlFreeAutomata(am);
855        }
856    }
857
858    /// Test that the free/get-init/compile entry points accept NULL.
859    ///
860    /// # Safety
861    ///
862    /// - `xmlFreeAutomata`, `xmlAutomataGetInitState` and `xmlAutomataCompile`
863    ///   accept NULL per the upstream C contract.
864    /// - `xmlAutomataCompile` returns NULL here; when it returns non-NULL the
865    ///   caller owns the resulting regexp and must free it exactly once with
866    ///   `xmlRegFreeRegexp`.
867    #[test]
868    fn test_new_automata_null_safety() {
869        unsafe {
870            xmlFreeAutomata(ptr::null_mut());
871            assert!(xmlAutomataGetInitState(ptr::null_mut()).is_null());
872            assert!(xmlAutomataCompile(ptr::null_mut()).is_null());
873        }
874    }
875
876    /// Test that a new state becomes the automata init state.
877    ///
878    /// # Safety
879    ///
880    /// - `am` must be a non-NULL, not-yet-freed pointer from `xmlNewAutomata`.
881    /// - The state pointer returned by `xmlAutomataNewState` is owned by the
882    ///   automata: it is only borrowed here and must not be freed separately.
883    /// - `xmlFreeAutomata(am)` releases the automata and all its states
884    ///   exactly once, after all borrowed state pointers are dead.
885    #[test]
886    fn test_new_state() {
887        unsafe {
888            let am = xmlNewAutomata();
889            let state = xmlAutomataNewState(am);
890            assert!(!state.is_null());
891            let init = xmlAutomataGetInitState(am);
892            assert_eq!(init, state);
893            xmlFreeAutomata(am);
894        }
895    }
896
897    /// Test adding an epsilon transition between two states.
898    ///
899    /// # Safety
900    ///
901    /// - `am`, `s1`, `s2` must be non-NULL pointers obtained from
902    ///   `xmlNewAutomata` and `xmlAutomataNewState` on the same automata and
903    ///   not yet freed; the states are borrowed and owned by `am`.
904    /// - The returned pointer aliases `s1` and must not be freed separately.
905    /// - `xmlFreeAutomata(am)` runs exactly once at the end.
906    #[test]
907    fn test_epsilon_transition() {
908        unsafe {
909            let am = xmlNewAutomata();
910            let s1 = xmlAutomataNewState(am);
911            let s2 = xmlAutomataNewState(am);
912            let result = xmlAutomataNewEpsilon(am, s1, s2);
913            assert!(!result.is_null());
914            assert_eq!(result, s1);
915            xmlFreeAutomata(am);
916        }
917    }
918
919    /// Test adding a character transition with a one-byte token.
920    ///
921    /// # Safety
922    ///
923    /// - `am`, `s1`, `s2` must be valid, not-yet-freed pointers owned by the
924    ///   automata as described for `test_epsilon_transition`.
925    /// - `token` must point to a valid NUL-terminated C string that lives for
926    ///   the duration of the call (only its first byte is read).
927    /// - The result aliases `s1`; the automata is freed exactly once at the
928    ///   end with `xmlFreeAutomata`.
929    #[test]
930    fn test_char_transition() {
931        unsafe {
932            let am = xmlNewAutomata();
933            let s1 = xmlAutomataNewState(am);
934            let s2 = xmlAutomataNewState(am);
935            let token = c"a".as_ptr() as *const core::ffi::c_char;
936            let result = xmlAutomataNewTransition(am, s1, s2, token, ptr::null_mut());
937            assert!(!result.is_null());
938            assert_eq!(result, s1);
939            xmlFreeAutomata(am);
940        }
941    }
942
943    /// Test adding a counted transition with min/max bounds.
944    ///
945    /// # Safety
946    ///
947    /// - `am`, `s1`, `s2` must be valid, not-yet-freed pointers from the
948    ///   automata constructors; `token` must point to a valid NUL-terminated
949    ///   C string valid for the call (only its first byte is read).
950    /// - The returned pointer aliases `s1`; `xmlFreeAutomata(am)` is the only
951    ///   free and runs exactly once.
952    #[test]
953    fn test_count_transition() {
954        unsafe {
955            let am = xmlNewAutomata();
956            let s1 = xmlAutomataNewState(am);
957            let s2 = xmlAutomataNewState(am);
958            let token = c"a".as_ptr() as *const core::ffi::c_char;
959            let result = xmlAutomataNewCountTrans(am, s1, s2, token, 1, 5, ptr::null_mut());
960            assert!(!result.is_null());
961            xmlFreeAutomata(am);
962        }
963    }
964
965    /// Test adding an any-character transition.
966    ///
967    /// # Safety
968    ///
969    /// - `am`, `s1`, `s2` must be non-NULL, not-yet-freed automata-owned
970    ///   pointers; the result aliases `s1` and must not be freed separately;
971    ///   `xmlFreeAutomata(am)` runs exactly once at the end.
972    #[test]
973    fn test_all_transition() {
974        unsafe {
975            let am = xmlNewAutomata();
976            let s1 = xmlAutomataNewState(am);
977            let s2 = xmlAutomataNewState(am);
978            let result = xmlAutomataNewAllTrans(am, s1, s2, 0);
979            assert!(!result.is_null());
980            xmlFreeAutomata(am);
981        }
982    }
983
984    /// Test adding a once (consuming) transition.
985    ///
986    /// # Safety
987    ///
988    /// - `am`, `s1`, `s2` must be valid non-NULL automata-owned pointers;
989    ///   `token` must point to a valid NUL-terminated C string valid for the
990    ///   call; the result aliases `s1`; `xmlFreeAutomata(am)` runs exactly
991    ///   once at the end.
992    #[test]
993    fn test_once_transition() {
994        unsafe {
995            let am = xmlNewAutomata();
996            let s1 = xmlAutomataNewState(am);
997            let s2 = xmlAutomataNewState(am);
998            let token = c"x".as_ptr() as *const core::ffi::c_char;
999            let result = xmlAutomataNewOnceTrans(am, s1, s2, token, 0, 1, ptr::null_mut());
1000            assert!(!result.is_null());
1001            xmlFreeAutomata(am);
1002        }
1003    }
1004
1005    /// Test counter-based counted/counter transitions.
1006    ///
1007    /// # Safety
1008    ///
1009    /// - `am`, `s1`, `s2` must be valid non-NULL pointers owned by the
1010    ///   automata and not yet freed; the counter ID and the transition
1011    ///   pointers returned are owned by `am` (borrowed here) and must not be
1012    ///   freed separately; `xmlFreeAutomata(am)` frees everything exactly
1013    ///   once.
1014    #[test]
1015    fn test_counter_transition() {
1016        unsafe {
1017            let am = xmlNewAutomata();
1018            let s1 = xmlAutomataNewState(am);
1019            let s2 = xmlAutomataNewState(am);
1020            let cid = xmlAutomataNewCounter(am, 0, 10);
1021            let r1 = xmlAutomataNewCountedTrans(am, s1, s2, cid);
1022            assert!(!r1.is_null());
1023            let r2 = xmlAutomataNewCounterTrans(am, s2, s1, cid);
1024            assert!(!r2.is_null());
1025            xmlFreeAutomata(am);
1026        }
1027    }
1028
1029    /// Test that compiling an automata with no accepting path yields NULL.
1030    ///
1031    /// # Safety
1032    ///
1033    /// - `am` must be a non-NULL, not-yet-freed pointer from `xmlNewAutomata`.
1034    /// - `xmlAutomataCompile` returns NULL here, so no regexp is owned; when
1035    ///   it returns non-NULL the caller must free it exactly once with
1036    ///   `xmlRegFreeRegexp`.
1037    /// - `xmlFreeAutomata(am)` runs exactly once at the end.
1038    #[test]
1039    fn test_compile_empty() {
1040        unsafe {
1041            let am = xmlNewAutomata();
1042            let result = xmlAutomataCompile(am);
1043            // No accepting path — NULL (upstream xmlRegEpxFromParse has no
1044            // final-state path to compile).
1045            assert!(result.is_null());
1046            xmlFreeAutomata(am);
1047        }
1048    }
1049
1050    /// Test that marking a state final returns 0 (no-op in this builder).
1051    ///
1052    /// # Safety
1053    ///
1054    /// - `am` and `state` must be valid, not-yet-freed automata-owned
1055    ///   pointers; `xmlAutomataSetFinalState` does not take ownership, and
1056    ///   the automata is freed exactly once with `xmlFreeAutomata` at the end.
1057    #[test]
1058    fn test_set_final_state() {
1059        unsafe {
1060            let am = xmlNewAutomata();
1061            let state = xmlAutomataNewState(am);
1062            let result = xmlAutomataSetFinalState(am, state);
1063            assert_eq!(result, 0);
1064            xmlFreeAutomata(am);
1065        }
1066    }
1067
1068    /// Test that an uncompiled automata reports deterministic (1).
1069    ///
1070    /// # Safety
1071    ///
1072    /// - `am` must be a valid, not-yet-freed pointer from `xmlNewAutomata`;
1073    ///   `xmlAutomataIsDeterministic` borrows it, and `xmlFreeAutomata(am)`
1074    ///   runs exactly once at the end.
1075    #[test]
1076    fn test_is_deterministic_not_compiled() {
1077        unsafe {
1078            let am = xmlNewAutomata();
1079            // Before compilation, should return 1 (assumed deterministic)
1080            assert_eq!(xmlAutomataIsDeterministic(am), 1);
1081            xmlFreeAutomata(am);
1082        }
1083    }
1084
1085    /// Test that `xmlAutomataNewState` returns NULL for a NULL automata.
1086    ///
1087    /// # Safety
1088    ///
1089    /// - Passing NULL to `xmlAutomataNewState` is accepted by the C contract
1090    ///   and returns NULL without dereferencing the argument.
1091    #[test]
1092    fn test_null_automata_returns_null_state() {
1093        unsafe {
1094            let state = xmlAutomataNewState(ptr::null_mut());
1095            assert!(state.is_null());
1096        }
1097    }
1098
1099    /// Test that `xmlAutomataGetInitState` returns NULL for a NULL automata.
1100    ///
1101    /// # Safety
1102    ///
1103    /// - Passing NULL to `xmlAutomataGetInitState` is accepted and returns
1104    ///   NULL without dereferencing; the result is not dereferenced here.
1105    #[test]
1106    fn test_null_automata_returns_null_init() {
1107        unsafe {
1108            assert!(xmlAutomataGetInitState(ptr::null_mut()).is_null());
1109        }
1110    }
1111
1112    /// Test that new states are appended to the automata state list.
1113    ///
1114    /// # Safety
1115    ///
1116    /// - `am` must be non-NULL and not yet freed; `(*am).states` is read
1117    ///   directly, so `am` must point to a live `XmlAutomata` (guaranteed by
1118    ///   `xmlNewAutomata` here) and must not be mutated concurrently.
1119    /// - `s1` and `s2` are owned by the automata and not freed separately;
1120    ///   `xmlFreeAutomata(am)` runs exactly once at the end.
1121    #[test]
1122    fn test_new_state_adds_to_list() {
1123        unsafe {
1124            let am = xmlNewAutomata();
1125            let s1 = xmlAutomataNewState(am);
1126            let s2 = xmlAutomataNewState(am);
1127            assert!(!s1.is_null());
1128            assert!(!s2.is_null());
1129            assert_ne!(s1, s2);
1130            assert_eq!((*am).states.len(), 2);
1131            xmlFreeAutomata(am);
1132        }
1133    }
1134
1135    /// Test that a two-transition chain compiles to a caller-owned regexp.
1136    ///
1137    /// # Safety
1138    ///
1139    /// - `am`, `s1`, `s2`, `s3` must be valid non-NULL automata-owned
1140    ///   pointers; `token_a` and `token_b` must point to valid NUL-terminated
1141    ///   strings valid for their respective calls.
1142    /// - `xmlAutomataCompile` returns a caller-owned regexp here: it must be
1143    ///   freed exactly once with `xmlRegFreeRegexp` before `xmlFreeAutomata`
1144    ///   releases the automata.
1145    #[test]
1146    fn test_compile_simple_chain() {
1147        unsafe {
1148            let am = xmlNewAutomata();
1149            let s1 = xmlAutomataNewState(am);
1150            let s2 = xmlAutomataNewState(am);
1151            let s3 = xmlAutomataNewState(am);
1152            let token_a = c"a".as_ptr() as *const core::ffi::c_char;
1153            let token_b = c"b".as_ptr() as *const core::ffi::c_char;
1154            xmlAutomataNewTransition(am, s1, s2, token_a, ptr::null_mut());
1155            xmlAutomataNewTransition(am, s2, s3, token_b, ptr::null_mut());
1156            let result = xmlAutomataCompile(am);
1157            // Caller-owned regexp (upstream 2.15): the chain compiles to a
1158            // non-NULL regexp.
1159            assert!(!result.is_null());
1160            xmlRegFreeRegexp(result);
1161            xmlFreeAutomata(am);
1162        }
1163    }
1164}