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