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::{
42    xmlRegExecPushString, xmlRegFreeExecCtxt, xmlRegFreeRegexp, xmlRegNewExecCtxt,
43    xmlRegexpCompile, xmlRegexpIsDeterministic, XmlRegexp,
44};
45use core::ffi::c_int;
46use core::ptr;
47
48/// Opaque pointer to an automata state.
49pub type XmlAutomataStatePtr = *mut XmlAutomataState;
50
51/// Opaque pointer to an automata.
52pub type XmlAutomataPtr = *mut XmlAutomata;
53
54/// UPSTREAM-PARITY: Corresponds to `_xmlAutomata` in libxml2.
55#[repr(C)]
56pub struct XmlAutomata {
57    /// Compiled regex, set by xmlAutomataCompile.
58    regexp: Option<Box<XmlRegexp>>,
59    /// List of all states.
60    states: Vec<*mut XmlAutomataState>,
61    /// The initial state.
62    init_state: Option<*mut XmlAutomataState>,
63    /// Last error code.
64    error: c_int,
65}
66
67/// UPSTREAM-PARITY: Corresponds to `_xmlAutomataState` in libxml2.
68#[repr(C)]
69pub struct XmlAutomataState {
70    /// Transitions from this state.
71    transitions: Vec<AutomataTransition>,
72}
73
74/// A transition in the automata.
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#[no_mangle]
108pub unsafe extern "C" fn xmlNewAutomata() -> XmlAutomataPtr {
109    let am = xmlMallocImpl(core::mem::size_of::<XmlAutomata>()) as XmlAutomataPtr;
110    if am.is_null() {
111        return ptr::null_mut();
112    }
113    unsafe {
114        core::ptr::write(&mut (*am).regexp, None as Option<Box<XmlRegexp>>);
115        core::ptr::write(&mut (*am).states, Vec::new());
116        (*am).init_state = None;
117        (*am).error = 0;
118    }
119    am
120}
121
122/// Free an automata.
123///
124/// UPSTREAM-PARITY: `xmlFreeAutomata()`
125#[no_mangle]
126pub unsafe extern "C" fn xmlFreeAutomata(am: XmlAutomataPtr) {
127    if am.is_null() {
128        return;
129    }
130    unsafe {
131        // Free all states
132        for &state in &(*am).states {
133            if !state.is_null() {
134                core::ptr::drop_in_place(&mut (*state).transitions);
135                xmlFreeImpl(state as *mut core::ffi::c_void);
136            }
137        }
138        // Drop the states Vec
139        core::ptr::drop_in_place(&mut (*am).states);
140        // Drop the compiled regexp if any
141        let _ = (*am).regexp.take();
142        xmlFreeImpl(am as *mut core::ffi::c_void);
143    }
144}
145
146/// Create a new automata state.
147///
148/// UPSTREAM-PARITY: `xmlAutomataNewState()`
149#[no_mangle]
150pub unsafe extern "C" fn xmlAutomataNewState(am: XmlAutomataPtr) -> XmlAutomataStatePtr {
151    if am.is_null() {
152        return ptr::null_mut();
153    }
154    let state = xmlMallocImpl(core::mem::size_of::<XmlAutomataState>()) as XmlAutomataStatePtr;
155    if state.is_null() {
156        return ptr::null_mut();
157    }
158    unsafe {
159        core::ptr::write(&mut (*state).transitions, Vec::new());
160        // Add to the automata's state list
161        (*am).states.push(state);
162        // Set as init state if first
163        if (*am).init_state.is_none() {
164            (*am).init_state = Some(state);
165        }
166    }
167    state
168}
169
170/// Set a state as the final (accepting) state.
171///
172/// UPSTREAM-PARITY: `xmlAutomataSetFinalState()`
173#[no_mangle]
174pub unsafe extern "C" fn xmlAutomataSetFinalState(
175    _am: XmlAutomataPtr,
176    _state: XmlAutomataStatePtr,
177) -> c_int {
178    // In our implementation, final states are determined by the compiled regex.
179    // This is a no-op for the automata builder; final states are handled during
180    // compilation.
181    0
182}
183
184/// Get the initial state of the automata.
185///
186/// UPSTREAM-PARITY: `xmlAutomataGetInitState()`
187#[no_mangle]
188pub unsafe extern "C" fn xmlAutomataGetInitState(am: XmlAutomataPtr) -> XmlAutomataStatePtr {
189    if am.is_null() {
190        return ptr::null_mut();
191    }
192    unsafe { (*am).init_state.unwrap_or(ptr::null_mut()) }
193}
194
195/// Add an epsilon (empty) transition between two states.
196///
197/// UPSTREAM-PARITY: `xmlAutomataNewEpsilon()`
198#[no_mangle]
199pub unsafe extern "C" fn xmlAutomataNewEpsilon(
200    am: XmlAutomataPtr,
201    from: XmlAutomataStatePtr,
202    to: XmlAutomataStatePtr,
203) -> XmlAutomataStatePtr {
204    if am.is_null() || from.is_null() || to.is_null() {
205        return ptr::null_mut();
206    }
207    unsafe {
208        (*from).transitions.push(AutomataTransition {
209            token: None,
210            min: 0,
211            max: 0,
212            to: Some(to),
213            once: false,
214            all: false,
215            epsilon: true,
216            counter: -1,
217            data: ptr::null_mut(),
218        });
219    }
220    from
221}
222
223/// Add a character transition between two states.
224///
225/// UPSTREAM-PARITY: `xmlAutomataNewTransition()`
226#[no_mangle]
227pub unsafe extern "C" fn xmlAutomataNewTransition(
228    am: XmlAutomataPtr,
229    from: XmlAutomataStatePtr,
230    to: XmlAutomataStatePtr,
231    token: *const core::ffi::c_char,
232    _data: *mut core::ffi::c_void,
233) -> XmlAutomataStatePtr {
234    if am.is_null() || from.is_null() || to.is_null() {
235        return ptr::null_mut();
236    }
237    let tok = if token.is_null() {
238        None
239    } else {
240        // Take the first byte of the token string
241        unsafe { Some(*token as u8) }
242    };
243    unsafe {
244        (*from).transitions.push(AutomataTransition {
245            token: tok,
246            min: 0,
247            max: 0,
248            to: Some(to),
249            once: false,
250            all: false,
251            epsilon: false,
252            counter: -1,
253            data: ptr::null_mut(),
254        });
255    }
256    from
257}
258
259/// Add a counted transition (with min/max bounds).
260///
261/// UPSTREAM-PARITY: `xmlAutomataNewCountTrans()`
262#[no_mangle]
263pub unsafe extern "C" fn xmlAutomataNewCountTrans(
264    am: XmlAutomataPtr,
265    from: XmlAutomataStatePtr,
266    to: XmlAutomataStatePtr,
267    token: *const core::ffi::c_char,
268    _data: *mut core::ffi::c_void,
269    min: c_int,
270    max: c_int,
271) -> XmlAutomataStatePtr {
272    if am.is_null() || from.is_null() || to.is_null() {
273        return ptr::null_mut();
274    }
275    let tok = if token.is_null() {
276        None
277    } else {
278        unsafe { Some(*token as u8) }
279    };
280    unsafe {
281        (*from).transitions.push(AutomataTransition {
282            token: tok,
283            min,
284            max,
285            to: Some(to),
286            once: false,
287            all: false,
288            epsilon: false,
289            counter: -1,
290            data: ptr::null_mut(),
291        });
292    }
293    from
294}
295
296/// Add a "once" transition (consumes exactly once within bounds).
297///
298/// UPSTREAM-PARITY: `xmlAutomataNewOnceTrans()`
299#[no_mangle]
300pub unsafe extern "C" fn xmlAutomataNewOnceTrans(
301    am: XmlAutomataPtr,
302    from: XmlAutomataStatePtr,
303    to: XmlAutomataStatePtr,
304    token: *const core::ffi::c_char,
305    _data: *mut core::ffi::c_void,
306    min: c_int,
307    max: c_int,
308) -> XmlAutomataStatePtr {
309    if am.is_null() || from.is_null() || to.is_null() {
310        return ptr::null_mut();
311    }
312    let tok = if token.is_null() {
313        None
314    } else {
315        unsafe { Some(*token as u8) }
316    };
317    unsafe {
318        (*from).transitions.push(AutomataTransition {
319            token: tok,
320            min,
321            max,
322            to: Some(to),
323            once: true,
324            all: false,
325            epsilon: false,
326            counter: -1,
327            data: ptr::null_mut(),
328        });
329    }
330    from
331}
332
333/// Add a transition that matches any character.
334///
335/// UPSTREAM-PARITY: `xmlAutomataNewAllTrans()`
336#[no_mangle]
337pub unsafe extern "C" fn xmlAutomataNewAllTrans(
338    am: XmlAutomataPtr,
339    from: XmlAutomataStatePtr,
340    to: XmlAutomataStatePtr,
341    _lax: c_int,
342) -> XmlAutomataStatePtr {
343    if am.is_null() || from.is_null() || to.is_null() {
344        return ptr::null_mut();
345    }
346    unsafe {
347        (*from).transitions.push(AutomataTransition {
348            token: None,
349            min: 0,
350            max: 0,
351            to: Some(to),
352            once: false,
353            all: true,
354            epsilon: false,
355            counter: -1,
356            data: ptr::null_mut(),
357        });
358    }
359    from
360}
361
362/// Add a transition associated with a counter.
363///
364/// UPSTREAM-PARITY: `xmlAutomataNewCountedTrans()`
365#[no_mangle]
366pub unsafe extern "C" fn xmlAutomataNewCountedTrans(
367    am: XmlAutomataPtr,
368    from: XmlAutomataStatePtr,
369    to: XmlAutomataStatePtr,
370    counter: c_int,
371) -> XmlAutomataStatePtr {
372    if am.is_null() || from.is_null() || to.is_null() {
373        return ptr::null_mut();
374    }
375    unsafe {
376        (*from).transitions.push(AutomataTransition {
377            token: None,
378            min: 0,
379            max: 0,
380            to: Some(to),
381            once: false,
382            all: false,
383            epsilon: false,
384            counter,
385            data: ptr::null_mut(),
386        });
387    }
388    from
389}
390
391/// Add a transition gated by a counter value.
392///
393/// UPSTREAM-PARITY: `xmlAutomataNewCounterTrans()`
394#[no_mangle]
395pub unsafe extern "C" fn xmlAutomataNewCounterTrans(
396    am: XmlAutomataPtr,
397    from: XmlAutomataStatePtr,
398    to: XmlAutomataStatePtr,
399    counter: c_int,
400) -> XmlAutomataStatePtr {
401    if am.is_null() || from.is_null() || to.is_null() {
402        return ptr::null_mut();
403    }
404    unsafe {
405        (*from).transitions.push(AutomataTransition {
406            token: None,
407            min: 0,
408            max: 0,
409            to: Some(to),
410            once: false,
411            all: false,
412            epsilon: false,
413            counter,
414            data: ptr::null_mut(),
415        });
416    }
417    from
418}
419
420/// Create a new counter with min/max bounds.
421///
422/// UPSTREAM-PARITY: `xmlAutomataNewCounter()`
423#[no_mangle]
424pub unsafe extern "C" fn xmlAutomataNewCounter(
425    _am: XmlAutomataPtr,
426    _min: c_int,
427    _max: c_int,
428) -> c_int {
429    // Counters are tracked by the automata; return a simple counter ID.
430    // In our simplified implementation, return 0 to indicate the first counter.
431    0
432}
433
434/// Compile the automata into a regex.
435///
436/// UPSTREAM-PARITY: `xmlAutomataCompile()`
437///
438/// This builds a regex pattern string from the automata's state machine and
439/// compiles it using the regex engine.
440#[no_mangle]
441pub unsafe extern "C" fn xmlAutomataCompile(am: XmlAutomataPtr) -> c_int {
442    if am.is_null() {
443        return -1;
444    }
445    unsafe {
446        // Build a regex pattern from the automata transitions.
447        // This is a simplified implementation that handles linear chains
448        // of character transitions.
449        let mut pattern = Vec::new();
450        let init = match (*am).init_state {
451            Some(s) => s,
452            None => return 0, // Empty automata — nothing to compile
453        };
454
455        // Walk the state machine to build a pattern.
456        // For now, build a simple pattern from the transition chain.
457        if build_pattern_from_automata(&*am, init, &mut pattern).is_err() {
458            (*am).error = -1;
459            return -1;
460        }
461
462        if pattern.is_empty() {
463            return 0;
464        }
465
466        // Compile the pattern
467        pattern.push(0); // null-terminate
468        let compiled = xmlRegexpCompile(pattern.as_ptr());
469        if compiled.is_null() {
470            (*am).error = -1;
471            return -1;
472        }
473
474        (*am).regexp = Some(Box::from_raw(compiled));
475        0
476    }
477}
478
479/// Build a regex pattern string from the automata state machine.
480///
481/// This walks the states starting from `state` and emits regex tokens
482/// for each transition.
483unsafe fn build_pattern_from_automata(
484    am: &XmlAutomata,
485    state: XmlAutomataStatePtr,
486    pattern: &mut Vec<u8>,
487) -> Result<(), ()> {
488    if state.is_null() {
489        return Ok(());
490    }
491
492    let transitions = &(*state).transitions;
493    if transitions.is_empty() {
494        return Ok(());
495    }
496
497    if transitions.len() == 1 {
498        let t = &transitions[0];
499        if t.epsilon {
500            // Follow epsilon transition
501            if let Some(to) = t.to {
502                return build_pattern_from_automata(am, to, pattern);
503            }
504        } else if t.all {
505            pattern.push(b'.');
506            if let Some(to) = t.to {
507                return build_pattern_from_automata(am, to, pattern);
508            }
509        } else if let Some(tok) = t.token {
510            pattern.push(tok);
511            if let Some(to) = t.to {
512                return build_pattern_from_automata(am, to, pattern);
513            }
514        }
515    } else {
516        // Multiple transitions — this is an alternation
517        pattern.push(b'(');
518        for (i, t) in transitions.iter().enumerate() {
519            if i > 0 {
520                pattern.push(b'|');
521            }
522            if let Some(tok) = t.token {
523                pattern.push(tok);
524            } else if t.all {
525                pattern.push(b'.');
526            }
527            if let Some(to) = t.to {
528                // Check if target has further transitions
529                if !(*to).transitions.is_empty() {
530                    // Follow the chain
531                    let mut sub = Vec::new();
532                    let _ = build_pattern_from_automata(am, to, &mut sub);
533                    pattern.extend(sub);
534                }
535            }
536        }
537        pattern.push(b')');
538    }
539
540    Ok(())
541}
542
543/// Check if the compiled automata is deterministic.
544///
545/// UPSTREAM-PARITY: `xmlAutomataIsDeterministic()`
546#[no_mangle]
547pub unsafe extern "C" fn xmlAutomataIsDeterministic(am: XmlAutomataPtr) -> c_int {
548    if am.is_null() {
549        return 0;
550    }
551    unsafe {
552        match &(*am).regexp {
553            Some(regexp) => xmlRegexpIsDeterministic(&**regexp as *const XmlRegexp),
554            None => 1, // Not compiled yet — assume deterministic
555        }
556    }
557}
558
559// ═══════════════════════════════════════════════════════════════════════════════
560// Tests
561// ═══════════════════════════════════════════════════════════════════════════════
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use core::ptr;
567
568    #[test]
569    fn test_new_automata() {
570        unsafe {
571            let am = xmlNewAutomata();
572            assert!(!am.is_null());
573            xmlFreeAutomata(am);
574        }
575    }
576
577    #[test]
578    fn test_new_automata_null_safety() {
579        unsafe {
580            xmlFreeAutomata(ptr::null_mut());
581            assert!(xmlAutomataGetInitState(ptr::null_mut()).is_null());
582            assert_eq!(xmlAutomataCompile(ptr::null_mut()), -1);
583        }
584    }
585
586    #[test]
587    fn test_new_state() {
588        unsafe {
589            let am = xmlNewAutomata();
590            let state = xmlAutomataNewState(am);
591            assert!(!state.is_null());
592            let init = xmlAutomataGetInitState(am);
593            assert_eq!(init, state);
594            xmlFreeAutomata(am);
595        }
596    }
597
598    #[test]
599    fn test_epsilon_transition() {
600        unsafe {
601            let am = xmlNewAutomata();
602            let s1 = xmlAutomataNewState(am);
603            let s2 = xmlAutomataNewState(am);
604            let result = xmlAutomataNewEpsilon(am, s1, s2);
605            assert!(!result.is_null());
606            assert_eq!(result, s1);
607            xmlFreeAutomata(am);
608        }
609    }
610
611    #[test]
612    fn test_char_transition() {
613        unsafe {
614            let am = xmlNewAutomata();
615            let s1 = xmlAutomataNewState(am);
616            let s2 = xmlAutomataNewState(am);
617            let token = b"a\0".as_ptr() as *const core::ffi::c_char;
618            let result = xmlAutomataNewTransition(am, s1, s2, token, ptr::null_mut());
619            assert!(!result.is_null());
620            assert_eq!(result, s1);
621            xmlFreeAutomata(am);
622        }
623    }
624
625    #[test]
626    fn test_count_transition() {
627        unsafe {
628            let am = xmlNewAutomata();
629            let s1 = xmlAutomataNewState(am);
630            let s2 = xmlAutomataNewState(am);
631            let token = b"a\0".as_ptr() as *const core::ffi::c_char;
632            let result = xmlAutomataNewCountTrans(am, s1, s2, token, ptr::null_mut(), 1, 5);
633            assert!(!result.is_null());
634            xmlFreeAutomata(am);
635        }
636    }
637
638    #[test]
639    fn test_all_transition() {
640        unsafe {
641            let am = xmlNewAutomata();
642            let s1 = xmlAutomataNewState(am);
643            let s2 = xmlAutomataNewState(am);
644            let result = xmlAutomataNewAllTrans(am, s1, s2, 0);
645            assert!(!result.is_null());
646            xmlFreeAutomata(am);
647        }
648    }
649
650    #[test]
651    fn test_once_transition() {
652        unsafe {
653            let am = xmlNewAutomata();
654            let s1 = xmlAutomataNewState(am);
655            let s2 = xmlAutomataNewState(am);
656            let token = b"x\0".as_ptr() as *const core::ffi::c_char;
657            let result = xmlAutomataNewOnceTrans(am, s1, s2, token, ptr::null_mut(), 0, 1);
658            assert!(!result.is_null());
659            xmlFreeAutomata(am);
660        }
661    }
662
663    #[test]
664    fn test_counter_transition() {
665        unsafe {
666            let am = xmlNewAutomata();
667            let s1 = xmlAutomataNewState(am);
668            let s2 = xmlAutomataNewState(am);
669            let cid = xmlAutomataNewCounter(am, 0, 10);
670            let r1 = xmlAutomataNewCountedTrans(am, s1, s2, cid);
671            assert!(!r1.is_null());
672            let r2 = xmlAutomataNewCounterTrans(am, s2, s1, cid);
673            assert!(!r2.is_null());
674            xmlFreeAutomata(am);
675        }
676    }
677
678    #[test]
679    fn test_compile_empty() {
680        unsafe {
681            let am = xmlNewAutomata();
682            let result = xmlAutomataCompile(am);
683            assert_eq!(result, 0);
684            xmlFreeAutomata(am);
685        }
686    }
687
688    #[test]
689    fn test_set_final_state() {
690        unsafe {
691            let am = xmlNewAutomata();
692            let state = xmlAutomataNewState(am);
693            let result = xmlAutomataSetFinalState(am, state);
694            assert_eq!(result, 0);
695            xmlFreeAutomata(am);
696        }
697    }
698
699    #[test]
700    fn test_is_deterministic_not_compiled() {
701        unsafe {
702            let am = xmlNewAutomata();
703            // Before compilation, should return 1 (assumed deterministic)
704            assert_eq!(xmlAutomataIsDeterministic(am), 1);
705            xmlFreeAutomata(am);
706        }
707    }
708
709    #[test]
710    fn test_null_automata_returns_null_state() {
711        unsafe {
712            let state = xmlAutomataNewState(ptr::null_mut());
713            assert!(state.is_null());
714        }
715    }
716
717    #[test]
718    fn test_null_automata_returns_null_init() {
719        unsafe {
720            assert!(xmlAutomataGetInitState(ptr::null_mut()).is_null());
721        }
722    }
723
724    #[test]
725    fn test_new_state_adds_to_list() {
726        unsafe {
727            let am = xmlNewAutomata();
728            let s1 = xmlAutomataNewState(am);
729            let s2 = xmlAutomataNewState(am);
730            assert!(!s1.is_null());
731            assert!(!s2.is_null());
732            assert_ne!(s1, s2);
733            assert_eq!((*am).states.len(), 2);
734            xmlFreeAutomata(am);
735        }
736    }
737
738    #[test]
739    fn test_compile_simple_chain() {
740        unsafe {
741            let am = xmlNewAutomata();
742            let s1 = xmlAutomataNewState(am);
743            let s2 = xmlAutomataNewState(am);
744            let s3 = xmlAutomataNewState(am);
745            let token_a = b"a\0".as_ptr() as *const core::ffi::c_char;
746            let token_b = b"b\0".as_ptr() as *const core::ffi::c_char;
747            xmlAutomataNewTransition(am, s1, s2, token_a, ptr::null_mut());
748            xmlAutomataNewTransition(am, s2, s3, token_b, ptr::null_mut());
749            let result = xmlAutomataCompile(am);
750            assert_eq!(result, 0);
751            xmlFreeAutomata(am);
752        }
753    }
754}