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