Skip to main content

libxml_rs/xml/relaxng/
mod.rs

1//! RELAX NG implementation (§27, §85 Phase 6).
2//!
3//! RELAX NG schema validation. Upstream libxml2 RELAX NG support is known to be
4//! incomplete/non-conformant in places — parity follows the oracle.
5//!
6//! Phase 6: Complete — RELAX NG grammar parsing, document validation,
7//! and C ABI exports are implemented.
8//!
9//! # UPSTREAM-PARITY
10//!
11//! This module implements a functional RELAX NG validator following the
12//! upstream libxml2 observable behavior. The implementation covers:
13//!
14//! - Full XML syntax for RELAX NG grammar definitions
15//! - Pattern-based validation of XML documents
16//! - Name classes for element/attribute matching
17//! - Basic datatype checking for `<data>` and `<value>` patterns
18//! - Reference resolution for `<ref>` and `<define>` patterns
19//! - Include support for external grammars (basic)
20//! - Compact syntax (basic support)
21//!
22//! Deviations from the OASIS RELAX NG specification that match the upstream
23//! libxml2 behavior are intentional.
24//!
25//! # Upstream contract
26//!
27//! Mirrors upstream relaxng.c (SRC-LIBXML2-2.15.0-RELAXNG-C, oracle tree
28//! `oracle/historical/src/libxml2-2.15.0/relaxng.c`): xmlRelaxNG parse/valid
29//! contexts, pattern compilation, name classes and the datatype hooks. Parity
30//! target: the system libxml2 2.15.3 oracle — NOT the OASIS RELAX NG
31//! specification where the two differ.
32//!
33//! # Conceptual behavior
34//!
35//! RELAX NG schema validation: grammar parsing, pattern-based document
36//! validation, name classes for element/attribute matching, datatype checks
37//! for data/value patterns, ref/define resolution, include support and basic
38//! compact syntax. Upstream libxml2 RELAX NG support is known to be
39//! incomplete/non-conformant in places; the module reproduces the oracle
40//! behavior, not the standard.
41//!
42//! # Ownership & safety invariants
43//!
44//! Ownership: schemas own their pattern tree (xmlRelaxNGFree); parser and
45//! valid contexts own their state (xmlRelaxNGFreeParserCtxt /
46//! xmlRelaxNGFreeValidCtxt); the validated document is borrowed. SAFETY:
47//! pattern references are resolved with cycle guards so recursive grammars
48//! cannot loop the validator.
49//!
50//! # Historical quirks & epochs
51//!
52//! RELAX NG landed in the 2.6 validation-era expansion (2003-2004,
53//! atlas/HISTORY.md 1.5); xmlRelaxNGInitTypes is empty after lazy init and
54//! xmlRelaxNGCleanupTypes is a documented no-op (R-000138 no-op set, matching
55//! upstream empty bodies).
56//!
57//! # Deliberate oddities
58//!
59//! Deliberate oddities: deviations from the OASIS specification that match
60//! upstream libxml2 behavior are intentional and kept; the C-API surface
61//! (xmlRelaxNGNewParserCtxt, xmlRelaxNGNewMemParserCtxt, xmlRelaxNGFree,
62//! xmlRelaxNGNewValidCtxt, ...) is exported with upstream signatures.
63//!
64//! # Proving courts
65//!
66//! RELAXNG court family; the data-ABI header-compile court compiles
67//! every relaxng.h declaration against the DSO; dso-loader 25/25; `cargo test
68//! --lib` exercises the validator.
69//!
70//! # Tempting simplifications that would break parity
71//!
72//! The tempting simplification is implementing full OASIS RELAX NG
73//! conformance — it would diverge from upstream libxml2 known non-conformance
74//! and break byte-identical validation output. Do not drop the compact-syntax
75//! handling: upstream accepts it and the oracle surface includes it.
76
77#![allow(
78    missing_docs,
79    non_snake_case,
80    non_camel_case_types,
81    non_upper_case_globals
82)]
83
84use core::ffi::c_void;
85use core::ptr;
86use std::os::raw::{c_char, c_int};
87
88use crate::abi::structs::*;
89use crate::abi::types::xmlElementType::*;
90
91// ═══════════════════════════════════════════════════════════════════════════════
92// RELAX NG Name Class
93// ═══════════════════════════════════════════════════════════════════════════════
94
95/// RELAX NG name class — determines which element/attribute names a pattern matches.
96///
97/// # UPSTREAM-PARITY
98///
99/// libxml2 defines name classes in `include/relaxng.h` as part of the
100/// pattern structure. This enum provides the same semantics.
101#[derive(Debug, Clone, PartialEq)]
102pub enum RelaxNgNameClass {
103    /// Match a specific name (`<name>`)
104    Name(String),
105    /// Match any name (`<anyName>`)
106    AnyName,
107    /// Match any name in a namespace (`<nsName>`)
108    NsName(String),
109    /// Choice between multiple name classes (`<choice>`)
110    Choice(Vec<RelaxNgNameClass>),
111    /// Exclude a name class (used with anyName/nsName)
112    Except(Box<RelaxNgNameClass>, Box<RelaxNgNameClass>),
113}
114
115impl RelaxNgNameClass {
116    /// Check if a given qualified name matches this name class.
117    pub fn matches(&self, name: &str, ns_uri: Option<&str>) -> bool {
118        match self {
119            RelaxNgNameClass::Name(n) => name == n.as_str(),
120            RelaxNgNameClass::AnyName => true,
121            RelaxNgNameClass::NsName(ns) => {
122                if let Some(uri) = ns_uri {
123                    uri == ns.as_str()
124                } else {
125                    false
126                }
127            }
128            RelaxNgNameClass::Choice(choices) => choices.iter().any(|c| c.matches(name, ns_uri)),
129            RelaxNgNameClass::Except(positive, negative) => {
130                positive.matches(name, ns_uri) && !negative.matches(name, ns_uri)
131            }
132        }
133    }
134}
135
136// ═══════════════════════════════════════════════════════════════════════════════
137// RELAX NG Pattern Types
138// ═══════════════════════════════════════════════════════════════════════════════
139
140/// RELAX NG pattern type — classification for pattern dispatch.
141///
142/// # UPSTREAM-PARITY
143///
144/// Mirrors libxml2's internal pattern type classification.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum RelaxNgPatternType {
147    Element,
148    Attribute,
149    Text,
150    Choice,
151    Sequence,
152    Interleave,
153    ZeroOrMore,
154    OneOrMore,
155    Optional,
156    List,
157    Group,
158    Data,
159    Value,
160    Ref,
161    Define,
162    Grammar,
163    NotAllowed,
164    Empty,
165    ExternalRef,
166    Include,
167    Start,
168}
169
170/// A RELAX NG pattern — the core building block of RELAX NG schemas.
171///
172/// Patterns form a tree structure where composite patterns contain child patterns.
173#[derive(Debug, Clone)]
174pub struct RelaxNgPattern {
175    /// The type of this pattern.
176    pub pattern_type: RelaxNgPatternType,
177    /// Name class (for element/attribute patterns).
178    pub name_class: Option<RelaxNgNameClass>,
179    /// Child patterns.
180    pub children: Vec<RelaxNgPattern>,
181    /// The name for `<ref>` and `<define>` patterns.
182    pub name: Option<String>,
183    /// The namespace URI for nsName or element/attribute.
184    pub ns: Option<String>,
185    /// Datatype for `<data>` patterns.
186    pub datatype: Option<String>,
187    /// Value for `<value>` patterns.
188    pub value: Option<String>,
189    /// Datatype library (e.g., "" for built-in).
190    pub datatype_library: Option<String>,
191}
192
193impl RelaxNgPattern {
194    /// Create a new pattern with the given type.
195    pub const fn new(pattern_type: RelaxNgPatternType) -> Self {
196        Self {
197            pattern_type,
198            name_class: None,
199            children: Vec::new(),
200            name: None,
201            ns: None,
202            datatype: None,
203            value: None,
204            datatype_library: None,
205        }
206    }
207
208    /// Create a simple element pattern with a name.
209    pub fn element(name: &str) -> Self {
210        let mut p = Self::new(RelaxNgPatternType::Element);
211        p.name_class = Some(RelaxNgNameClass::Name(name.to_string()));
212        p
213    }
214
215    /// Create a simple attribute pattern with a name.
216    pub fn attribute(name: &str) -> Self {
217        let mut p = Self::new(RelaxNgPatternType::Attribute);
218        p.name_class = Some(RelaxNgNameClass::Name(name.to_string()));
219        p
220    }
221
222    /// Create a text pattern.
223    pub const fn text() -> Self {
224        Self::new(RelaxNgPatternType::Text)
225    }
226
227    /// Create an empty pattern.
228    pub const fn empty() -> Self {
229        Self::new(RelaxNgPatternType::Empty)
230    }
231
232    /// Create a notAllowed pattern.
233    pub const fn not_allowed() -> Self {
234        Self::new(RelaxNgPatternType::NotAllowed)
235    }
236}
237
238// ═══════════════════════════════════════════════════════════════════════════════
239// RELAX NG Define (named pattern definition)
240// ═══════════════════════════════════════════════════════════════════════════════
241
242/// A named pattern definition (`<define>`).
243#[derive(Debug, Clone)]
244pub struct RelaxNgDefine {
245    /// The name of this definition.
246    pub name: String,
247    /// The pattern body.
248    pub pattern: RelaxNgPattern,
249}
250
251// ═══════════════════════════════════════════════════════════════════════════════
252// RELAX NG Grammar
253// ═══════════════════════════════════════════════════════════════════════════════
254
255/// A RELAX NG grammar — the top-level container for pattern definitions.
256///
257/// Contains named pattern definitions and an optional start pattern.
258#[derive(Debug, Clone)]
259pub struct RelaxNgGrammar {
260    /// Named pattern definitions (`<define>` elements).
261    pub defines: Vec<RelaxNgDefine>,
262    /// The start pattern (`<start>`).
263    pub start: Option<RelaxNgPattern>,
264    /// Included grammars (from `<include>`).
265    pub includes: Vec<RelaxNgGrammar>,
266}
267
268impl RelaxNgGrammar {
269    pub const fn new() -> Self {
270        Self {
271            defines: Vec::new(),
272            start: None,
273            includes: Vec::new(),
274        }
275    }
276
277    /// Look up a named pattern definition.
278    pub fn lookup(&self, name: &str) -> Option<&RelaxNgPattern> {
279        for def in &self.defines {
280            if def.name == name {
281                return Some(&def.pattern);
282            }
283        }
284        // Also search includes
285        for inc in &self.includes {
286            if let Some(p) = inc.lookup(name) {
287                return Some(p);
288            }
289        }
290        None
291    }
292}
293
294impl Default for RelaxNgGrammar {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300// ═══════════════════════════════════════════════════════════════════════════════
301// RELAX NG Schema
302// ═══════════════════════════════════════════════════════════════════════════════
303
304/// A compiled RELAX NG schema.
305///
306/// Wraps a grammar and tracks any errors encountered during parsing.
307#[derive(Debug, Clone)]
308pub struct RelaxNgSchema {
309    /// The top-level grammar.
310    pub grammar: RelaxNgGrammar,
311    /// Errors encountered during parsing.
312    pub errors: Vec<String>,
313    /// Whether parsing hit an unrecoverable grammar error. Upstream
314    /// `xmlRelaxNGParse` aborts (returns NULL after reporting the recorded
315    /// diagnostics through the parser error callbacks) when a pattern is
316    /// structurally invalid — e.g. `xmlRelaxNGParseElement` on an `<element>`
317    /// with no content — while plain warnings keep the schema usable.
318    pub fatal: bool,
319}
320
321impl RelaxNgSchema {
322    pub const fn new() -> Self {
323        Self {
324            grammar: RelaxNgGrammar::new(),
325            errors: Vec::new(),
326            fatal: false,
327        }
328    }
329}
330
331impl Default for RelaxNgSchema {
332    fn default() -> Self {
333        Self::new()
334    }
335}
336
337// ═══════════════════════════════════════════════════════════════════════════════
338// RELAX NG Validation Context
339// ═══════════════════════════════════════════════════════════════════════════════
340
341/// Validation context for RELAX NG schema validation.
342///
343/// Tracks errors, current element path, and pattern recursion depth
344/// during validation of an XML document against a RELAX NG schema.
345/// Mirrors libxml2's `xmlRelaxNGValidCtxt`.
346#[derive(Debug)]
347pub struct RelaxNgValidCtxt {
348    /// The schema being validated against.
349    pub schema: Option<RelaxNgSchema>,
350    /// Accumulated validation errors.
351    pub errors: Vec<String>,
352    /// Number of validation errors.
353    pub nb_errors: i32,
354    /// Current element path (stack of element names).
355    pub path: Vec<String>,
356    /// Maximum recursion depth for pattern matching.
357    pub depth_max: i32,
358    /// Current recursion depth.
359    pub depth: i32,
360}
361
362impl RelaxNgValidCtxt {
363    pub const fn new() -> Self {
364        Self {
365            schema: None,
366            errors: Vec::new(),
367            nb_errors: 0,
368            path: Vec::new(),
369            depth_max: 256,
370            depth: 0,
371        }
372    }
373
374    /// Record a validation error.
375    pub fn record_error(&mut self, msg: String) {
376        self.errors.push(msg);
377        self.nb_errors += 1;
378    }
379
380    /// Get the current path as a string (e.g., "/root/child").
381    pub fn current_path(&self) -> String {
382        if self.path.is_empty() {
383            "/".to_string()
384        } else {
385            format!("/{}", self.path.join("/"))
386        }
387    }
388}
389
390impl Default for RelaxNgValidCtxt {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396// ═══════════════════════════════════════════════════════════════════════════════
397// Internal helpers for RELAX NG parsing
398// ═══════════════════════════════════════════════════════════════════════════════
399
400/// Get the local name of an element node (strip namespace prefix).
401///
402/// # SAFETY
403///
404/// - `node` must be a valid pointer to an _xmlNode or NULL.
405unsafe fn get_local_name(node: *mut _xmlNode) -> String {
406    if node.is_null() {
407        return String::new();
408    }
409    unsafe {
410        let name = (*node).name;
411        if name.is_null() {
412            return String::new();
413        }
414        let mut len = 0;
415        while *name.add(len) != 0 {
416            len += 1;
417        }
418        let slice = std::slice::from_raw_parts(name, len);
419        if let Ok(s) = std::str::from_utf8(slice) {
420            if let Some(pos) = s.find(':') {
421                s[pos + 1..].to_string()
422            } else {
423                s.to_string()
424            }
425        } else {
426            String::new()
427        }
428    }
429}
430
431/// Get the text content of an xmlNode (recursively collects text children).
432///
433/// # SAFETY
434///
435/// - `node` must be a valid pointer to an _xmlNode or NULL.
436unsafe fn get_node_text(node: *mut _xmlNode) -> String {
437    if node.is_null() {
438        return String::new();
439    }
440    let mut result = String::new();
441    unsafe {
442        let mut child = (*node).children;
443        while !child.is_null() {
444            if ((*child).type_ == XML_TEXT_NODE as c_int
445                || (*child).type_ == XML_CDATA_SECTION_NODE as c_int)
446                && !(*child).content.is_null()
447            {
448                let content = (*child).content;
449                let mut len = 0;
450                while *content.add(len) != 0 {
451                    len += 1;
452                }
453                let slice = std::slice::from_raw_parts(content, len);
454                result.push_str(&String::from_utf8_lossy(slice));
455            }
456            child = (*child).next;
457        }
458    }
459    result
460}
461
462/// Get the qualified name of a node (with namespace prefix if available).
463///
464/// # SAFETY
465///
466/// - `node` must be a valid pointer to an _xmlNode or NULL.
467unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
468    if node.is_null() {
469        return String::new();
470    }
471    unsafe {
472        let ns = (*node).ns;
473        let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
474            let mut len = 0;
475            while *(*ns).prefix.add(len) != 0 {
476                len += 1;
477            }
478            let slice = std::slice::from_raw_parts((*ns).prefix, len);
479            if let Ok(s) = std::str::from_utf8(slice) {
480                format!("{}:", s)
481            } else {
482                String::new()
483            }
484        } else {
485            String::new()
486        };
487
488        let name = (*node).name;
489        if name.is_null() {
490            return String::new();
491        }
492        let mut len = 0;
493        while *name.add(len) != 0 {
494            len += 1;
495        }
496        let slice = std::slice::from_raw_parts(name, len);
497        if let Ok(s) = std::str::from_utf8(slice) {
498            format!("{}{}", prefix, s)
499        } else {
500            String::new()
501        }
502    }
503}
504
505/// Get the namespace URI of a node.
506///
507/// # SAFETY
508///
509/// - `node` must be a valid pointer to an _xmlNode or NULL.
510unsafe fn get_node_ns_uri(node: *mut _xmlNode) -> Option<String> {
511    if node.is_null() {
512        return None;
513    }
514    unsafe {
515        let ns = (*node).ns;
516        if ns.is_null() || (*ns).href.is_null() {
517            return None;
518        }
519        let href = (*ns).href;
520        let mut len = 0;
521        while *href.add(len) != 0 {
522            len += 1;
523        }
524        let slice = std::slice::from_raw_parts(href, len);
525        if let Ok(s) = std::str::from_utf8(slice) {
526            Some(s.to_string())
527        } else {
528            None
529        }
530    }
531}
532
533/// Get an attribute value from an xmlNode.
534///
535/// # SAFETY
536///
537/// - `node` must be a valid pointer to an _xmlNode or NULL.
538unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
539    if node.is_null() {
540        return None;
541    }
542    unsafe {
543        let mut prop = (*node).properties;
544        while !prop.is_null() {
545            let prop_name = (*prop).name;
546            if !prop_name.is_null() {
547                let mut len = 0;
548                while *prop_name.add(len) != 0 {
549                    len += 1;
550                }
551                let slice = std::slice::from_raw_parts(prop_name, len);
552                if let Ok(s) = std::str::from_utf8(slice) {
553                    if s == name {
554                        return Some(get_node_text(prop as *mut _xmlNode));
555                    }
556                }
557            }
558            prop = (*prop).next;
559        }
560    }
561    None
562}
563
564/// Check if an xmlNode is an element with a given local name.
565///
566/// # SAFETY
567///
568/// - `node` must be a valid pointer to an _xmlNode or NULL.
569#[allow(dead_code)]
570unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
571    if node.is_null() {
572        return false;
573    }
574    unsafe {
575        let name = (*node).name;
576        if name.is_null() {
577            return false;
578        }
579        let mut len = 0;
580        while *name.add(len) != 0 {
581            len += 1;
582        }
583        let slice = std::slice::from_raw_parts(name, len);
584        if let Ok(s) = std::str::from_utf8(slice) {
585            let local = if let Some(pos) = s.find(':') {
586                &s[pos + 1..]
587            } else {
588                s
589            };
590            return local == local_name;
591        }
592    }
593    false
594}
595
596// ═══════════════════════════════════════════════════════════════════════════════
597// RELAX NG Schema Parsing
598// ═══════════════════════════════════════════════════════════════════════════════
599
600/// Parse a RELAX NG schema from an XML string.
601///
602/// # UPSTREAM-PARITY
603///
604/// Equivalent to `xmlRelaxNGParse` in libxml2 when given a parser context
605/// created from a memory buffer.
606///
607/// # Safety
608///
609/// - `xml_doc` must be a valid `&str`; its bytes stay readable for the
610///   duration of the `xmlReadMemory` call.
611/// - The document pointer returned by `xmlReadMemory` is NULL-checked before
612///   being passed to `rng_parse_doc`, and is freed exactly once with
613///   `xmlFreeDoc`.
614///
615/// Returns the parsed schema, or an error message on failure.
616pub fn rng_parse(xml_doc: &str) -> Result<RelaxNgSchema, String> {
617    let doc_ptr = unsafe {
618        crate::abi::exports_xml2::xmlReadMemory(
619            xml_doc.as_ptr() as *const c_char,
620            xml_doc.len() as c_int,
621            c"schema.rng".as_ptr() as *const c_char,
622            ptr::null(),
623            0,
624        )
625    };
626
627    if doc_ptr.is_null() {
628        return Err("Failed to parse RELAX NG schema XML document".to_string());
629    }
630
631    let result = unsafe { rng_parse_doc(doc_ptr) };
632    unsafe {
633        crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
634    }
635    result
636}
637
638/// Get the URL of a parsed document (its `URL` field), as a string.
639///
640/// # SAFETY
641///
642/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
643unsafe fn doc_url(doc: *mut _xmlDoc) -> Option<String> {
644    if doc.is_null() {
645        return None;
646    }
647    unsafe {
648        let url = (*doc).URL;
649        if url.is_null() {
650            return None;
651        }
652        let mut len = 0;
653        while *url.add(len) != 0 {
654            len += 1;
655        }
656        let slice = std::slice::from_raw_parts(url, len);
657        Some(String::from_utf8_lossy(slice).to_string())
658    }
659}
660
661/// Resolve a (possibly relative) RELAX NG `href` against the base URL of the
662/// grammar that references it. Mirrors upstream relaxng.c (include /
663/// externalRef handling): `base = xmlNodeGetBase(cur->doc, cur);
664/// URL = xmlBuildURI(href, base);` — a bare relative name resolves against
665/// the including schema's own location, not the process CWD.
666///
667/// # SAFETY
668///
669/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
670unsafe fn resolve_rng_href(doc: *mut _xmlDoc, href: &str) -> String {
671    let Some(base) = (unsafe { doc_url(doc) }) else {
672        // Memory-parse (or URL-less): keep the raw href (relative names then
673        // resolve against the CWD, matching upstream's NULL base).
674        return href.to_string();
675    };
676    if base.is_empty() {
677        return href.to_string();
678    }
679    let Ok(base_c) = std::ffi::CString::new(base.as_str()) else {
680        return href.to_string();
681    };
682    let Ok(href_c) = std::ffi::CString::new(href) else {
683        return href.to_string();
684    };
685    let resolved = crate::abi::exports_uri::xmlBuildURI(href_c.as_ptr(), base_c.as_ptr());
686    if resolved.is_null() {
687        href.to_string()
688    } else {
689        // xmlBuildURI returns an xmlChar* string (not a doc); convert bytes.
690        unsafe {
691            let mut len = 0;
692            while *resolved.add(len) != 0 {
693                len += 1;
694            }
695            let slice = std::slice::from_raw_parts(resolved, len);
696            let s = String::from_utf8_lossy(slice).to_string();
697            crate::abi::allocator::xmlFreeImpl(resolved as *mut core::ffi::c_void);
698            s
699        }
700    }
701}
702
703/// Parse a RELAX NG schema from a document.
704///
705/// # SAFETY
706///
707/// - `doc` must be a valid pointer to an _xmlDoc representing a RELAX NG schema.
708unsafe fn rng_parse_doc(doc: *mut _xmlDoc) -> Result<RelaxNgSchema, String> {
709    unsafe {
710        let root = (*doc).children;
711        if root.is_null() {
712            return Err("RELAX NG document has no root element".to_string());
713        }
714
715        // Find the root element (skip any non-element nodes like comments)
716        let mut root_elem = root;
717        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
718            root_elem = (*root_elem).next;
719        }
720
721        if root_elem.is_null() {
722            return Err("RELAX NG document has no root element".to_string());
723        }
724
725        let base = unsafe { doc_url(doc) };
726        let local_name = get_local_name(root_elem);
727        let mut schema = RelaxNgSchema::new();
728
729        match local_name.as_str() {
730            "grammar" => {
731                // Top-level grammar
732                schema.grammar = rng_parse_grammar_node(root_elem, &mut schema, base.as_deref());
733                Ok(schema)
734            }
735            "element" | "attribute" | "text" | "choice" | "sequence" | "interleave"
736            | "zeroOrMore" | "oneOrMore" | "optional" | "list" | "group" | "data" | "value"
737            | "ref" | "notAllowed" | "empty" | "externalRef" | "define" | "start" | "include" => {
738                // Single pattern as root (simplified grammar)
739                let pattern = rng_parse_pattern(root_elem, &mut schema);
740                schema.grammar.start = Some(pattern);
741                Ok(schema)
742            }
743            _ => Err(format!("Unknown RELAX NG root element: '{}'", local_name)),
744        }
745    }
746}
747
748/// Parse a `<grammar>` element.
749///
750/// `base` is the URL of the document this grammar node lives in; it anchors
751/// relative `<include href>` resolution (recursively for nested includes).
752///
753/// # SAFETY
754///
755/// - `node` must be a valid pointer to a `<grammar>` element node.
756unsafe fn rng_parse_grammar_node(
757    node: *mut _xmlNode,
758    schema: &mut RelaxNgSchema,
759    base: Option<&str>,
760) -> RelaxNgGrammar {
761    unsafe {
762        let mut grammar = RelaxNgGrammar::new();
763
764        let mut child = (*node).children;
765        while !child.is_null() {
766            if (*child).type_ == XML_ELEMENT_NODE as c_int {
767                let local = get_local_name(child);
768                match local.as_str() {
769                    "define" => {
770                        let def = rng_parse_define(child, schema);
771                        grammar.defines.push(def);
772                    }
773                    "start" => {
774                        grammar.start = Some(rng_parse_pattern(child, schema));
775                    }
776                    "include" => {
777                        if let Some(inc) = rng_parse_include(child, schema, base) {
778                            // UPSTREAM-PARITY (relaxng.c combined grammar): a
779                            // grammar with no <start> of its own inherits the
780                            // start pattern of an included grammar.
781                            if grammar.start.is_none() {
782                                grammar.start = inc.start.clone();
783                            }
784                            grammar.includes.push(inc);
785                        }
786                    }
787                    "div" => {
788                        // <div> is a grouping element; recurse into it
789                        let sub_grammar = rng_parse_grammar_node(child, schema, base);
790                        grammar.defines.extend(sub_grammar.defines);
791                        if sub_grammar.start.is_some() {
792                            grammar.start = sub_grammar.start;
793                        }
794                        grammar.includes.extend(sub_grammar.includes);
795                    }
796                    _ => {
797                        // Unknown element inside grammar — treat as pattern error
798                        schema
799                            .errors
800                            .push(format!("Unexpected element '<{}>' in grammar", local));
801                    }
802                }
803            }
804            child = (*child).next;
805        }
806
807        grammar
808    }
809}
810
811/// Parse a `<define>` element.
812///
813/// # SAFETY
814///
815/// - `node` must be a valid pointer to a `<define>` element node.
816unsafe fn rng_parse_define(node: *mut _xmlNode, schema: &mut RelaxNgSchema) -> RelaxNgDefine {
817    unsafe {
818        let name = get_attr(node, "name").unwrap_or_default();
819        let pattern = rng_parse_pattern(node, schema);
820        RelaxNgDefine { name, pattern }
821    }
822}
823
824/// Parse an `<include>` element.
825///
826/// `base` anchors the relative `href` (the URL of the including grammar's
827/// document). Nested includes inside the loaded grammar resolve against the
828/// loaded document's own URL.
829///
830/// UPSTREAM-PARITY (relaxng.c xmlRelaxNGParseInclude): the returned grammar
831/// is the INCLUDED document's grammar with the `<include>` element's own
832/// children applied — a `<define>` child redefines the same-named definition
833/// of the included grammar (or adds a new one), and a `<start>` child
834/// replaces the included start pattern.
835///
836/// # SAFETY
837///
838/// - `node` must be a valid pointer to an `<include>` element node.
839unsafe fn rng_parse_include(
840    node: *mut _xmlNode,
841    schema: &mut RelaxNgSchema,
842    base: Option<&str>,
843) -> Option<RelaxNgGrammar> {
844    unsafe {
845        let href = get_attr(node, "href");
846        let mut grammar = if let Some(ref url) = href {
847            // UPSTREAM-PARITY (relaxng.c): resolve the href against the
848            // including document's base (xmlBuildURI) so relative includes
849            // resolve next to the schema file, not the process CWD.
850            let resolved = match base {
851                Some(base) => {
852                    let base_c = std::ffi::CString::new(base).ok()?;
853                    let href_c = std::ffi::CString::new(url.clone()).ok()?;
854                    let r = crate::abi::exports_uri::xmlBuildURI(href_c.as_ptr(), base_c.as_ptr());
855                    if r.is_null() {
856                        url.clone()
857                    } else {
858                        let mut len = 0;
859                        while *r.add(len) != 0 {
860                            len += 1;
861                        }
862                        let slice = std::slice::from_raw_parts(r, len);
863                        let s = String::from_utf8_lossy(slice).to_string();
864                        crate::abi::allocator::xmlFreeImpl(r as *mut core::ffi::c_void);
865                        s
866                    }
867                }
868                None => url.clone(),
869            };
870            let url_c = std::ffi::CString::new(resolved.clone()).ok()?;
871            let doc = crate::abi::exports_xml2::xmlParseFile(url_c.as_ptr());
872            if doc.is_null() {
873                return None;
874            }
875            let mut inc_schema = RelaxNgSchema::new();
876            let grammar = rng_parse_grammar_node(
877                {
878                    let mut root = (*doc).children;
879                    while !root.is_null() && (*root).type_ != XML_ELEMENT_NODE as c_int {
880                        root = (*root).next;
881                    }
882                    root
883                },
884                &mut inc_schema,
885                doc_url(doc).as_deref(),
886            );
887            crate::abi::exports_xml2::xmlFreeDoc(doc);
888            grammar
889        } else {
890            // Inline grammar in include
891            let mut inc_schema = RelaxNgSchema::new();
892            rng_parse_grammar_node(node, &mut inc_schema, base)
893        };
894
895        // Apply the <include> element's own children (redefinitions) to the
896        // included grammar. For an href include, `node` is the <include>
897        // element itself; for an inline include, the grammar was parsed FROM
898        // it already, so re-parsing would double it — only the href case
899        // carries both an external grammar and inline redefinitions.
900        if href.is_some() {
901            let mut ovr_schema = RelaxNgSchema::new();
902            let ovr = rng_parse_grammar_node(node, &mut ovr_schema, base);
903            for def in ovr.defines {
904                if let Some(existing) = grammar.defines.iter_mut().find(|d| d.name == def.name) {
905                    *existing = def;
906                } else {
907                    grammar.defines.push(def);
908                }
909            }
910            if ovr.start.is_some() {
911                grammar.start = ovr.start;
912            }
913            grammar.includes.extend(ovr.includes);
914        }
915        Some(grammar)
916    }
917}
918
919/// Parse a pattern from an element node. Dispatches based on element name.
920///
921/// # SAFETY
922///
923/// - `node` must be a valid pointer to an XML element node.
924unsafe fn rng_parse_pattern(node: *mut _xmlNode, schema: &mut RelaxNgSchema) -> RelaxNgPattern {
925    unsafe {
926        let local = get_local_name(node);
927
928        match local.as_str() {
929            "element" => rng_parse_element_pattern(node, schema),
930            "attribute" => rng_parse_attribute_pattern(node, schema),
931            "text" => RelaxNgPattern::text(),
932            "empty" => RelaxNgPattern::empty(),
933            "notAllowed" => RelaxNgPattern::not_allowed(),
934            "choice" => rng_parse_composite_pattern(node, RelaxNgPatternType::Choice, schema),
935            "sequence" => rng_parse_composite_pattern(node, RelaxNgPatternType::Sequence, schema),
936            "interleave" => {
937                rng_parse_composite_pattern(node, RelaxNgPatternType::Interleave, schema)
938            }
939            "zeroOrMore" => rng_parse_unary_pattern(node, RelaxNgPatternType::ZeroOrMore, schema),
940            "oneOrMore" => rng_parse_unary_pattern(node, RelaxNgPatternType::OneOrMore, schema),
941            "optional" => rng_parse_unary_pattern(node, RelaxNgPatternType::Optional, schema),
942            "list" => rng_parse_unary_pattern(node, RelaxNgPatternType::List, schema),
943            "group" => rng_parse_composite_pattern(node, RelaxNgPatternType::Group, schema),
944            "data" => rng_parse_data_pattern(node, schema),
945            "value" => rng_parse_value_pattern(node, schema),
946            "ref" => rng_parse_ref_pattern(node),
947            "externalRef" => rng_parse_external_ref(node, schema),
948            "define" | "start" | "grammar" | "include" | "div" => {
949                // These are grammar-level elements; return the content pattern
950                let mut child = (*node).children;
951                let mut result = RelaxNgPattern::empty();
952                while !child.is_null() {
953                    if (*child).type_ == XML_ELEMENT_NODE as c_int {
954                        result = rng_parse_pattern(child, schema);
955                        break;
956                    }
957                    child = (*child).next;
958                }
959                result
960            }
961            _ => {
962                // Unknown element — treat as empty pattern
963                schema
964                    .errors
965                    .push(format!("Unknown pattern element '<{}>'", local));
966                RelaxNgPattern::empty()
967            }
968        }
969    }
970}
971
972/// Parse an `<element>` pattern.
973///
974/// # SAFETY
975///
976/// - `node` must be a valid pointer to an `<element>` element node.
977unsafe fn rng_parse_element_pattern(
978    node: *mut _xmlNode,
979    schema: &mut RelaxNgSchema,
980) -> RelaxNgPattern {
981    unsafe {
982        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Element);
983
984        // Parse name class from name attribute or child <name>, <anyName>, <nsName>, <choice>
985        let name_attr = get_attr(node, "name");
986        pattern.name = name_attr.clone();
987
988        if let Some(ref n) = name_attr {
989            pattern.name_class = Some(RelaxNgNameClass::Name(n.clone()));
990        }
991
992        // Parse children for name class and content pattern
993        let mut child = (*node).children;
994        let mut has_content = false;
995
996        while !child.is_null() {
997            if (*child).type_ == XML_ELEMENT_NODE as c_int {
998                let child_local = get_local_name(child);
999
1000                match child_local.as_str() {
1001                    "name" => {
1002                        let text = get_node_text(child);
1003                        if !text.is_empty() {
1004                            pattern.name_class =
1005                                Some(RelaxNgNameClass::Name(text.trim().to_string()));
1006                        }
1007                    }
1008                    "anyName" => {
1009                        pattern.name_class = Some(rng_parse_any_name(child));
1010                    }
1011                    "nsName" => {
1012                        pattern.name_class = Some(rng_parse_ns_name(child));
1013                    }
1014                    "choice" if pattern.name_class.is_none() => {
1015                        // Name class choice (only before content)
1016                        pattern.name_class = Some(rng_parse_name_class_choice(child));
1017                    }
1018                    _ => {
1019                        // Content pattern. UPSTREAM-PARITY (relaxng.c
1020                        // xmlRelaxNGParseElement): multiple content children
1021                        // form an implicit group — all of them are kept (the
1022                        // former single-content gate silently dropped
1023                        // every pattern after the first, e.g. the `<optional>`
1024                        // in `<element name="root">a<optional>b</optional>`
1025                        // which then surfaced as a spurious extra element).
1026                        pattern.children.push(rng_parse_pattern(child, schema));
1027                        has_content = true;
1028                    }
1029                }
1030            }
1031            child = (*child).next;
1032        }
1033
1034        // UPSTREAM-PARITY (relaxng.c xmlRelaxNGParseElement): an <element>
1035        // pattern must carry a content pattern; without one parsing aborts
1036        // with this exact diagnostic (DOMDocument_relaxNGValidateSource_error2
1037        // pins "xmlRelaxNGParseElement: element has no content").
1038        if !has_content {
1039            schema
1040                .errors
1041                .push("xmlRelaxNGParseElement: element has no content".to_string());
1042            schema.fatal = true;
1043        }
1044
1045        pattern
1046    }
1047}
1048
1049/// Parse an `<attribute>` pattern.
1050///
1051/// # SAFETY
1052///
1053/// - `node` must be a valid pointer to an `<attribute>` element node.
1054unsafe fn rng_parse_attribute_pattern(
1055    node: *mut _xmlNode,
1056    schema: &mut RelaxNgSchema,
1057) -> RelaxNgPattern {
1058    unsafe {
1059        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Attribute);
1060
1061        // Parse name class
1062        let name_attr = get_attr(node, "name");
1063        pattern.name = name_attr.clone();
1064
1065        if let Some(ref n) = name_attr {
1066            pattern.name_class = Some(RelaxNgNameClass::Name(n.clone()));
1067        }
1068
1069        // Parse children
1070        let mut child = (*node).children;
1071        while !child.is_null() {
1072            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1073                let child_local = get_local_name(child);
1074
1075                match child_local.as_str() {
1076                    "name" => {
1077                        let text = get_node_text(child);
1078                        if !text.is_empty() {
1079                            pattern.name_class =
1080                                Some(RelaxNgNameClass::Name(text.trim().to_string()));
1081                        }
1082                    }
1083                    "anyName" => {
1084                        pattern.name_class = Some(rng_parse_any_name(child));
1085                    }
1086                    "nsName" => {
1087                        pattern.name_class = Some(rng_parse_ns_name(child));
1088                    }
1089                    "choice" if pattern.name_class.is_none() => {
1090                        pattern.name_class = Some(rng_parse_name_class_choice(child));
1091                    }
1092                    _ => {
1093                        // Content pattern (text, data, etc.)
1094                        pattern.children.push(rng_parse_pattern(child, schema));
1095                    }
1096                }
1097            }
1098            child = (*child).next;
1099        }
1100
1101        pattern
1102    }
1103}
1104
1105/// Parse an `<anyName>` element (possibly with `<except>`).
1106///
1107/// # SAFETY
1108///
1109/// - `node` must be a valid pointer to an `<anyName>` element node.
1110unsafe fn rng_parse_any_name(node: *mut _xmlNode) -> RelaxNgNameClass {
1111    unsafe {
1112        // Check for <except> child
1113        let mut child = (*node).children;
1114        while !child.is_null() {
1115            if (*child).type_ == XML_ELEMENT_NODE as c_int && get_local_name(child) == "except" {
1116                let except_nc = rng_parse_name_class_content(child);
1117                return RelaxNgNameClass::Except(
1118                    Box::new(RelaxNgNameClass::AnyName),
1119                    Box::new(except_nc),
1120                );
1121            }
1122            child = (*child).next;
1123        }
1124        RelaxNgNameClass::AnyName
1125    }
1126}
1127
1128/// Parse an `<nsName>` element (possibly with `<except>`).
1129///
1130/// # SAFETY
1131///
1132/// - `node` must be a valid pointer to an `<nsName>` element node.
1133unsafe fn rng_parse_ns_name(node: *mut _xmlNode) -> RelaxNgNameClass {
1134    unsafe {
1135        let ns = get_attr(node, "ns").unwrap_or_default();
1136
1137        // Check for <except> child
1138        let mut child = (*node).children;
1139        while !child.is_null() {
1140            if (*child).type_ == XML_ELEMENT_NODE as c_int && get_local_name(child) == "except" {
1141                let except_nc = rng_parse_name_class_content(child);
1142                return RelaxNgNameClass::Except(
1143                    Box::new(RelaxNgNameClass::NsName(ns)),
1144                    Box::new(except_nc),
1145                );
1146            }
1147            child = (*child).next;
1148        }
1149
1150        RelaxNgNameClass::NsName(ns)
1151    }
1152}
1153
1154/// Parse name class children of a `<choice>` element used as name class.
1155///
1156/// # SAFETY
1157///
1158/// - `node` must be a valid pointer to an element node containing name class children.
1159unsafe fn rng_parse_name_class_choice(node: *mut _xmlNode) -> RelaxNgNameClass {
1160    unsafe {
1161        let mut choices = Vec::new();
1162        let mut child = (*node).children;
1163        while !child.is_null() {
1164            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1165                choices.push(rng_parse_name_class_item(child));
1166            }
1167            child = (*child).next;
1168        }
1169        if choices.len() == 1 {
1170            choices.remove(0)
1171        } else {
1172            RelaxNgNameClass::Choice(choices)
1173        }
1174    }
1175}
1176
1177/// Parse a single name class item from a name class context.
1178///
1179/// # SAFETY
1180///
1181/// - `node` must be a valid pointer to an element node.
1182unsafe fn rng_parse_name_class_item(node: *mut _xmlNode) -> RelaxNgNameClass {
1183    unsafe {
1184        let local = get_local_name(node);
1185        match local.as_str() {
1186            "name" => {
1187                let text = get_node_text(node);
1188                RelaxNgNameClass::Name(text.trim().to_string())
1189            }
1190            "anyName" => rng_parse_any_name(node),
1191            "nsName" => rng_parse_ns_name(node),
1192            "choice" => rng_parse_name_class_choice(node),
1193            _ => {
1194                // Default: treat as name
1195                let text = get_node_text(node);
1196                if text.trim().is_empty() {
1197                    RelaxNgNameClass::AnyName
1198                } else {
1199                    RelaxNgNameClass::Name(text.trim().to_string())
1200                }
1201            }
1202        }
1203    }
1204}
1205
1206/// Parse name class content from an `<except>` or similar element.
1207///
1208/// # SAFETY
1209///
1210/// - `node` must be a valid pointer to an element node.
1211unsafe fn rng_parse_name_class_content(node: *mut _xmlNode) -> RelaxNgNameClass {
1212    unsafe {
1213        let mut names = Vec::new();
1214        let mut child = (*node).children;
1215        while !child.is_null() {
1216            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1217                names.push(rng_parse_name_class_item(child));
1218            }
1219            child = (*child).next;
1220        }
1221        if names.is_empty() {
1222            RelaxNgNameClass::AnyName
1223        } else if names.len() == 1 {
1224            names.remove(0)
1225        } else {
1226            RelaxNgNameClass::Choice(names)
1227        }
1228    }
1229}
1230
1231/// Parse a composite pattern (sequence, choice, interleave, group).
1232///
1233/// # SAFETY
1234///
1235/// - `node` must be a valid pointer to an element node.
1236unsafe fn rng_parse_composite_pattern(
1237    node: *mut _xmlNode,
1238    pattern_type: RelaxNgPatternType,
1239    schema: &mut RelaxNgSchema,
1240) -> RelaxNgPattern {
1241    unsafe {
1242        let mut pattern = RelaxNgPattern::new(pattern_type);
1243
1244        let mut child = (*node).children;
1245        while !child.is_null() {
1246            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1247                pattern.children.push(rng_parse_pattern(child, schema));
1248            }
1249            child = (*child).next;
1250        }
1251
1252        pattern
1253    }
1254}
1255
1256/// Parse a unary pattern (zeroOrMore, oneOrMore, optional, list).
1257///
1258/// # SAFETY
1259///
1260/// - `node` must be a valid pointer to an element node.
1261unsafe fn rng_parse_unary_pattern(
1262    node: *mut _xmlNode,
1263    pattern_type: RelaxNgPatternType,
1264    schema: &mut RelaxNgSchema,
1265) -> RelaxNgPattern {
1266    unsafe {
1267        let mut pattern = RelaxNgPattern::new(pattern_type);
1268
1269        let mut child = (*node).children;
1270        while !child.is_null() {
1271            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1272                pattern.children.push(rng_parse_pattern(child, schema));
1273                // Only take the first child pattern for unary patterns
1274                break;
1275            }
1276            child = (*child).next;
1277        }
1278
1279        pattern
1280    }
1281}
1282
1283/// Parse a `<data>` pattern.
1284///
1285/// # SAFETY
1286///
1287/// - `node` must be a valid pointer to a `<data>` element node.
1288unsafe fn rng_parse_data_pattern(
1289    node: *mut _xmlNode,
1290    _schema: &mut RelaxNgSchema,
1291) -> RelaxNgPattern {
1292    unsafe {
1293        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Data);
1294        pattern.datatype = get_attr(node, "type");
1295        pattern.datatype_library = get_attr(node, "datatypeLibrary");
1296
1297        // For basic support, we store the type and don't process params in detail
1298        pattern
1299    }
1300}
1301
1302/// Parse a `<value>` pattern.
1303///
1304/// # SAFETY
1305///
1306/// - `node` must be a valid pointer to a `<value>` element node.
1307unsafe fn rng_parse_value_pattern(
1308    node: *mut _xmlNode,
1309    _schema: &mut RelaxNgSchema,
1310) -> RelaxNgPattern {
1311    unsafe {
1312        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Value);
1313        pattern.datatype = get_attr(node, "type");
1314        pattern.datatype_library = get_attr(node, "datatypeLibrary");
1315        pattern.value = Some(get_node_text(node).trim().to_string());
1316
1317        pattern
1318    }
1319}
1320
1321/// Parse a `<ref>` pattern.
1322///
1323/// # SAFETY
1324///
1325/// - `node` must be a valid pointer to a `<ref>` element node.
1326unsafe fn rng_parse_ref_pattern(node: *mut _xmlNode) -> RelaxNgPattern {
1327    unsafe {
1328        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::Ref);
1329        pattern.name = get_attr(node, "name");
1330        pattern
1331    }
1332}
1333
1334/// Parse an `<externalRef>` pattern.
1335///
1336/// # SAFETY
1337///
1338/// - `node` must be a valid pointer to an `<externalRef>` element node.
1339unsafe fn rng_parse_external_ref(
1340    node: *mut _xmlNode,
1341    _schema: &mut RelaxNgSchema,
1342) -> RelaxNgPattern {
1343    unsafe {
1344        let mut pattern = RelaxNgPattern::new(RelaxNgPatternType::ExternalRef);
1345        let href = get_attr(node, "href");
1346        pattern.name = href;
1347        pattern
1348    }
1349}
1350
1351// ═══════════════════════════════════════════════════════════════════════════════
1352// RELAX NG Validation Logic
1353// ═══════════════════════════════════════════════════════════════════════════════
1354
1355/// Validate an XML document against a RELAX NG schema.
1356///
1357/// Returns `true` if the document is valid.
1358///
1359/// # SAFETY
1360///
1361/// - `schema` must be a valid reference to a parsed schema.
1362/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
1363/// - `ctxt` must be a valid mutable reference to a validation context.
1364pub unsafe fn rng_validate_doc(
1365    schema: &RelaxNgSchema,
1366    doc: *mut _xmlDoc,
1367    ctxt: &mut RelaxNgValidCtxt,
1368) -> bool {
1369    unsafe {
1370        if doc.is_null() {
1371            ctxt.record_error("Document is null".to_string());
1372            return false;
1373        }
1374
1375        let root = (*doc).children;
1376        if root.is_null() {
1377            ctxt.record_error("Document has no children".to_string());
1378            return false;
1379        }
1380
1381        // Find the root element
1382        let mut root_elem = root;
1383        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
1384            root_elem = (*root_elem).next;
1385        }
1386
1387        if root_elem.is_null() {
1388            ctxt.record_error("Document has no root element".to_string());
1389            return false;
1390        }
1391
1392        // Get the start pattern
1393        let start_pattern = match &schema.grammar.start {
1394            Some(p) => p,
1395            None => {
1396                ctxt.record_error("Schema has no start pattern".to_string());
1397                return false;
1398            }
1399        };
1400
1401        ctxt.path.clear();
1402        let valid = rng_validate_pattern(start_pattern, root_elem, schema, ctxt);
1403
1404        // Check for remaining unmatched errors
1405        valid
1406    }
1407}
1408
1409/// Run RELAX NG validation of a document against an already-compiled schema
1410/// (xmlRelaxNGPtr from xmlRelaxNGParse), collecting the diagnostics WITHOUT
1411/// dispatching them to any registered handler. Used by the xmlTextReader
1412/// (which validates deferred, at first Read, and raises the diagnostics
1413/// through the global libxml error channel itself).
1414///
1415/// # SAFETY
1416///
1417/// - `schema` must be a valid compiled-schema pointer from xmlRelaxNGParse;
1418/// - `doc` must be a valid parsed document.
1419pub(crate) unsafe fn rng_validate_doc_compiled(
1420    schema: *mut c_void,
1421    doc: *mut _xmlDoc,
1422) -> (bool, Vec<String>) {
1423    unsafe {
1424        if schema.is_null() || doc.is_null() {
1425            return (false, Vec::new());
1426        }
1427        let schema_ref = &*(schema as *const RelaxNgSchema);
1428        let mut ctxt = RelaxNgValidCtxt::new();
1429        let valid = rng_validate_doc(schema_ref, doc, &mut ctxt);
1430        (valid, ctxt.errors)
1431    }
1432}
1433
1434/// Validate a pattern against a node.
1435///
1436/// # SAFETY
1437///
1438/// - `node` must be a valid pointer to an _xmlNode or NULL.
1439fn rng_validate_pattern(
1440    pattern: &RelaxNgPattern,
1441    node: *mut _xmlNode,
1442    schema: &RelaxNgSchema,
1443    ctxt: &mut RelaxNgValidCtxt,
1444) -> bool {
1445    unsafe {
1446        if ctxt.depth >= ctxt.depth_max {
1447            ctxt.record_error("Maximum validation depth exceeded".to_string());
1448            return false;
1449        }
1450        ctxt.depth += 1;
1451
1452        let result = match pattern.pattern_type {
1453            RelaxNgPatternType::Element => {
1454                rng_validate_element_pattern(pattern, node, schema, ctxt)
1455            }
1456            RelaxNgPatternType::Attribute => {
1457                rng_validate_attribute_pattern(pattern, node, schema, ctxt)
1458            }
1459            RelaxNgPatternType::Text => rng_validate_text_pattern(node, ctxt),
1460            RelaxNgPatternType::Empty => rng_validate_empty_pattern(node, ctxt),
1461            RelaxNgPatternType::NotAllowed => {
1462                let name = get_node_qname(node);
1463                ctxt.record_error(format!(
1464                    "Element '{}' is not allowed at '{}'",
1465                    name,
1466                    ctxt.current_path()
1467                ));
1468                false
1469            }
1470            RelaxNgPatternType::Choice => rng_validate_choice_pattern(pattern, node, schema, ctxt),
1471            RelaxNgPatternType::Sequence => {
1472                rng_validate_sequence_pattern(pattern, node, schema, ctxt)
1473            }
1474            RelaxNgPatternType::Interleave => {
1475                rng_validate_interleave_pattern(pattern, node, schema, ctxt)
1476            }
1477            RelaxNgPatternType::ZeroOrMore => {
1478                rng_validate_zero_or_more(pattern, node, schema, ctxt)
1479            }
1480            RelaxNgPatternType::OneOrMore => rng_validate_one_or_more(pattern, node, schema, ctxt),
1481            RelaxNgPatternType::Optional => {
1482                rng_validate_optional_pattern(pattern, node, schema, ctxt)
1483            }
1484            RelaxNgPatternType::List => rng_validate_list_pattern(pattern, node, schema, ctxt),
1485            RelaxNgPatternType::Group => rng_validate_group_pattern(pattern, node, schema, ctxt),
1486            RelaxNgPatternType::Data => rng_validate_data_pattern(pattern, node, ctxt),
1487            RelaxNgPatternType::Value => rng_validate_value_pattern(pattern, node, ctxt),
1488            RelaxNgPatternType::Ref => rng_validate_ref_pattern(pattern, node, schema, ctxt),
1489            RelaxNgPatternType::ExternalRef => {
1490                // External refs are resolved during parsing; treat as empty
1491                rng_validate_empty_pattern(node, ctxt)
1492            }
1493            RelaxNgPatternType::Define
1494            | RelaxNgPatternType::Grammar
1495            | RelaxNgPatternType::Start
1496            | RelaxNgPatternType::Include => {
1497                // These shouldn't appear during validation; treat as pass-through
1498                rng_validate_children(pattern, node, schema, ctxt)
1499            }
1500        };
1501
1502        ctxt.depth -= 1;
1503        result
1504    }
1505}
1506
1507/// Validate a pattern's children against a node's children.
1508///
1509/// # SAFETY
1510///
1511/// - `node` must be a valid pointer to an _xmlNode or NULL.
1512fn rng_validate_children(
1513    pattern: &RelaxNgPattern,
1514    node: *mut _xmlNode,
1515    schema: &RelaxNgSchema,
1516    ctxt: &mut RelaxNgValidCtxt,
1517) -> bool {
1518    {
1519        if pattern.children.is_empty() {
1520            return true;
1521        }
1522        // Validate each child pattern against the same node
1523        let mut valid = true;
1524        for child in &pattern.children {
1525            valid &= rng_validate_pattern(child, node, schema, ctxt);
1526        }
1527        valid
1528    }
1529}
1530
1531/// Validate an element pattern against an element node.
1532///
1533/// # SAFETY
1534///
1535/// - `node` must be a valid pointer to an _xmlNode or NULL.
1536fn rng_validate_element_pattern(
1537    pattern: &RelaxNgPattern,
1538    node: *mut _xmlNode,
1539    schema: &RelaxNgSchema,
1540    ctxt: &mut RelaxNgValidCtxt,
1541) -> bool {
1542    unsafe {
1543        if node.is_null() || (*node).type_ != XML_ELEMENT_NODE as c_int {
1544            return false;
1545        }
1546
1547        let node_name = get_node_qname(node);
1548        let ns_uri = get_node_ns_uri(node);
1549
1550        // Check if the node name matches the element pattern's name class
1551        if let Some(ref nc) = pattern.name_class {
1552            if !nc.matches(&node_name, ns_uri.as_deref()) {
1553                let pat_name = pattern.name.as_deref().unwrap_or("?");
1554                ctxt.record_error(format!(
1555                    "Element '{}' does not match expected element pattern '{}' at '{}'",
1556                    node_name,
1557                    pat_name,
1558                    ctxt.current_path()
1559                ));
1560                return false;
1561            }
1562        }
1563
1564        // Push the element name onto the path
1565        ctxt.path.push(node_name.clone());
1566
1567        // Validate child patterns against this element's CHILDREN.
1568        // UPSTREAM-PARITY (relaxng.c xmlRelaxNGValidateElement): the
1569        // content patterns describe the element's children, not the element
1570        // itself. The pre-fix code validated them against the element node
1571        // directly, so any nested element pattern failed its name match and
1572        // every non-empty schema rejected every document (Phase 14 lxml
1573        // RelaxNG court).
1574        let valid = rng_validate_content(&pattern.children, node, schema, ctxt);
1575
1576        ctxt.path.pop();
1577        valid
1578    }
1579}
1580
1581/// Validate an element's content model (the child patterns of an element
1582/// pattern) against the element's child nodes.
1583///
1584/// An empty content model requires no element children; a single structural
1585/// pattern (sequence/choice/interleave/group/zeroOrMore/oneOrMore/optional)
1586/// consumes the child list with its own logic; anything else is an implicit
1587/// sequence over the element's element-children, where a `text` pattern
1588/// validates against the element itself (any text is allowed) without
1589/// consuming a child.
1590///
1591/// # SAFETY
1592///
1593/// - `node` must be a valid pointer to an _xmlNode or NULL.
1594fn rng_validate_content(
1595    content: &[RelaxNgPattern],
1596    node: *mut _xmlNode,
1597    schema: &RelaxNgSchema,
1598    ctxt: &mut RelaxNgValidCtxt,
1599) -> bool {
1600    if content.is_empty() {
1601        return rng_validate_empty_pattern(node, ctxt);
1602    }
1603    if content.len() == 1 {
1604        let p = &content[0];
1605        match p.pattern_type {
1606            RelaxNgPatternType::Sequence
1607            | RelaxNgPatternType::Choice
1608            | RelaxNgPatternType::Interleave
1609            | RelaxNgPatternType::Group
1610            | RelaxNgPatternType::ZeroOrMore
1611            | RelaxNgPatternType::OneOrMore
1612            | RelaxNgPatternType::Optional
1613            | RelaxNgPatternType::List => {
1614                return rng_validate_pattern(p, node, schema, ctxt);
1615            }
1616            RelaxNgPatternType::Text => return rng_validate_text_pattern(node, ctxt),
1617            RelaxNgPatternType::Empty => return rng_validate_empty_pattern(node, ctxt),
1618            RelaxNgPatternType::Data => return rng_validate_data_pattern(p, node, ctxt),
1619            RelaxNgPatternType::Value => return rng_validate_value_pattern(p, node, ctxt),
1620            RelaxNgPatternType::Attribute => {
1621                return rng_validate_attribute_pattern(p, node, schema, ctxt)
1622            }
1623            _ => {}
1624        }
1625    }
1626
1627    unsafe {
1628        // Collect the element's element-children in document order.
1629        let mut child_nodes: Vec<*mut _xmlNode> = Vec::new();
1630        let mut child = (*node).children;
1631        while !child.is_null() {
1632            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1633                child_nodes.push(child);
1634            }
1635            child = (*child).next;
1636        }
1637
1638        let mut valid = true;
1639        let mut child_idx = 0;
1640        for child_pat in content {
1641            // Patterns over the element's own character data and attributes
1642            // do NOT consume an element child: `text` matches any text,
1643            // `data`/`value` validate the concatenated text content,
1644            // `attribute` validates the element's attributes, and `empty`
1645            // requires no element children (upstream relaxng.c treats these
1646            // against the element's data/attributes, not its child list).
1647            match child_pat.pattern_type {
1648                RelaxNgPatternType::Text => {
1649                    valid &= rng_validate_text_pattern(node, ctxt);
1650                    continue;
1651                }
1652                RelaxNgPatternType::Data => {
1653                    valid &= rng_validate_data_pattern(child_pat, node, ctxt);
1654                    continue;
1655                }
1656                RelaxNgPatternType::Value => {
1657                    valid &= rng_validate_value_pattern(child_pat, node, ctxt);
1658                    continue;
1659                }
1660                RelaxNgPatternType::Attribute => {
1661                    valid &= rng_validate_attribute_pattern(child_pat, node, schema, ctxt);
1662                    continue;
1663                }
1664                RelaxNgPatternType::Empty => {
1665                    valid &= rng_validate_empty_pattern(node, ctxt);
1666                    continue;
1667                }
1668                _ => {}
1669            }
1670            if child_idx >= child_nodes.len() {
1671                match child_pat.pattern_type {
1672                    RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
1673                        // These are fine to have no matching children.
1674                        continue;
1675                    }
1676                    RelaxNgPatternType::OneOrMore => {
1677                        ctxt.record_error(format!(
1678                            "Expected at least one matching child for oneOrMore at '{}'",
1679                            ctxt.current_path()
1680                        ));
1681                        valid = false;
1682                        continue;
1683                    }
1684                    _ => {
1685                        ctxt.record_error(format!(
1686                            "Expected more child elements for content model at '{}'",
1687                            ctxt.current_path()
1688                        ));
1689                        valid = false;
1690                        continue;
1691                    }
1692                }
1693            }
1694            let child_node = child_nodes[child_idx];
1695            valid &= rng_validate_pattern(child_pat, child_node, schema, ctxt);
1696            child_idx += 1;
1697        }
1698
1699        // Check for extra children.
1700        if child_idx < child_nodes.len() {
1701            let extra_name = get_node_qname(child_nodes[child_idx]);
1702            // UPSTREAM-PARITY (relaxng.c xmlRelaxNGValidateElement, error
1703            // XML_RELAXNG_ERR_ELEMWRONG): "Did not expect element %s there".
1704            ctxt.record_error(format!("Did not expect element {} there", extra_name));
1705            valid = false;
1706        }
1707
1708        valid
1709    }
1710}
1711
1712/// Validate an attribute pattern.
1713///
1714/// # SAFETY
1715///
1716/// - `node` must be a valid pointer to an _xmlNode or NULL.
1717fn rng_validate_attribute_pattern(
1718    pattern: &RelaxNgPattern,
1719    node: *mut _xmlNode,
1720    _schema: &RelaxNgSchema,
1721    ctxt: &mut RelaxNgValidCtxt,
1722) -> bool {
1723    unsafe {
1724        if node.is_null() || (*node).type_ != XML_ELEMENT_NODE as c_int {
1725            return false;
1726        }
1727
1728        // Get the attribute name from the pattern's name class
1729        let attr_name = match &pattern.name_class {
1730            Some(RelaxNgNameClass::Name(n)) => n.clone(),
1731            Some(RelaxNgNameClass::AnyName) => {
1732                // Any attribute is allowed — just check that there's at least one
1733                // attribute that matches (or return true if the attribute is optional)
1734                // For now, we just return true for anyName since we can't know which
1735                // attribute to check
1736                return true;
1737            }
1738            Some(RelaxNgNameClass::NsName(ns)) => {
1739                // Any attribute in the given namespace.
1740                // Check if there's an attribute with a matching namespace.
1741                let mut prop = (*node).properties;
1742                while !prop.is_null() {
1743                    let prop_ns = get_node_ns_uri(prop as *mut _xmlNode);
1744                    if let Some(ref uri) = prop_ns {
1745                        if uri == ns {
1746                            // Validate content pattern against attribute value
1747                            if let Some(content) = &pattern.children.first() {
1748                                let val = get_node_text(prop as *mut _xmlNode);
1749                                let valid = match content.pattern_type {
1750                                    RelaxNgPatternType::Text => true,
1751                                    RelaxNgPatternType::Data => rng_validate_datatype_value(
1752                                        content.datatype.as_deref(),
1753                                        &val,
1754                                    ),
1755                                    RelaxNgPatternType::Value => {
1756                                        content.value.as_deref() == Some(&val)
1757                                    }
1758                                    _ => true,
1759                                };
1760                                if !valid {
1761                                    ctxt.record_error(format!(
1762                                        "Attribute '{}' has invalid value at '{}'",
1763                                        prop_ns.unwrap_or_default(),
1764                                        ctxt.current_path()
1765                                    ));
1766                                    return false;
1767                                }
1768                            }
1769                            return true;
1770                        }
1771                    }
1772                    prop = (*prop).next;
1773                }
1774                // No matching attribute found — attribute is required
1775                // (In RELAX NG, attributes are implicitly required)
1776                ctxt.record_error(format!(
1777                    "Required attribute in namespace '{}' is missing at '{}'",
1778                    ns,
1779                    ctxt.current_path()
1780                ));
1781                return false;
1782            }
1783            _ => {
1784                // Complex name class — just check if any attribute matches
1785                // This is a simplified check
1786                return true;
1787            }
1788        };
1789
1790        // Check if the attribute exists on the element
1791        let attr_value = get_attr(node, &attr_name);
1792
1793        match attr_value {
1794            Some(val) => {
1795                // Validate content pattern against attribute value
1796                if let Some(content) = pattern.children.first() {
1797                    let valid = match content.pattern_type {
1798                        RelaxNgPatternType::Text => true,
1799                        RelaxNgPatternType::Data => {
1800                            rng_validate_datatype_value(content.datatype.as_deref(), &val)
1801                        }
1802                        RelaxNgPatternType::Value => content.value.as_deref() == Some(&val),
1803                        _ => true,
1804                    };
1805                    if !valid {
1806                        ctxt.record_error(format!(
1807                            "Attribute '{}' has invalid value '{}' at '{}'",
1808                            attr_name,
1809                            val,
1810                            ctxt.current_path()
1811                        ));
1812                        return false;
1813                    }
1814                }
1815                true
1816            }
1817            None => {
1818                // Attribute not found — only error if the pattern requires it
1819                // (In RELAX NG, attributes are implicitly required unless wrapped in optional)
1820                ctxt.record_error(format!(
1821                    "Required attribute '{}' is missing at '{}'",
1822                    attr_name,
1823                    ctxt.current_path()
1824                ));
1825                false
1826            }
1827        }
1828    }
1829}
1830
1831/// Validate a text pattern against text content.
1832///
1833/// # SAFETY
1834///
1835/// - `node` must be a valid pointer to an _xmlNode or NULL.
1836const fn rng_validate_text_pattern(node: *mut _xmlNode, _ctxt: &mut RelaxNgValidCtxt) -> bool {
1837    {
1838        if node.is_null() {
1839            return false;
1840        }
1841        // Text pattern matches any text content (or mixed content with elements)
1842        // In RELAX NG, text allows any text content
1843        true
1844    }
1845}
1846
1847/// Validate an empty pattern.
1848///
1849/// # SAFETY
1850///
1851/// - `node` must be a valid pointer to an _xmlNode or NULL.
1852fn rng_validate_empty_pattern(node: *mut _xmlNode, ctxt: &mut RelaxNgValidCtxt) -> bool {
1853    unsafe {
1854        if node.is_null() {
1855            return true;
1856        }
1857        // Empty pattern — the element must have no element children
1858        let mut child = (*node).children;
1859        while !child.is_null() {
1860            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1861                let child_name = get_node_qname(child);
1862                ctxt.record_error(format!(
1863                    "Unexpected child element '{}' in empty content at '{}'",
1864                    child_name,
1865                    ctxt.current_path()
1866                ));
1867                return false;
1868            }
1869            child = (*child).next;
1870        }
1871        true
1872    }
1873}
1874
1875/// Validate a choice pattern.
1876///
1877/// # SAFETY
1878///
1879/// - `node` must be a valid pointer to an _xmlNode or NULL.
1880fn rng_validate_choice_pattern(
1881    pattern: &RelaxNgPattern,
1882    node: *mut _xmlNode,
1883    schema: &RelaxNgSchema,
1884    ctxt: &mut RelaxNgValidCtxt,
1885) -> bool {
1886    unsafe {
1887        if pattern.children.is_empty() {
1888            return false;
1889        }
1890
1891        // At least one choice must match
1892        let mut last_error = String::new();
1893        for child in &pattern.children {
1894            let saved_errors = ctxt.errors.len();
1895            let saved_nb = ctxt.nb_errors;
1896
1897            if rng_validate_pattern(child, node, schema, ctxt) {
1898                // Restore errors from failed choices
1899                // (errors from failed branches should be discarded)
1900                return true;
1901            }
1902
1903            // Capture the last error for reporting
1904            if ctxt.errors.len() > saved_errors {
1905                last_error = ctxt.errors.last().unwrap().clone();
1906            }
1907
1908            // Restore error state (choice means at least one alternative must pass)
1909            ctxt.errors.truncate(saved_errors);
1910            ctxt.nb_errors = saved_nb;
1911        }
1912
1913        // If we got here, none of the choices matched
1914        let node_name = if node.is_null() {
1915            "null".to_string()
1916        } else {
1917            get_node_qname(node)
1918        };
1919        ctxt.record_error(format!(
1920            "No choice pattern matched for '{}' at '{}'. Last error: {}",
1921            node_name,
1922            ctxt.current_path(),
1923            last_error
1924        ));
1925        false
1926    }
1927}
1928
1929/// Validate a sequence pattern.
1930///
1931/// # SAFETY
1932///
1933/// - `node` must be a valid pointer to an _xmlNode or NULL.
1934fn rng_validate_sequence_pattern(
1935    pattern: &RelaxNgPattern,
1936    node: *mut _xmlNode,
1937    schema: &RelaxNgSchema,
1938    ctxt: &mut RelaxNgValidCtxt,
1939) -> bool {
1940    unsafe {
1941        if node.is_null() {
1942            return pattern.children.is_empty();
1943        }
1944
1945        let mut valid = true;
1946
1947        // For sequence validation, we validate each child pattern against
1948        // the node's children in order.
1949        // Collect element children of the node
1950        let mut child_nodes: Vec<*mut _xmlNode> = Vec::new();
1951        let mut child = (*node).children;
1952        while !child.is_null() {
1953            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1954                child_nodes.push(child);
1955            }
1956            child = (*child).next;
1957        }
1958
1959        // Simple sequence matching: validate each child pattern
1960        // against corresponding child nodes
1961        let mut child_idx = 0;
1962        for child_pat in &pattern.children {
1963            if child_idx >= child_nodes.len() {
1964                // Check if the pattern is optional or zero-or-more
1965                match child_pat.pattern_type {
1966                    RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
1967                        // These are fine to have no matching children
1968                        continue;
1969                    }
1970                    RelaxNgPatternType::OneOrMore => {
1971                        ctxt.record_error(format!(
1972                            "Expected at least one matching child for oneOrMore at '{}'",
1973                            ctxt.current_path()
1974                        ));
1975                        valid = false;
1976                        continue;
1977                    }
1978                    _ => {
1979                        ctxt.record_error(format!(
1980                            "Expected more child elements for sequence at '{}'",
1981                            ctxt.current_path()
1982                        ));
1983                        valid = false;
1984                        continue;
1985                    }
1986                }
1987            }
1988
1989            let child_node = child_nodes[child_idx];
1990            valid &= rng_validate_pattern(child_pat, child_node, schema, ctxt);
1991            child_idx += 1;
1992        }
1993
1994        // Check for extra children
1995        if child_idx < child_nodes.len() {
1996            let extra_name = get_node_qname(child_nodes[child_idx]);
1997            ctxt.record_error(format!(
1998                "Unexpected extra element '{}' in sequence at '{}'",
1999                extra_name,
2000                ctxt.current_path()
2001            ));
2002            valid = false;
2003        }
2004
2005        valid
2006    }
2007}
2008
2009/// Validate an interleave pattern.
2010///
2011/// # SAFETY
2012///
2013/// - `node` must be a valid pointer to an _xmlNode or NULL.
2014fn rng_validate_interleave_pattern(
2015    pattern: &RelaxNgPattern,
2016    node: *mut _xmlNode,
2017    schema: &RelaxNgSchema,
2018    ctxt: &mut RelaxNgValidCtxt,
2019) -> bool {
2020    unsafe {
2021        if node.is_null() {
2022            return pattern.children.is_empty();
2023        }
2024
2025        // Interleave means child patterns can match in any order.
2026        // For simplicity, we validate each child pattern against a fresh
2027        // traversal of the node's children.
2028        let mut valid = true;
2029
2030        for child_pat in &pattern.children {
2031            // For each pattern in the interleave, check if there's at least
2032            // one child node that matches
2033            let mut child = (*node).children;
2034            let mut matched = false;
2035
2036            while !child.is_null() {
2037                if (*child).type_ == XML_ELEMENT_NODE as c_int {
2038                    let saved_errors = ctxt.errors.len();
2039                    let saved_nb = ctxt.nb_errors;
2040
2041                    if rng_validate_pattern(child_pat, child, schema, ctxt) {
2042                        matched = true;
2043                        break;
2044                    }
2045
2046                    // Restore errors for this attempt
2047                    ctxt.errors.truncate(saved_errors);
2048                    ctxt.nb_errors = saved_nb;
2049                }
2050                child = (*child).next;
2051            }
2052
2053            if !matched {
2054                // Check if pattern is optional
2055                match child_pat.pattern_type {
2056                    RelaxNgPatternType::Optional | RelaxNgPatternType::ZeroOrMore => {
2057                        // Optional patterns can be absent
2058                    }
2059                    _ => {
2060                        let pat_desc = format!("{:?}", child_pat.pattern_type);
2061                        ctxt.record_error(format!(
2062                            "Interleave pattern '{}' did not match any child at '{}'",
2063                            pat_desc,
2064                            ctxt.current_path()
2065                        ));
2066                        valid = false;
2067                    }
2068                }
2069            }
2070        }
2071
2072        valid
2073    }
2074}
2075
2076/// Validate a zeroOrMore pattern.
2077///
2078/// # SAFETY
2079///
2080/// - `node` must be a valid pointer to an _xmlNode or NULL.
2081fn rng_validate_zero_or_more(
2082    pattern: &RelaxNgPattern,
2083    node: *mut _xmlNode,
2084    schema: &RelaxNgSchema,
2085    ctxt: &mut RelaxNgValidCtxt,
2086) -> bool {
2087    unsafe {
2088        if node.is_null() || pattern.children.is_empty() {
2089            return true;
2090        }
2091
2092        let child_pat = &pattern.children[0];
2093        let valid = true;
2094
2095        // Match as many child nodes as possible against the pattern
2096        let mut child = (*node).children;
2097        while !child.is_null() {
2098            if (*child).type_ == XML_ELEMENT_NODE as c_int {
2099                let saved_errors = ctxt.errors.len();
2100                let saved_nb = ctxt.nb_errors;
2101
2102                if !rng_validate_pattern(child_pat, child, schema, ctxt) {
2103                    // This child doesn't match the zeroOrMore pattern
2104                    // Restore errors and stop
2105                    ctxt.errors.truncate(saved_errors);
2106                    ctxt.nb_errors = saved_nb;
2107                    break;
2108                }
2109                // Successfully matched — errors from matching are valid
2110            }
2111            child = (*child).next;
2112        }
2113
2114        valid
2115    }
2116}
2117
2118/// Validate a oneOrMore pattern.
2119///
2120/// # SAFETY
2121///
2122/// - `node` must be a valid pointer to an _xmlNode or NULL.
2123fn rng_validate_one_or_more(
2124    pattern: &RelaxNgPattern,
2125    node: *mut _xmlNode,
2126    schema: &RelaxNgSchema,
2127    ctxt: &mut RelaxNgValidCtxt,
2128) -> bool {
2129    unsafe {
2130        if node.is_null() || pattern.children.is_empty() {
2131            ctxt.record_error(format!(
2132                "Expected at least one matching element for oneOrMore at '{}'",
2133                ctxt.current_path()
2134            ));
2135            return false;
2136        }
2137
2138        let child_pat = &pattern.children[0];
2139        let mut matched = false;
2140        let mut valid = true;
2141
2142        // Match at least one child, then as many as possible
2143        let mut child = (*node).children;
2144        while !child.is_null() {
2145            if (*child).type_ == XML_ELEMENT_NODE as c_int {
2146                let saved_errors = ctxt.errors.len();
2147                let saved_nb = ctxt.nb_errors;
2148
2149                if rng_validate_pattern(child_pat, child, schema, ctxt) {
2150                    matched = true;
2151                } else {
2152                    // Restore errors and stop
2153                    ctxt.errors.truncate(saved_errors);
2154                    ctxt.nb_errors = saved_nb;
2155                    break;
2156                }
2157            }
2158            child = (*child).next;
2159        }
2160
2161        if !matched {
2162            ctxt.record_error(format!(
2163                "Expected at least one matching element for oneOrMore at '{}'",
2164                ctxt.current_path()
2165            ));
2166            valid = false;
2167        }
2168
2169        valid
2170    }
2171}
2172
2173/// Validate an optional pattern.
2174///
2175/// # SAFETY
2176///
2177/// - `node` must be a valid pointer to an _xmlNode or NULL.
2178fn rng_validate_optional_pattern(
2179    pattern: &RelaxNgPattern,
2180    node: *mut _xmlNode,
2181    schema: &RelaxNgSchema,
2182    ctxt: &mut RelaxNgValidCtxt,
2183) -> bool {
2184    {
2185        if pattern.children.is_empty() {
2186            return true;
2187        }
2188
2189        // Optional means the pattern can match or not — both are valid
2190        let child_pat = &pattern.children[0];
2191        let saved_errors = ctxt.errors.len();
2192        let saved_nb = ctxt.nb_errors;
2193
2194        let result = rng_validate_pattern(child_pat, node, schema, ctxt);
2195
2196        if !result {
2197            // Restore errors — it's okay that optional didn't match
2198            ctxt.errors.truncate(saved_errors);
2199            ctxt.nb_errors = saved_nb;
2200        }
2201
2202        true // Optional always returns true
2203    }
2204}
2205
2206/// Validate a list pattern.
2207///
2208/// # SAFETY
2209///
2210/// - `node` must be a valid pointer to an _xmlNode or NULL.
2211fn rng_validate_list_pattern(
2212    pattern: &RelaxNgPattern,
2213    node: *mut _xmlNode,
2214    _schema: &RelaxNgSchema,
2215    _ctxt: &mut RelaxNgValidCtxt,
2216) -> bool {
2217    unsafe {
2218        if node.is_null() {
2219            return pattern.children.is_empty();
2220        }
2221
2222        // List pattern: whitespace-separated tokens, each matching the child pattern
2223        let text = get_node_text(node);
2224        if text.trim().is_empty() {
2225            return true;
2226        }
2227
2228        let tokens: Vec<&str> = text.split_whitespace().collect();
2229        let mut valid = true;
2230
2231        for token in &tokens {
2232            // For each token, validate against the child pattern
2233            // We do a simplified check — just ensure it's non-empty
2234            if token.is_empty() {
2235                valid = false;
2236                break;
2237            }
2238        }
2239
2240        valid
2241    }
2242}
2243
2244/// Validate a group pattern.
2245///
2246/// # SAFETY
2247///
2248/// - `node` must be a valid pointer to an _xmlNode or NULL.
2249fn rng_validate_group_pattern(
2250    pattern: &RelaxNgPattern,
2251    node: *mut _xmlNode,
2252    schema: &RelaxNgSchema,
2253    ctxt: &mut RelaxNgValidCtxt,
2254) -> bool {
2255    {
2256        // Group is similar to sequence — validate children in order
2257        rng_validate_sequence_pattern(pattern, node, schema, ctxt)
2258    }
2259}
2260
2261/// Validate a data pattern.
2262///
2263/// # SAFETY
2264///
2265/// - `node` must be a valid pointer to an _xmlNode or NULL.
2266fn rng_validate_data_pattern(
2267    pattern: &RelaxNgPattern,
2268    node: *mut _xmlNode,
2269    ctxt: &mut RelaxNgValidCtxt,
2270) -> bool {
2271    unsafe {
2272        if node.is_null() {
2273            return false;
2274        }
2275
2276        let text = get_node_text(node);
2277        let datatype = pattern.datatype.as_deref();
2278
2279        if !rng_validate_datatype_value(datatype, &text) {
2280            ctxt.record_error(format!(
2281                "Value '{}' does not match datatype '{:?}' at '{}'",
2282                text,
2283                datatype,
2284                ctxt.current_path()
2285            ));
2286            return false;
2287        }
2288
2289        true
2290    }
2291}
2292
2293/// Validate a value pattern.
2294///
2295/// # SAFETY
2296///
2297/// - `node` must be a valid pointer to an _xmlNode or NULL.
2298fn rng_validate_value_pattern(
2299    pattern: &RelaxNgPattern,
2300    node: *mut _xmlNode,
2301    ctxt: &mut RelaxNgValidCtxt,
2302) -> bool {
2303    unsafe {
2304        if node.is_null() {
2305            return false;
2306        }
2307
2308        let text = get_node_text(node).trim().to_string();
2309        let expected = pattern.value.as_deref().unwrap_or("");
2310
2311        if text != expected {
2312            ctxt.record_error(format!(
2313                "Value '{}' does not match expected value '{}' at '{}'",
2314                text,
2315                expected,
2316                ctxt.current_path()
2317            ));
2318            return false;
2319        }
2320
2321        true
2322    }
2323}
2324
2325/// Validate a ref pattern.
2326///
2327/// # SAFETY
2328///
2329/// - `node` must be a valid pointer to an _xmlNode or NULL.
2330fn rng_validate_ref_pattern(
2331    pattern: &RelaxNgPattern,
2332    node: *mut _xmlNode,
2333    schema: &RelaxNgSchema,
2334    ctxt: &mut RelaxNgValidCtxt,
2335) -> bool {
2336    {
2337        let ref_name = pattern.name.as_deref().unwrap_or("");
2338
2339        if ref_name.is_empty() {
2340            ctxt.record_error("Ref pattern has no name".to_string());
2341            return false;
2342        }
2343
2344        // Look up the definition
2345        match schema.grammar.lookup(ref_name) {
2346            Some(def_pattern) => {
2347                // Validate against the definition's pattern
2348                rng_validate_pattern(def_pattern, node, schema, ctxt)
2349            }
2350            None => {
2351                ctxt.record_error(format!(
2352                    "Undefined reference '{}' at '{}'",
2353                    ref_name,
2354                    ctxt.current_path()
2355                ));
2356                false
2357            }
2358        }
2359    }
2360}
2361
2362// ═══════════════════════════════════════════════════════════════════════════════
2363// Datatype Validation for RELAX NG
2364// ═══════════════════════════════════════════════════════════════════════════════
2365
2366/// Validate a value against a RELAX NG datatype.
2367///
2368/// RELAX NG supports a subset of XML Schema datatypes. This function
2369/// provides basic datatype validation for common types.
2370fn rng_validate_datatype_value(datatype: Option<&str>, value: &str) -> bool {
2371    let dt = match datatype {
2372        Some(d) => d,
2373        None => return true, // No datatype specified — accept anything
2374    };
2375
2376    match dt {
2377        "string" | "token" => true,
2378        "boolean" => {
2379            matches!(value, "true" | "false" | "1" | "0")
2380        }
2381        "integer" | "int" | "short" | "byte" | "long" => {
2382            if value.is_empty() {
2383                return false;
2384            }
2385            let trimmed = if value.starts_with('+') || value.starts_with('-') {
2386                &value[1..]
2387            } else {
2388                value
2389            };
2390            !trimmed.is_empty() && trimmed.chars().all(|c| c.is_ascii_digit())
2391        }
2392        "decimal" | "double" | "float" => {
2393            if value.is_empty() {
2394                return false;
2395            }
2396            // Allow INF, -INF, NaN for float/double
2397            if matches!(dt, "float" | "double") && matches!(value, "INF" | "-INF" | "NaN") {
2398                return true;
2399            }
2400            value.parse::<f64>().is_ok()
2401        }
2402        "NCName" | "Name" | "ID" | "IDREF" | "NMTOKEN" => {
2403            !value.is_empty() && !value.starts_with(|c: char| c.is_ascii_digit())
2404        }
2405        "anyURI" => {
2406            // Simple URI validation — non-empty and no spaces
2407            !value.is_empty() && !value.contains(char::is_whitespace)
2408        }
2409        "QName" => {
2410            if value.is_empty() {
2411                return false;
2412            }
2413            if let Some(pos) = value.find(':') {
2414                pos > 0 && pos < value.len() - 1
2415            } else {
2416                true
2417            }
2418        }
2419        _ => {
2420            // Unknown datatype — accept by default
2421            // This matches libxml2's lenient behavior
2422            true
2423        }
2424    }
2425}
2426
2427// ═══════════════════════════════════════════════════════════════════════════════
2428// Public API Functions
2429// ═══════════════════════════════════════════════════════════════════════════════
2430
2431/// Parse a RELAX NG schema from an XML string.
2432///
2433/// Returns the parsed schema, or an error message on failure.
2434pub fn rng_parse_schema(xml_doc: &str) -> Result<RelaxNgSchema, String> {
2435    rng_parse(xml_doc)
2436}
2437
2438/// Parse a RELAX NG schema from a parsed XML document.
2439///
2440/// # SAFETY
2441///
2442/// - `doc` must be a valid pointer to an _xmlDoc.
2443pub unsafe fn rng_parse_schema_doc(doc: *mut _xmlDoc) -> Result<RelaxNgSchema, String> {
2444    rng_parse_doc(doc)
2445}
2446
2447/// Validate a document against a RELAX NG schema.
2448///
2449/// Returns `true` if the document is valid.
2450///
2451/// # SAFETY
2452///
2453/// - `doc` must be a valid pointer to an _xmlDoc or NULL.
2454pub unsafe fn rng_validate_doc_schema(
2455    schema: &RelaxNgSchema,
2456    doc: *mut _xmlDoc,
2457    ctxt: &mut RelaxNgValidCtxt,
2458) -> bool {
2459    rng_validate_doc(schema, doc, ctxt)
2460}
2461
2462// ═══════════════════════════════════════════════════════════════════════════════
2463// C ABI Functions
2464// ═══════════════════════════════════════════════════════════════════════════════
2465
2466// These are the C-compatible entry points that get exported via the ABI layer.
2467// They use raw pointers and follow libxml2's calling conventions.
2468
2469/// Create a new RELAX NG parser context.
2470/// RELAX NG parser context (upstream `xmlRelaxNGParserCtxt`).
2471///
2472/// Owns the eagerly-parsed schema; `xmlRelaxNGParse` hands out a NEW schema
2473/// object (a clone) so the context and the schema have separate lifetimes,
2474/// exactly as upstream callers expect (lxml: `xmlRelaxNGParse` then
2475/// `xmlRelaxNGFreeParserCtxt`, with `xmlRelaxNGFree` on the schema at
2476/// dealloc). The pre-fix implementation returned the context as the schema
2477/// pointer, so `xmlRelaxNGFreeParserCtxt` freed the schema out from under
2478/// consumers — a use-after-free (Phase 14 lxml RelaxNG court).
2479pub(crate) struct RelaxNgParserCtxt {
2480    /// The parsed schema, if parsing succeeded.
2481    pub(crate) schema: Option<RelaxNgSchema>,
2482    /// The schema resource name for file-based parser contexts. Kept so
2483    /// `xmlRelaxNGParse` can report upstream's "could not load %s" diagnostic
2484    /// when the resource was not readable (the I/O warning itself is emitted
2485    /// by the document open, mirroring upstream xmlRelaxNGParseFile).
2486    pub(crate) url: Option<String>,
2487}
2488
2489/// Create a new RELAX NG parser context from a URL.
2490///
2491/// # UPSTREAM-PARITY
2492///
2493/// ```c
2494/// xmlRelaxNGParserCtxtPtr xmlRelaxNGNewParserCtxt(const char *URL);
2495/// ```
2496///
2497/// # SAFETY
2498///
2499/// - `url` must be a valid null-terminated C string or NULL.
2500#[no_mangle]
2501pub unsafe extern "C" fn xmlRelaxNGNewParserCtxt(url: *const c_char) -> *mut c_void {
2502    if url.is_null() {
2503        return Box::into_raw(Box::new(RelaxNgParserCtxt {
2504            schema: None,
2505            url: None,
2506        })) as *mut c_void;
2507    }
2508
2509    let url_str = unsafe {
2510        let mut len = 0;
2511        while *url.add(len) != 0 {
2512            len += 1;
2513        }
2514        let slice = std::slice::from_raw_parts(url as *const u8, len);
2515        String::from_utf8_lossy(slice).to_string()
2516    };
2517
2518    // Parse the schema from the URL eagerly and keep it in the context. The
2519    // document is opened through the standard input machinery
2520    // (xmlParseFile), so an unreadable resource raises the upstream I/O
2521    // warning exactly like xmlRelaxNGParseFile.
2522    let schema = if url_str.is_empty() {
2523        None
2524    } else {
2525        let url_c = std::ffi::CString::new(url_str.clone()).ok();
2526        if let Some(c) = url_c {
2527            let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
2528            if !doc.is_null() {
2529                let result = rng_parse_doc(doc);
2530                crate::abi::exports_xml2::xmlFreeDoc(doc);
2531                result.ok()
2532            } else {
2533                None
2534            }
2535        } else {
2536            None
2537        }
2538    };
2539    Box::into_raw(Box::new(RelaxNgParserCtxt {
2540        schema,
2541        url: Some(url_str),
2542    })) as *mut c_void
2543}
2544
2545/// Create a new RELAX NG parser context from a memory buffer.
2546///
2547/// # UPSTREAM-PARITY
2548///
2549/// ```c
2550/// xmlRelaxNGParserCtxtPtr xmlRelaxNGNewMemParserCtxt(const char *buffer, int size);
2551/// ```
2552///
2553/// # SAFETY
2554///
2555/// - `buffer` must be a valid pointer to a buffer of at least `size` bytes.
2556#[no_mangle]
2557pub unsafe extern "C" fn xmlRelaxNGNewMemParserCtxt(
2558    buffer: *const c_char,
2559    size: c_int,
2560) -> *mut c_void {
2561    if buffer.is_null() || size <= 0 {
2562        return ptr::null_mut();
2563    }
2564
2565    // Parse the schema immediately and keep it in the parser context;
2566    // `xmlRelaxNGParse` hands out a fresh schema object (Phase 14: the
2567    // context and the schema must have separate lifetimes — lxml frees the
2568    // context right after xmlRelaxNGParse and the schema at dealloc).
2569    let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
2570    let xml_str = String::from_utf8_lossy(buf_slice).to_string();
2571
2572    let schema = rng_parse(&xml_str).ok();
2573    Box::into_raw(Box::new(RelaxNgParserCtxt { schema, url: None })) as *mut c_void
2574}
2575
2576/// Parse a RELAX NG schema.
2577///
2578/// # UPSTREAM-PARITY
2579///
2580/// ```c
2581/// xmlRelaxNGPtr xmlRelaxNGParse(xmlRelaxNGParserCtxtPtr ctxt);
2582/// ```
2583///
2584/// # SAFETY
2585///
2586/// - `ctxt` must be a valid pointer to a parser context, or NULL.
2587#[no_mangle]
2588pub unsafe extern "C" fn xmlRelaxNGParse(ctxt: *mut c_void) -> *mut c_void {
2589    if ctxt.is_null() {
2590        return ptr::null_mut();
2591    }
2592
2593    // UPSTREAM-PARITY (relaxng.c xmlRelaxNGParse): the parser context owns
2594    // the parsed schema; this hands out a NEW schema object so the caller
2595    // can free the context independently (lxml frees the context right
2596    // after this call). The pre-fix implementation returned the context
2597    // itself, so xmlRelaxNGFreeParserCtxt freed the schema out from under
2598    // the consumer.
2599    let pctxt = unsafe { &*ctxt.cast::<RelaxNgParserCtxt>() };
2600    match &pctxt.schema {
2601        Some(schema) => {
2602            // A fatal grammar error (e.g. an <element> pattern without a
2603            // content model) aborts the parse upstream: the recorded
2604            // diagnostics go to the registered parser handlers and no schema
2605            // is returned (php then reports "Invalid RelaxNG" —
2606            // DOMDocument_relaxNGValidateSource_error2).
2607            if schema.fatal {
2608                crate::abi::exports_relaxng::dispatch_relaxng_parser_errors(
2609                    ctxt as usize,
2610                    &schema.errors,
2611                );
2612                return ptr::null_mut();
2613            }
2614            Box::into_raw(Box::new(schema.clone())) as *mut c_void
2615        }
2616        None => {
2617            // The main schema resource could not be loaded (xmlParseFile
2618            // already emitted the I/O warning). Report upstream's
2619            // "could not load" diagnostic through the parser handlers
2620            // (DOMDocument_relaxNGValidate_error2) before returning NULL.
2621            if let Some(ref url) = pctxt.url {
2622                if !url.is_empty() {
2623                    let msg = format!("xmlRelaxNGParse: could not load {}\n", url);
2624                    crate::abi::exports_relaxng::dispatch_relaxng_parser_errors(
2625                        ctxt as usize,
2626                        std::slice::from_ref(&msg),
2627                    );
2628                }
2629            }
2630            ptr::null_mut()
2631        }
2632    }
2633}
2634
2635/// Free a RELAX NG schema.
2636///
2637/// # UPSTREAM-PARITY
2638///
2639/// ```c
2640/// void xmlRelaxNGFree(xmlRelaxNGPtr schema);
2641/// ```
2642///
2643/// # SAFETY
2644///
2645/// - `schema` must be a valid pointer to a schema, or NULL.
2646#[no_mangle]
2647pub unsafe extern "C" fn xmlRelaxNGFree(schema: *mut c_void) {
2648    if schema.is_null() {
2649        return;
2650    }
2651    // SAFETY: Reconstruct the Box to drop it.
2652    unsafe {
2653        let _ = Box::from_raw(schema as *mut RelaxNgSchema);
2654    }
2655}
2656
2657/// Free a RELAX NG parser context.
2658///
2659/// # UPSTREAM-PARITY
2660///
2661/// ```c
2662/// void xmlRelaxNGFreeParserCtxt(xmlRelaxNGParserCtxtPtr ctxt);
2663/// ```
2664///
2665/// # SAFETY
2666///
2667/// - `ctxt` must be a valid pointer to a parser context, or NULL.
2668#[no_mangle]
2669pub unsafe extern "C" fn xmlRelaxNGFreeParserCtxt(ctxt: *mut c_void) {
2670    if ctxt.is_null() {
2671        return;
2672    }
2673    // SAFETY: Reconstruct the Box to drop it (the context is a separate
2674    // allocation from the schema handed out by xmlRelaxNGParse).
2675    unsafe {
2676        let _ = Box::from_raw(ctxt as *mut RelaxNgParserCtxt);
2677    }
2678}
2679
2680/// Create a new RELAX NG validation context.
2681///
2682/// # UPSTREAM-PARITY
2683///
2684/// ```c
2685/// xmlRelaxNGValidCtxtPtr xmlRelaxNGNewValidCtxt(xmlRelaxNGPtr schema);
2686/// ```
2687///
2688/// # SAFETY
2689///
2690/// - `schema` must be a valid pointer to a schema, or NULL.
2691#[no_mangle]
2692pub unsafe extern "C" fn xmlRelaxNGNewValidCtxt(schema: *mut c_void) -> *mut c_void {
2693    let mut ctxt = RelaxNgValidCtxt::new();
2694
2695    if !schema.is_null() {
2696        // SAFETY: The schema pointer is assumed to be a valid RelaxNgSchema.
2697        unsafe {
2698            let schema_ref = &*(schema as *const RelaxNgSchema);
2699            ctxt.schema = Some(schema_ref.clone());
2700        }
2701    }
2702
2703    let boxed = Box::new(ctxt);
2704    Box::into_raw(boxed) as *mut c_void
2705}
2706
2707/// Free a RELAX NG validation context.
2708///
2709/// # UPSTREAM-PARITY
2710///
2711/// ```c
2712/// void xmlRelaxNGFreeValidCtxt(xmlRelaxNGValidCtxtPtr ctxt);
2713/// ```
2714///
2715/// # SAFETY
2716///
2717/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2718#[no_mangle]
2719pub unsafe extern "C" fn xmlRelaxNGFreeValidCtxt(ctxt: *mut c_void) {
2720    if ctxt.is_null() {
2721        return;
2722    }
2723    // SAFETY: Reconstruct the Box to drop it.
2724    unsafe {
2725        let _ = Box::from_raw(ctxt as *mut RelaxNgValidCtxt);
2726    }
2727}
2728
2729/// Validate a document against a RELAX NG schema.
2730///
2731/// # UPSTREAM-PARITY
2732///
2733/// ```c
2734/// int xmlRelaxNGValidateDoc(xmlRelaxNGValidCtxtPtr ctxt, xmlDocPtr doc);
2735/// ```
2736///
2737/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
2738///
2739/// # SAFETY
2740///
2741/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2742/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2743#[no_mangle]
2744pub unsafe extern "C" fn xmlRelaxNGValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
2745    if ctxt.is_null() || doc.is_null() {
2746        return -1;
2747    }
2748
2749    unsafe {
2750        let valid_ctxt = &mut *(ctxt as *mut RelaxNgValidCtxt);
2751        let schema = match &valid_ctxt.schema {
2752            Some(s) => s,
2753            None => return -1,
2754        };
2755
2756        let mut temp_ctxt = RelaxNgValidCtxt::new();
2757
2758        let valid = rng_validate_doc(schema, doc, &mut temp_ctxt);
2759
2760        if valid {
2761            0
2762        } else {
2763            valid_ctxt.errors = temp_ctxt.errors;
2764            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
2765            // UPSTREAM-PARITY: forward each recorded error to the context's
2766            // registered handlers (xmlRelaxNGSetValidErrors /
2767            // xmlRelaxNGSetValidStructuredErrors) so consumers like lxml's
2768            // RelaxNG.validate (which installs serror = _receiveError)
2769            // populate their error_log.
2770            crate::abi::exports_relaxng::dispatch_relaxng_valid_errors(
2771                ctxt as usize,
2772                &valid_ctxt.errors,
2773                ptr::null_mut(),
2774            );
2775            temp_ctxt.nb_errors
2776        }
2777    }
2778}
2779
2780/// Validate a full element against a RELAX NG schema.
2781///
2782/// # UPSTREAM-PARITY
2783///
2784/// ```c
2785/// int xmlRelaxNGValidateFullElement(xmlRelaxNGValidCtxtPtr ctxt,
2786///                                    xmlDocPtr doc,
2787///                                    xmlNodePtr elem);
2788/// ```
2789///
2790/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
2791///
2792/// # SAFETY
2793///
2794/// - `ctxt` must be a valid pointer to a validation context, or NULL.
2795/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
2796/// - `elem` must be a valid pointer to an element node, or NULL.
2797#[no_mangle]
2798pub unsafe extern "C" fn xmlRelaxNGValidateFullElement(
2799    ctxt: *mut c_void,
2800    doc: *mut _xmlDoc,
2801    elem: *mut _xmlNode,
2802) -> c_int {
2803    if ctxt.is_null() || doc.is_null() || elem.is_null() {
2804        return -1;
2805    }
2806
2807    unsafe {
2808        let valid_ctxt = &mut *(ctxt as *mut RelaxNgValidCtxt);
2809        let schema = match &valid_ctxt.schema {
2810            Some(s) => s,
2811            None => return -1,
2812        };
2813
2814        let mut temp_ctxt = RelaxNgValidCtxt::new();
2815        temp_ctxt.path = valid_ctxt.path.clone();
2816
2817        let start_pattern = match &schema.grammar.start {
2818            Some(p) => p,
2819            None => return -1,
2820        };
2821
2822        let valid = rng_validate_pattern(start_pattern, elem, schema, &mut temp_ctxt);
2823
2824        if valid {
2825            0
2826        } else {
2827            valid_ctxt.errors = temp_ctxt.errors;
2828            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
2829            temp_ctxt.nb_errors
2830        }
2831    }
2832}
2833
2834// ═══════════════════════════════════════════════════════════════════════════════
2835// Tests
2836// ═══════════════════════════════════════════════════════════════════════════════
2837
2838#[cfg(test)]
2839mod tests {
2840    use super::*;
2841
2842    // ── Name Class Tests ───────────────────────────────────────────────────
2843
2844    #[test]
2845    fn test_name_class_name() {
2846        let nc = RelaxNgNameClass::Name("foo".to_string());
2847        assert!(nc.matches("foo", None));
2848        assert!(!nc.matches("bar", None));
2849        assert!(!nc.matches("FOO", None));
2850    }
2851
2852    #[test]
2853    fn test_name_class_any_name() {
2854        let nc = RelaxNgNameClass::AnyName;
2855        assert!(nc.matches("foo", None));
2856        assert!(nc.matches("bar", None));
2857        assert!(nc.matches("anything", Some("urn:ns")));
2858    }
2859
2860    #[test]
2861    fn test_name_class_ns_name() {
2862        let nc = RelaxNgNameClass::NsName("urn:example".to_string());
2863        assert!(nc.matches("foo", Some("urn:example")));
2864        assert!(!nc.matches("foo", Some("urn:other")));
2865        assert!(!nc.matches("foo", None));
2866    }
2867
2868    #[test]
2869    fn test_name_class_choice() {
2870        let nc = RelaxNgNameClass::Choice(vec![
2871            RelaxNgNameClass::Name("a".to_string()),
2872            RelaxNgNameClass::Name("b".to_string()),
2873        ]);
2874        assert!(nc.matches("a", None));
2875        assert!(nc.matches("b", None));
2876        assert!(!nc.matches("c", None));
2877    }
2878
2879    #[test]
2880    fn test_name_class_except() {
2881        let nc = RelaxNgNameClass::Except(
2882            Box::new(RelaxNgNameClass::AnyName),
2883            Box::new(RelaxNgNameClass::Name("bad".to_string())),
2884        );
2885        assert!(nc.matches("good", None));
2886        assert!(!nc.matches("bad", None));
2887    }
2888
2889    // ── Schema Parsing Tests ──────────────────────────────────────────────
2890
2891    #[test]
2892    fn test_parse_simple_element_schema() {
2893        let schema_xml = r#"<?xml version="1.0"?>
2894<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2895  <text/>
2896</element>"#;
2897
2898        let result = rng_parse(schema_xml);
2899        assert!(result.is_ok(), "Failed to parse schema: {:?}", result.err());
2900        let schema = result.unwrap();
2901        assert!(schema.grammar.start.is_some());
2902        if let Some(ref start) = schema.grammar.start {
2903            assert_eq!(start.pattern_type, RelaxNgPatternType::Element);
2904            assert_eq!(start.name.as_deref(), Some("root"));
2905        }
2906    }
2907
2908    #[test]
2909    fn test_parse_grammar_schema() {
2910        let schema_xml = r#"<?xml version="1.0"?>
2911<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2912  <start>
2913    <element name="root">
2914      <text/>
2915    </element>
2916  </start>
2917</grammar>"#;
2918
2919        let result = rng_parse(schema_xml);
2920        assert!(
2921            result.is_ok(),
2922            "Failed to parse grammar: {:?}",
2923            result.err()
2924        );
2925        let schema = result.unwrap();
2926        assert!(schema.grammar.start.is_some());
2927    }
2928
2929    #[test]
2930    fn test_parse_with_define_and_ref() {
2931        let schema_xml = r#"<?xml version="1.0"?>
2932<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2933  <define name="textBlock">
2934    <text/>
2935  </define>
2936  <start>
2937    <element name="doc">
2938      <ref name="textBlock"/>
2939    </element>
2940  </start>
2941</grammar>"#;
2942
2943        let result = rng_parse(schema_xml);
2944        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
2945        let schema = result.unwrap();
2946        assert_eq!(schema.grammar.defines.len(), 1);
2947        assert_eq!(schema.grammar.defines[0].name, "textBlock");
2948        assert!(schema.grammar.start.is_some());
2949    }
2950
2951    #[test]
2952    fn test_parse_choice_schema() {
2953        let schema_xml = r#"<?xml version="1.0"?>
2954<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2955  <start>
2956    <choice>
2957      <element name="a">
2958        <text/>
2959      </element>
2960      <element name="b">
2961        <text/>
2962      </element>
2963    </choice>
2964  </start>
2965</grammar>"#;
2966
2967        let result = rng_parse(schema_xml);
2968        assert!(result.is_ok(), "Failed to parse choice: {:?}", result.err());
2969    }
2970
2971    #[test]
2972    fn test_parse_attribute_schema() {
2973        let schema_xml = r#"<?xml version="1.0"?>
2974<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
2975  <attribute name="attr1">
2976    <text/>
2977  </attribute>
2978  <text/>
2979</element>"#;
2980
2981        let result = rng_parse(schema_xml);
2982        assert!(
2983            result.is_ok(),
2984            "Failed to parse attribute: {:?}",
2985            result.err()
2986        );
2987    }
2988
2989    #[test]
2990    fn test_parse_empty_document_fails() {
2991        let result = rng_parse("");
2992        assert!(result.is_err());
2993    }
2994
2995    #[test]
2996    fn test_parse_invalid_xml_fails() {
2997        let result = rng_parse("not valid xml <<<");
2998        assert!(result.is_err());
2999    }
3000
3001    // ── Validation Tests ──────────────────────────────────────────────────
3002
3003    /// Validates a document against a schema with a single `element` pattern.
3004    ///
3005    /// # Safety
3006    ///
3007    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3008    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3009    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3010    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3011    #[test]
3012    fn test_validate_simple_element() {
3013        let schema_xml = r#"<?xml version="1.0"?>
3014<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3015  <text/>
3016</element>"#;
3017
3018        let doc_xml = r#"<?xml version="1.0"?>
3019<root>Hello</root>"#;
3020
3021        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3022
3023        let doc = unsafe {
3024            crate::abi::exports_xml2::xmlReadMemory(
3025                doc_xml.as_ptr() as *const c_char,
3026                doc_xml.len() as c_int,
3027                c"test.xml".as_ptr() as *const c_char,
3028                ptr::null(),
3029                0,
3030            )
3031        };
3032        assert!(!doc.is_null(), "Failed to parse document");
3033
3034        let mut ctxt = RelaxNgValidCtxt::new();
3035        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3036        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3037
3038        assert!(valid, "Validation failed: {:?}", ctxt.errors);
3039    }
3040
3041    /// Verifies validation fails when the root element name does not match.
3042    ///
3043    /// # Safety
3044    ///
3045    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3046    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3047    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3048    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3049    #[test]
3050    fn test_validate_element_mismatch() {
3051        let schema_xml = r#"<?xml version="1.0"?>
3052<element name="expected" xmlns="http://relaxng.org/ns/structure/1.0">
3053  <text/>
3054</element>"#;
3055
3056        let doc_xml = r#"<?xml version="1.0"?>
3057<actual>Content</actual>"#;
3058
3059        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3060
3061        let doc = unsafe {
3062            crate::abi::exports_xml2::xmlReadMemory(
3063                doc_xml.as_ptr() as *const c_char,
3064                doc_xml.len() as c_int,
3065                c"test.xml".as_ptr() as *const c_char,
3066                ptr::null(),
3067                0,
3068            )
3069        };
3070        assert!(!doc.is_null());
3071
3072        let mut ctxt = RelaxNgValidCtxt::new();
3073        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3074        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3075
3076        assert!(!valid, "Validation should have failed");
3077        assert!(ctxt.nb_errors > 0);
3078    }
3079
3080    /// Validates a document whose element carries a declared attribute.
3081    ///
3082    /// # Safety
3083    ///
3084    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3085    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3086    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3087    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3088    #[test]
3089    fn test_validate_with_attribute() {
3090        let schema_xml = r#"<?xml version="1.0"?>
3091<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3092  <attribute name="id">
3093    <text/>
3094  </attribute>
3095  <text/>
3096</element>"#;
3097
3098        let doc_xml = r#"<?xml version="1.0"?>
3099<root id="x1">Content</root>"#;
3100
3101        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3102
3103        let doc = unsafe {
3104            crate::abi::exports_xml2::xmlReadMemory(
3105                doc_xml.as_ptr() as *const c_char,
3106                doc_xml.len() as c_int,
3107                c"test.xml".as_ptr() as *const c_char,
3108                ptr::null(),
3109                0,
3110            )
3111        };
3112        assert!(!doc.is_null());
3113
3114        let mut ctxt = RelaxNgValidCtxt::new();
3115        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3116        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3117
3118        assert!(valid, "Validation failed: {:?}", ctxt.errors);
3119    }
3120
3121    /// Verifies validation fails when a required attribute is absent.
3122    ///
3123    /// # Safety
3124    ///
3125    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3126    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3127    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3128    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3129    #[test]
3130    fn test_validate_missing_attribute() {
3131        let schema_xml = r#"<?xml version="1.0"?>
3132<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3133  <attribute name="required">
3134    <text/>
3135  </attribute>
3136  <text/>
3137</element>"#;
3138
3139        let doc_xml = r#"<?xml version="1.0"?>
3140<root>Content</root>"#;
3141
3142        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3143
3144        let doc = unsafe {
3145            crate::abi::exports_xml2::xmlReadMemory(
3146                doc_xml.as_ptr() as *const c_char,
3147                doc_xml.len() as c_int,
3148                c"test.xml".as_ptr() as *const c_char,
3149                ptr::null(),
3150                0,
3151            )
3152        };
3153        assert!(!doc.is_null());
3154
3155        let mut ctxt = RelaxNgValidCtxt::new();
3156        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3157        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3158
3159        assert!(
3160            !valid,
3161            "Validation should have failed for missing attribute"
3162        );
3163    }
3164
3165    /// Validates a document matching one branch of a `choice` pattern.
3166    ///
3167    /// # Safety
3168    ///
3169    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3170    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3171    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3172    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3173    #[test]
3174    fn test_validate_with_choice() {
3175        let schema_xml = r#"<?xml version="1.0"?>
3176<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3177  <start>
3178    <choice>
3179      <element name="a">
3180        <text/>
3181      </element>
3182      <element name="b">
3183        <text/>
3184      </element>
3185    </choice>
3186  </start>
3187</grammar>"#;
3188
3189        let doc_xml = r#"<?xml version="1.0"?>
3190<a>First choice</a>"#;
3191
3192        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3193
3194        let doc = unsafe {
3195            crate::abi::exports_xml2::xmlReadMemory(
3196                doc_xml.as_ptr() as *const c_char,
3197                doc_xml.len() as c_int,
3198                c"test.xml".as_ptr() as *const c_char,
3199                ptr::null(),
3200                0,
3201            )
3202        };
3203        assert!(!doc.is_null());
3204
3205        let mut ctxt = RelaxNgValidCtxt::new();
3206        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3207        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3208
3209        assert!(valid, "Choice validation failed: {:?}", ctxt.errors);
3210    }
3211
3212    /// Verifies validation fails when no `choice` branch matches.
3213    ///
3214    /// # Safety
3215    ///
3216    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3217    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3218    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3219    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3220    #[test]
3221    fn test_validate_choice_no_match() {
3222        let schema_xml = r#"<?xml version="1.0"?>
3223<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3224  <start>
3225    <choice>
3226      <element name="a">
3227        <text/>
3228      </element>
3229      <element name="b">
3230        <text/>
3231      </element>
3232    </choice>
3233  </start>
3234</grammar>"#;
3235
3236        let doc_xml = r#"<?xml version="1.0"?>
3237<c>Neither choice</c>"#;
3238
3239        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3240
3241        let doc = unsafe {
3242            crate::abi::exports_xml2::xmlReadMemory(
3243                doc_xml.as_ptr() as *const c_char,
3244                doc_xml.len() as c_int,
3245                c"test.xml".as_ptr() as *const c_char,
3246                ptr::null(),
3247                0,
3248            )
3249        };
3250        assert!(!doc.is_null());
3251
3252        let mut ctxt = RelaxNgValidCtxt::new();
3253        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3254        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3255
3256        assert!(
3257            !valid,
3258            "Validation should have failed for no matching choice"
3259        );
3260    }
3261
3262    /// Validates a grammar that uses a named `ref` to a `define`.
3263    ///
3264    /// # Safety
3265    ///
3266    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3267    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3268    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3269    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3270    #[test]
3271    fn test_validate_grammar_with_ref() {
3272        let schema_xml = r#"<?xml version="1.0"?>
3273<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3274  <define name="para">
3275    <element name="p">
3276      <text/>
3277    </element>
3278  </define>
3279  <start>
3280    <element name="doc">
3281      <zeroOrMore>
3282        <ref name="para"/>
3283      </zeroOrMore>
3284    </element>
3285  </start>
3286</grammar>"#;
3287
3288        let doc_xml = r#"<?xml version="1.0"?>
3289<doc><p>First</p><p>Second</p></doc>"#;
3290
3291        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3292
3293        let doc = unsafe {
3294            crate::abi::exports_xml2::xmlReadMemory(
3295                doc_xml.as_ptr() as *const c_char,
3296                doc_xml.len() as c_int,
3297                c"test.xml".as_ptr() as *const c_char,
3298                ptr::null(),
3299                0,
3300            )
3301        };
3302        assert!(!doc.is_null());
3303
3304        let mut ctxt = RelaxNgValidCtxt::new();
3305        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3306        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3307
3308        assert!(valid, "Ref validation failed: {:?}", ctxt.errors);
3309    }
3310
3311    /// Validates repeated children under a `zeroOrMore` pattern.
3312    ///
3313    /// # Safety
3314    ///
3315    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3316    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3317    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3318    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3319    #[test]
3320    fn test_validate_zero_or_more() {
3321        let schema_xml = r#"<?xml version="1.0"?>
3322<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3323  <zeroOrMore>
3324    <element name="item">
3325      <text/>
3326    </element>
3327  </zeroOrMore>
3328</element>"#;
3329
3330        let doc_xml = r#"<?xml version="1.0"?>
3331<root><item>A</item><item>B</item></root>"#;
3332
3333        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3334
3335        let doc = unsafe {
3336            crate::abi::exports_xml2::xmlReadMemory(
3337                doc_xml.as_ptr() as *const c_char,
3338                doc_xml.len() as c_int,
3339                c"test.xml".as_ptr() as *const c_char,
3340                ptr::null(),
3341                0,
3342            )
3343        };
3344        assert!(!doc.is_null());
3345
3346        let mut ctxt = RelaxNgValidCtxt::new();
3347        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3348        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3349
3350        assert!(valid, "zeroOrMore validation failed: {:?}", ctxt.errors);
3351    }
3352
3353    /// Verifies a `zeroOrMore` pattern accepts zero repetitions.
3354    ///
3355    /// # Safety
3356    ///
3357    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3358    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3359    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3360    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3361    #[test]
3362    fn test_validate_zero_or_more_empty() {
3363        let schema_xml = r#"<?xml version="1.0"?>
3364<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3365  <zeroOrMore>
3366    <element name="item">
3367      <text/>
3368    </element>
3369  </zeroOrMore>
3370</element>"#;
3371
3372        let doc_xml = r#"<?xml version="1.0"?>
3373<root></root>"#;
3374
3375        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3376
3377        let doc = unsafe {
3378            crate::abi::exports_xml2::xmlReadMemory(
3379                doc_xml.as_ptr() as *const c_char,
3380                doc_xml.len() as c_int,
3381                c"test.xml".as_ptr() as *const c_char,
3382                ptr::null(),
3383                0,
3384            )
3385        };
3386        assert!(!doc.is_null());
3387
3388        let mut ctxt = RelaxNgValidCtxt::new();
3389        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3390        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3391
3392        assert!(valid, "Empty zeroOrMore should be valid");
3393    }
3394
3395    /// Validates a document with one occurrence under `oneOrMore`.
3396    ///
3397    /// # Safety
3398    ///
3399    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3400    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3401    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3402    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3403    #[test]
3404    fn test_validate_one_or_more() {
3405        let schema_xml = r#"<?xml version="1.0"?>
3406<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3407  <oneOrMore>
3408    <element name="item">
3409      <text/>
3410    </element>
3411  </oneOrMore>
3412</element>"#;
3413
3414        let doc_xml = r#"<?xml version="1.0"?>
3415<root><item>Single</item></root>"#;
3416
3417        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3418
3419        let doc = unsafe {
3420            crate::abi::exports_xml2::xmlReadMemory(
3421                doc_xml.as_ptr() as *const c_char,
3422                doc_xml.len() as c_int,
3423                c"test.xml".as_ptr() as *const c_char,
3424                ptr::null(),
3425                0,
3426            )
3427        };
3428        assert!(!doc.is_null());
3429
3430        let mut ctxt = RelaxNgValidCtxt::new();
3431        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3432        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3433
3434        assert!(valid, "oneOrMore validation failed: {:?}", ctxt.errors);
3435    }
3436
3437    /// Validates an `optional` element that is present.
3438    ///
3439    /// # Safety
3440    ///
3441    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3442    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3443    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3444    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3445    #[test]
3446    fn test_validate_optional_present() {
3447        let schema_xml = r#"<?xml version="1.0"?>
3448<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3449  <optional>
3450    <element name="opt">
3451      <text/>
3452    </element>
3453  </optional>
3454  <text/>
3455</element>"#;
3456
3457        let doc_xml = r#"<?xml version="1.0"?>
3458<root><opt>present</opt>text</root>"#;
3459
3460        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3461
3462        let doc = unsafe {
3463            crate::abi::exports_xml2::xmlReadMemory(
3464                doc_xml.as_ptr() as *const c_char,
3465                doc_xml.len() as c_int,
3466                c"test.xml".as_ptr() as *const c_char,
3467                ptr::null(),
3468                0,
3469            )
3470        };
3471        assert!(!doc.is_null());
3472
3473        let mut ctxt = RelaxNgValidCtxt::new();
3474        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3475        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3476
3477        assert!(
3478            valid,
3479            "Optional present validation failed: {:?}",
3480            ctxt.errors
3481        );
3482    }
3483
3484    /// Validates an `optional` element that is absent.
3485    ///
3486    /// # Safety
3487    ///
3488    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3489    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3490    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3491    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3492    #[test]
3493    fn test_validate_optional_absent() {
3494        let schema_xml = r#"<?xml version="1.0"?>
3495<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3496  <optional>
3497    <element name="opt">
3498      <text/>
3499    </element>
3500  </optional>
3501  <text/>
3502</element>"#;
3503
3504        let doc_xml = r#"<?xml version="1.0"?>
3505<root>text only</root>"#;
3506
3507        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3508
3509        let doc = unsafe {
3510            crate::abi::exports_xml2::xmlReadMemory(
3511                doc_xml.as_ptr() as *const c_char,
3512                doc_xml.len() as c_int,
3513                c"test.xml".as_ptr() as *const c_char,
3514                ptr::null(),
3515                0,
3516            )
3517        };
3518        assert!(!doc.is_null());
3519
3520        let mut ctxt = RelaxNgValidCtxt::new();
3521        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3522        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3523
3524        assert!(
3525            valid,
3526            "Optional absent validation failed: {:?}",
3527            ctxt.errors
3528        );
3529    }
3530
3531    /// Validates children in the declared `sequence` order.
3532    ///
3533    /// # Safety
3534    ///
3535    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3536    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3537    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3538    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3539    #[test]
3540    fn test_validate_sequence() {
3541        let schema_xml = r#"<?xml version="1.0"?>
3542<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3543  <start>
3544    <element name="root">
3545      <sequence>
3546        <element name="first">
3547          <text/>
3548        </element>
3549        <element name="second">
3550          <text/>
3551        </element>
3552      </sequence>
3553    </element>
3554  </start>
3555</grammar>"#;
3556
3557        let doc_xml = r#"<?xml version="1.0"?>
3558<root><first>First</first><second>Second</second></root>"#;
3559
3560        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3561
3562        let doc = unsafe {
3563            crate::abi::exports_xml2::xmlReadMemory(
3564                doc_xml.as_ptr() as *const c_char,
3565                doc_xml.len() as c_int,
3566                c"test.xml".as_ptr() as *const c_char,
3567                ptr::null(),
3568                0,
3569            )
3570        };
3571        assert!(!doc.is_null());
3572
3573        let mut ctxt = RelaxNgValidCtxt::new();
3574        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3575        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3576
3577        assert!(valid, "Sequence validation failed: {:?}", ctxt.errors);
3578    }
3579
3580    /// Validates a `data` pattern with an integer type.
3581    ///
3582    /// # Safety
3583    ///
3584    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3585    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3586    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3587    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3588    #[test]
3589    fn test_validate_data_pattern() {
3590        let schema_xml = r#"<?xml version="1.0"?>
3591<element name="age" xmlns="http://relaxng.org/ns/structure/1.0">
3592  <data type="integer"/>
3593</element>"#;
3594
3595        let doc_xml = r#"<?xml version="1.0"?>
3596<age>25</age>"#;
3597
3598        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3599
3600        let doc = unsafe {
3601            crate::abi::exports_xml2::xmlReadMemory(
3602                doc_xml.as_ptr() as *const c_char,
3603                doc_xml.len() as c_int,
3604                c"test.xml".as_ptr() as *const c_char,
3605                ptr::null(),
3606                0,
3607            )
3608        };
3609        assert!(!doc.is_null());
3610
3611        let mut ctxt = RelaxNgValidCtxt::new();
3612        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3613        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3614
3615        assert!(valid, "Data pattern validation failed: {:?}", ctxt.errors);
3616    }
3617
3618    /// Verifies a non-integer value fails a `data` pattern.
3619    ///
3620    /// # Safety
3621    ///
3622    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3623    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3624    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3625    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3626    #[test]
3627    fn test_validate_data_pattern_invalid() {
3628        let schema_xml = r#"<?xml version="1.0"?>
3629<element name="age" xmlns="http://relaxng.org/ns/structure/1.0">
3630  <data type="integer"/>
3631</element>"#;
3632
3633        let doc_xml = r#"<?xml version="1.0"?>
3634<age>not-a-number</age>"#;
3635
3636        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3637
3638        let doc = unsafe {
3639            crate::abi::exports_xml2::xmlReadMemory(
3640                doc_xml.as_ptr() as *const c_char,
3641                doc_xml.len() as c_int,
3642                c"test.xml".as_ptr() as *const c_char,
3643                ptr::null(),
3644                0,
3645            )
3646        };
3647        assert!(!doc.is_null());
3648
3649        let mut ctxt = RelaxNgValidCtxt::new();
3650        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3651        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3652
3653        assert!(!valid, "Validation should have failed for invalid integer");
3654    }
3655
3656    /// Validates a `value` pattern with an exact match.
3657    ///
3658    /// # Safety
3659    ///
3660    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3661    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3662    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3663    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3664    #[test]
3665    fn test_validate_value_pattern() {
3666        let schema_xml = r#"<?xml version="1.0"?>
3667<element name="status" xmlns="http://relaxng.org/ns/structure/1.0">
3668  <value>active</value>
3669</element>"#;
3670
3671        let doc_xml = r#"<?xml version="1.0"?>
3672<status>active</status>"#;
3673
3674        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3675
3676        let doc = unsafe {
3677            crate::abi::exports_xml2::xmlReadMemory(
3678                doc_xml.as_ptr() as *const c_char,
3679                doc_xml.len() as c_int,
3680                c"test.xml".as_ptr() as *const c_char,
3681                ptr::null(),
3682                0,
3683            )
3684        };
3685        assert!(!doc.is_null());
3686
3687        let mut ctxt = RelaxNgValidCtxt::new();
3688        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3689        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3690
3691        assert!(valid, "Value pattern validation failed: {:?}", ctxt.errors);
3692    }
3693
3694    /// Verifies a `value` pattern mismatch fails validation.
3695    ///
3696    /// # Safety
3697    ///
3698    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3699    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3700    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3701    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3702    #[test]
3703    fn test_validate_value_pattern_mismatch() {
3704        let schema_xml = r#"<?xml version="1.0"?>
3705<element name="status" xmlns="http://relaxng.org/ns/structure/1.0">
3706  <value>active</value>
3707</element>"#;
3708
3709        let doc_xml = r#"<?xml version="1.0"?>
3710<status>inactive</status>"#;
3711
3712        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3713
3714        let doc = unsafe {
3715            crate::abi::exports_xml2::xmlReadMemory(
3716                doc_xml.as_ptr() as *const c_char,
3717                doc_xml.len() as c_int,
3718                c"test.xml".as_ptr() as *const c_char,
3719                ptr::null(),
3720                0,
3721            )
3722        };
3723        assert!(!doc.is_null());
3724
3725        let mut ctxt = RelaxNgValidCtxt::new();
3726        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3727        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3728
3729        assert!(!valid, "Validation should have failed for value mismatch");
3730    }
3731
3732    /// Verifies a `notAllowed` pattern always fails validation.
3733    ///
3734    /// # Safety
3735    ///
3736    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3737    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3738    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3739    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3740    #[test]
3741    fn test_validate_not_allowed() {
3742        let schema_xml = r#"<?xml version="1.0"?>
3743<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3744  <notAllowed/>
3745</element>"#;
3746
3747        let doc_xml = r#"<?xml version="1.0"?>
3748<root>should not be allowed</root>"#;
3749
3750        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3751
3752        let doc = unsafe {
3753            crate::abi::exports_xml2::xmlReadMemory(
3754                doc_xml.as_ptr() as *const c_char,
3755                doc_xml.len() as c_int,
3756                c"test.xml".as_ptr() as *const c_char,
3757                ptr::null(),
3758                0,
3759            )
3760        };
3761        assert!(!doc.is_null());
3762
3763        let mut ctxt = RelaxNgValidCtxt::new();
3764        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3765        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3766
3767        assert!(!valid, "notAllowed should cause validation failure");
3768    }
3769
3770    /// Validates children in any order under an `interleave` pattern.
3771    ///
3772    /// # Safety
3773    ///
3774    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3775    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3776    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3777    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3778    #[test]
3779    fn test_validate_interleave() {
3780        let schema_xml = r#"<?xml version="1.0"?>
3781<grammar xmlns="http://relaxng.org/ns/structure/1.0">
3782  <start>
3783    <element name="root">
3784      <interleave>
3785        <element name="a">
3786          <text/>
3787        </element>
3788        <element name="b">
3789          <text/>
3790        </element>
3791      </interleave>
3792    </element>
3793  </start>
3794</grammar>"#;
3795
3796        let doc_xml = r#"<?xml version="1.0"?>
3797<root><a>A</a><b>B</b></root>"#;
3798
3799        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3800
3801        let doc = unsafe {
3802            crate::abi::exports_xml2::xmlReadMemory(
3803                doc_xml.as_ptr() as *const c_char,
3804                doc_xml.len() as c_int,
3805                c"test.xml".as_ptr() as *const c_char,
3806                ptr::null(),
3807                0,
3808            )
3809        };
3810        assert!(!doc.is_null());
3811
3812        let mut ctxt = RelaxNgValidCtxt::new();
3813        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3814        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3815
3816        assert!(valid, "Interleave validation failed: {:?}", ctxt.errors);
3817    }
3818
3819    /// Validates an empty element against an `empty` pattern.
3820    ///
3821    /// # Safety
3822    ///
3823    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
3824    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
3825    ///   the pointer is asserted non-NULL before `rng_validate_doc`
3826    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
3827    #[test]
3828    fn test_validate_empty_element() {
3829        let schema_xml = r#"<?xml version="1.0"?>
3830<element name="br" xmlns="http://relaxng.org/ns/structure/1.0">
3831  <empty/>
3832</element>"#;
3833
3834        let doc_xml = r#"<?xml version="1.0"?>
3835<br/>"#;
3836
3837        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
3838
3839        let doc = unsafe {
3840            crate::abi::exports_xml2::xmlReadMemory(
3841                doc_xml.as_ptr() as *const c_char,
3842                doc_xml.len() as c_int,
3843                c"test.xml".as_ptr() as *const c_char,
3844                ptr::null(),
3845                0,
3846            )
3847        };
3848        assert!(!doc.is_null());
3849
3850        let mut ctxt = RelaxNgValidCtxt::new();
3851        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
3852        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
3853
3854        assert!(valid, "Empty element validation failed: {:?}", ctxt.errors);
3855    }
3856
3857    // ── Datatype Validation Tests ──────────────────────────────────────────
3858
3859    #[test]
3860    fn test_validate_datatype_string() {
3861        assert!(rng_validate_datatype_value(Some("string"), "hello"));
3862        assert!(rng_validate_datatype_value(Some("string"), ""));
3863    }
3864
3865    #[test]
3866    fn test_validate_datatype_boolean() {
3867        assert!(rng_validate_datatype_value(Some("boolean"), "true"));
3868        assert!(rng_validate_datatype_value(Some("boolean"), "false"));
3869        assert!(rng_validate_datatype_value(Some("boolean"), "1"));
3870        assert!(rng_validate_datatype_value(Some("boolean"), "0"));
3871        assert!(!rng_validate_datatype_value(Some("boolean"), "yes"));
3872        assert!(!rng_validate_datatype_value(Some("boolean"), "no"));
3873    }
3874
3875    #[test]
3876    fn test_validate_datatype_integer() {
3877        assert!(rng_validate_datatype_value(Some("integer"), "42"));
3878        assert!(rng_validate_datatype_value(Some("integer"), "-42"));
3879        assert!(rng_validate_datatype_value(Some("integer"), "+42"));
3880        assert!(!rng_validate_datatype_value(Some("integer"), "12.5"));
3881        assert!(!rng_validate_datatype_value(Some("integer"), "abc"));
3882        assert!(!rng_validate_datatype_value(Some("integer"), ""));
3883    }
3884
3885    #[test]
3886    fn test_validate_datatype_decimal() {
3887        assert!(rng_validate_datatype_value(Some("decimal"), "42"));
3888        assert!(rng_validate_datatype_value(Some("decimal"), "12.5"));
3889        assert!(rng_validate_datatype_value(Some("decimal"), "-3.14"));
3890        assert!(!rng_validate_datatype_value(Some("decimal"), ""));
3891    }
3892
3893    #[test]
3894    fn test_validate_datatype_float() {
3895        assert!(rng_validate_datatype_value(Some("float"), "3.14"));
3896        assert!(rng_validate_datatype_value(Some("float"), "INF"));
3897        assert!(rng_validate_datatype_value(Some("float"), "-INF"));
3898        assert!(rng_validate_datatype_value(Some("float"), "NaN"));
3899        assert!(!rng_validate_datatype_value(Some("float"), ""));
3900    }
3901
3902    #[test]
3903    fn test_validate_datatype_ncname() {
3904        assert!(rng_validate_datatype_value(Some("NCName"), "myElement"));
3905        assert!(rng_validate_datatype_value(Some("NCName"), "_foo"));
3906        assert!(!rng_validate_datatype_value(Some("NCName"), "123abc"));
3907        assert!(!rng_validate_datatype_value(Some("NCName"), ""));
3908    }
3909
3910    #[test]
3911    fn test_validate_datatype_any_uri() {
3912        assert!(rng_validate_datatype_value(
3913            Some("anyURI"),
3914            "http://example.com"
3915        ));
3916        assert!(rng_validate_datatype_value(Some("anyURI"), "urn:isbn:1234"));
3917        assert!(!rng_validate_datatype_value(Some("anyURI"), ""));
3918        assert!(!rng_validate_datatype_value(Some("anyURI"), "has space"));
3919    }
3920
3921    #[test]
3922    fn test_validate_datatype_qname() {
3923        assert!(rng_validate_datatype_value(Some("QName"), "ns:local"));
3924        assert!(rng_validate_datatype_value(Some("QName"), "local"));
3925        assert!(!rng_validate_datatype_value(Some("QName"), ""));
3926    }
3927
3928    // ── C ABI Tests ───────────────────────────────────────────────────────
3929
3930    /// Exercises the C ABI schema parse round trip: `xmlRelaxNGNewMemParserCtxt`,
3931    /// `xmlRelaxNGParse` and `xmlRelaxNGFree`.
3932    ///
3933    /// # Safety
3934    ///
3935    /// - The static `schema_xml` string stays valid for the
3936    ///   `xmlRelaxNGNewMemParserCtxt` call; the parser context and the schema
3937    ///   returned by `xmlRelaxNGParse` are asserted non-NULL before use, and
3938    ///   the schema is freed exactly once with `xmlRelaxNGFree`.
3939    #[test]
3940    fn test_c_abi_new_parse_free() {
3941        let schema_xml = r#"<?xml version="1.0"?>
3942<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3943  <text/>
3944</element>"#;
3945
3946        let ctxt = unsafe {
3947            xmlRelaxNGNewMemParserCtxt(
3948                schema_xml.as_ptr() as *const c_char,
3949                schema_xml.len() as c_int,
3950            )
3951        };
3952        assert!(!ctxt.is_null(), "Parser context should not be null");
3953
3954        let schema = unsafe { xmlRelaxNGParse(ctxt) };
3955        assert!(!schema.is_null(), "Schema should not be null");
3956
3957        // Free the schema
3958        unsafe { xmlRelaxNGFree(schema) };
3959    }
3960
3961    /// Exercises the C ABI document validation path.
3962    ///
3963    /// # Safety
3964    ///
3965    /// - The static schema and document strings stay valid for their
3966    ///   `xmlRelaxNGNewMemParserCtxt` and `xmlReadMemory` calls; the parser
3967    ///   context, schema, validation context and document pointers are
3968    ///   asserted non-NULL before use, and the document, validation context
3969    ///   and schema are each freed exactly once.
3970    #[test]
3971    fn test_c_abi_validate_doc() {
3972        let schema_xml = r#"<?xml version="1.0"?>
3973<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
3974  <text/>
3975</element>"#;
3976
3977        let doc_xml = r#"<?xml version="1.0"?>
3978<root>Hello</root>"#;
3979
3980        let ctxt = unsafe {
3981            xmlRelaxNGNewMemParserCtxt(
3982                schema_xml.as_ptr() as *const c_char,
3983                schema_xml.len() as c_int,
3984            )
3985        };
3986        let schema = unsafe { xmlRelaxNGParse(ctxt) };
3987        assert!(!schema.is_null());
3988
3989        let valid_ctxt = unsafe { xmlRelaxNGNewValidCtxt(schema) };
3990        assert!(!valid_ctxt.is_null());
3991
3992        let doc = unsafe {
3993            crate::abi::exports_xml2::xmlReadMemory(
3994                doc_xml.as_ptr() as *const c_char,
3995                doc_xml.len() as c_int,
3996                c"test.xml".as_ptr() as *const c_char,
3997                ptr::null(),
3998                0,
3999            )
4000        };
4001        assert!(!doc.is_null());
4002
4003        let result = unsafe { xmlRelaxNGValidateDoc(valid_ctxt, doc) };
4004        assert_eq!(result, 0, "Validation should succeed");
4005
4006        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
4007        unsafe { xmlRelaxNGFreeValidCtxt(valid_ctxt) };
4008        unsafe { xmlRelaxNGFree(schema) };
4009    }
4010
4011    /// Exercises the C ABI full-element validation path.
4012    ///
4013    /// # Safety
4014    ///
4015    /// - The static schema and document strings stay valid for their
4016    ///   `xmlRelaxNGNewMemParserCtxt` and `xmlReadMemory` calls; the parser
4017    ///   context, schema, validation context and document pointers are
4018    ///   asserted non-NULL before use.
4019    /// - The document tree walked through `children` and `next` links must be
4020    ///   well-formed and NULL-terminated; the located `item` node is asserted
4021    ///   non-NULL before being passed to `xmlRelaxNGValidateFullElement`.
4022    /// - The document, validation context and schema are each freed exactly
4023    ///   once.
4024    #[test]
4025    fn test_c_abi_validate_full_element() {
4026        let schema_xml = r#"<?xml version="1.0"?>
4027<element name="item" xmlns="http://relaxng.org/ns/structure/1.0">
4028  <text/>
4029</element>"#;
4030
4031        let doc_xml = r#"<?xml version="1.0"?>
4032<root><item>Content</item></root>"#;
4033
4034        let ctxt = unsafe {
4035            xmlRelaxNGNewMemParserCtxt(
4036                schema_xml.as_ptr() as *const c_char,
4037                schema_xml.len() as c_int,
4038            )
4039        };
4040        let schema = unsafe { xmlRelaxNGParse(ctxt) };
4041        assert!(!schema.is_null());
4042
4043        let valid_ctxt = unsafe { xmlRelaxNGNewValidCtxt(schema) };
4044        assert!(!valid_ctxt.is_null());
4045
4046        let doc = unsafe {
4047            crate::abi::exports_xml2::xmlReadMemory(
4048                doc_xml.as_ptr() as *const c_char,
4049                doc_xml.len() as c_int,
4050                c"test.xml".as_ptr() as *const c_char,
4051                ptr::null(),
4052                0,
4053            )
4054        };
4055        assert!(!doc.is_null());
4056
4057        // Find the <item> element
4058        let item = unsafe {
4059            // Start from the first child of the document (root element)
4060            let mut node = (*doc).children;
4061            while !node.is_null() {
4062                if (*node).type_ == XML_ELEMENT_NODE as c_int {
4063                    break;
4064                }
4065                node = (*node).next;
4066            }
4067            if !node.is_null() {
4068                // Now find <item> child of root
4069                node = (*node).children;
4070                while !node.is_null() {
4071                    if (*node).type_ == XML_ELEMENT_NODE as c_int {
4072                        break;
4073                    }
4074                    node = (*node).next;
4075                }
4076            }
4077            node
4078        };
4079        assert!(!item.is_null(), "Should find <item> element");
4080
4081        let result = unsafe { xmlRelaxNGValidateFullElement(valid_ctxt, doc, item) };
4082        assert_eq!(result, 0, "Element validation should succeed");
4083
4084        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
4085        unsafe { xmlRelaxNGFreeValidCtxt(valid_ctxt) };
4086        unsafe { xmlRelaxNGFree(schema) };
4087    }
4088
4089    /// Verifies the C ABI accepts NULL pointers and NULL frees.
4090    ///
4091    /// # Safety
4092    ///
4093    /// - NULL pointers are passed only to C ABI entry points that accept NULL
4094    ///   inputs, and the free functions must tolerate NULL without
4095    ///   dereferencing.
4096    #[test]
4097    fn test_c_abi_null_handling() {
4098        // Test null pointer handling
4099        assert_eq!(
4100            unsafe { xmlRelaxNGValidateDoc(ptr::null_mut(), ptr::null_mut()) },
4101            -1
4102        );
4103        assert_eq!(
4104            unsafe {
4105                xmlRelaxNGValidateFullElement(ptr::null_mut(), ptr::null_mut(), ptr::null_mut())
4106            },
4107            -1
4108        );
4109        assert!(unsafe { xmlRelaxNGNewMemParserCtxt(ptr::null(), 0).is_null() });
4110
4111        // Free with null should not crash
4112        unsafe { xmlRelaxNGFree(ptr::null_mut()) };
4113        unsafe { xmlRelaxNGFreeParserCtxt(ptr::null_mut()) };
4114        unsafe { xmlRelaxNGFreeValidCtxt(ptr::null_mut()) };
4115    }
4116
4117    // ── Edge Case Tests ───────────────────────────────────────────────────
4118
4119    #[test]
4120    fn test_parse_with_div() {
4121        let schema_xml = r#"<?xml version="1.0"?>
4122<grammar xmlns="http://relaxng.org/ns/structure/1.0">
4123  <div>
4124    <define name="shared">
4125      <text/>
4126    </define>
4127  </div>
4128  <start>
4129    <element name="root">
4130      <ref name="shared"/>
4131    </element>
4132  </start>
4133</grammar>"#;
4134
4135        let result = rng_parse(schema_xml);
4136        assert!(
4137            result.is_ok(),
4138            "Failed to parse with div: {:?}",
4139            result.err()
4140        );
4141        let schema = result.unwrap();
4142        assert_eq!(schema.grammar.defines.len(), 1);
4143        assert_eq!(schema.grammar.defines[0].name, "shared");
4144    }
4145
4146    /// Validates a `list` pattern with space-separated tokens.
4147    ///
4148    /// # Safety
4149    ///
4150    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
4151    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
4152    ///   the pointer is asserted non-NULL before `rng_validate_doc`
4153    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
4154    #[test]
4155    fn test_validate_list_pattern() {
4156        let schema_xml = r#"<?xml version="1.0"?>
4157<element name="tokens" xmlns="http://relaxng.org/ns/structure/1.0">
4158  <list>
4159    <data type="token"/>
4160  </list>
4161</element>"#;
4162
4163        let doc_xml = r#"<?xml version="1.0"?>
4164<tokens>abc def ghi</tokens>"#;
4165
4166        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
4167
4168        let doc = unsafe {
4169            crate::abi::exports_xml2::xmlReadMemory(
4170                doc_xml.as_ptr() as *const c_char,
4171                doc_xml.len() as c_int,
4172                c"test.xml".as_ptr() as *const c_char,
4173                ptr::null(),
4174                0,
4175            )
4176        };
4177        assert!(!doc.is_null());
4178
4179        let mut ctxt = RelaxNgValidCtxt::new();
4180        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
4181        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
4182
4183        assert!(valid, "List pattern validation failed: {:?}", ctxt.errors);
4184    }
4185
4186    /// Validates children in order under a `group` pattern.
4187    ///
4188    /// # Safety
4189    ///
4190    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
4191    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
4192    ///   the pointer is asserted non-NULL before `rng_validate_doc`
4193    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
4194    #[test]
4195    fn test_validate_group_pattern() {
4196        let schema_xml = r#"<?xml version="1.0"?>
4197<grammar xmlns="http://relaxng.org/ns/structure/1.0">
4198  <start>
4199    <element name="root">
4200      <group>
4201        <element name="a">
4202          <text/>
4203        </element>
4204        <element name="b">
4205          <text/>
4206        </element>
4207      </group>
4208    </element>
4209  </start>
4210</grammar>"#;
4211
4212        let doc_xml = r#"<?xml version="1.0"?>
4213<root><a>First</a><b>Second</b></root>"#;
4214
4215        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
4216
4217        let doc = unsafe {
4218            crate::abi::exports_xml2::xmlReadMemory(
4219                doc_xml.as_ptr() as *const c_char,
4220                doc_xml.len() as c_int,
4221                c"test.xml".as_ptr() as *const c_char,
4222                ptr::null(),
4223                0,
4224            )
4225        };
4226        assert!(!doc.is_null());
4227
4228        let mut ctxt = RelaxNgValidCtxt::new();
4229        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
4230        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
4231
4232        assert!(valid, "Group validation failed: {:?}", ctxt.errors);
4233    }
4234
4235    /// Verifies validation fails when a `ref` names an undefined define.
4236    ///
4237    /// # Safety
4238    ///
4239    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
4240    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
4241    ///   the pointer is asserted non-NULL before `rng_validate_doc`
4242    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
4243    #[test]
4244    fn test_validate_undefined_ref() {
4245        let schema_xml = r#"<?xml version="1.0"?>
4246<grammar xmlns="http://relaxng.org/ns/structure/1.0">
4247  <start>
4248    <element name="root">
4249      <ref name="undefined"/>
4250    </element>
4251  </start>
4252</grammar>"#;
4253
4254        let doc_xml = r#"<?xml version="1.0"?>
4255<root>Content</root>"#;
4256
4257        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
4258
4259        let doc = unsafe {
4260            crate::abi::exports_xml2::xmlReadMemory(
4261                doc_xml.as_ptr() as *const c_char,
4262                doc_xml.len() as c_int,
4263                c"test.xml".as_ptr() as *const c_char,
4264                ptr::null(),
4265                0,
4266            )
4267        };
4268        assert!(!doc.is_null());
4269
4270        let mut ctxt = RelaxNgValidCtxt::new();
4271        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
4272        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
4273
4274        assert!(!valid, "Undefined ref should cause failure");
4275    }
4276
4277    /// Verifies `rng_validate_doc` rejects a NULL document pointer.
4278    ///
4279    /// # Safety
4280    ///
4281    /// - `rng_validate_doc` is called with a NULL document pointer, which it
4282    ///   must reject without dereferencing.
4283    #[test]
4284    fn test_validate_null_doc() {
4285        let schema = RelaxNgSchema::new();
4286        let mut ctxt = RelaxNgValidCtxt::new();
4287        let valid = unsafe { rng_validate_doc(&schema, ptr::null_mut(), &mut ctxt) };
4288        assert!(!valid);
4289    }
4290
4291    #[test]
4292    fn test_parse_external_ref_schema() {
4293        let schema_xml = r#"<?xml version="1.0"?>
4294<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
4295  <externalRef href="external.rng"/>
4296</element>"#;
4297
4298        let result = rng_parse(schema_xml);
4299        assert!(
4300            result.is_ok(),
4301            "Failed to parse externalRef: {:?}",
4302            result.err()
4303        );
4304        let schema = result.unwrap();
4305        if let Some(ref start) = schema.grammar.start {
4306            assert_eq!(start.pattern_type, RelaxNgPatternType::Element);
4307            assert_eq!(start.name.as_deref(), Some("root"));
4308        }
4309    }
4310
4311    #[test]
4312    fn test_validate_boolean_datatype() {
4313        assert!(rng_validate_datatype_value(Some("boolean"), "true"));
4314        assert!(rng_validate_datatype_value(Some("boolean"), "false"));
4315        assert!(!rng_validate_datatype_value(Some("boolean"), "maybe"));
4316    }
4317
4318    #[test]
4319    fn test_validate_unknown_datatype() {
4320        // Unknown datatypes should be accepted (lenient behavior)
4321        assert!(rng_validate_datatype_value(Some("custom-type"), "anything"));
4322    }
4323
4324    #[test]
4325    fn test_validate_no_datatype() {
4326        // No datatype specified — accept anything
4327        assert!(rng_validate_datatype_value(None, "anything"));
4328    }
4329
4330    #[test]
4331    fn test_parse_schema_with_ns_prefix() {
4332        let schema_xml = r#"<?xml version="1.0"?>
4333<rng:element name="root" xmlns:rng="http://relaxng.org/ns/structure/1.0">
4334  <rng:text/>
4335</rng:element>"#;
4336
4337        let result = rng_parse(schema_xml);
4338        assert!(result.is_ok(), "Failed with ns prefix: {:?}", result.err());
4339    }
4340
4341    #[test]
4342    fn test_validation_context_path() {
4343        let mut ctxt = RelaxNgValidCtxt::new();
4344        assert_eq!(ctxt.current_path(), "/");
4345
4346        ctxt.path.push("root".to_string());
4347        assert_eq!(ctxt.current_path(), "/root");
4348
4349        ctxt.path.push("child".to_string());
4350        assert_eq!(ctxt.current_path(), "/root/child");
4351
4352        ctxt.path.pop();
4353        assert_eq!(ctxt.current_path(), "/root");
4354    }
4355
4356    /// Verifies a `sequence` pattern rejects children in the wrong order.
4357    ///
4358    /// # Safety
4359    ///
4360    /// - The static `doc_xml` string is passed to `xmlReadMemory`, which reads
4361    ///   exactly `doc_xml.len()` bytes and returns an owned `_xmlDoc` or NULL;
4362    ///   the pointer is asserted non-NULL before `rng_validate_doc`
4363    ///   dereferences it and is freed exactly once with `xmlFreeDoc`.
4364    #[test]
4365    fn test_validate_sequence_wrong_order() {
4366        let schema_xml = r#"<?xml version="1.0"?>
4367<grammar xmlns="http://relaxng.org/ns/structure/1.0">
4368  <start>
4369    <element name="root">
4370      <sequence>
4371        <element name="first">
4372          <text/>
4373        </element>
4374        <element name="second">
4375          <text/>
4376        </element>
4377      </sequence>
4378    </element>
4379  </start>
4380</grammar>"#;
4381
4382        let doc_xml = r#"<?xml version="1.0"?>
4383<root><second>Wrong</second><first>Order</first></root>"#;
4384
4385        let schema = rng_parse(schema_xml).expect("Failed to parse schema");
4386
4387        let doc = unsafe {
4388            crate::abi::exports_xml2::xmlReadMemory(
4389                doc_xml.as_ptr() as *const c_char,
4390                doc_xml.len() as c_int,
4391                c"test.xml".as_ptr() as *const c_char,
4392                ptr::null(),
4393                0,
4394            )
4395        };
4396        assert!(!doc.is_null());
4397
4398        let mut ctxt = RelaxNgValidCtxt::new();
4399        let valid = unsafe { rng_validate_doc(&schema, doc, &mut ctxt) };
4400        unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
4401
4402        // The sequence validates patterns against children in order,
4403        // so wrong order should fail
4404        assert!(!valid, "Wrong sequence order should fail");
4405    }
4406}