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