Skip to main content

libxml_rs/xml/schemas/
mod.rs

1//! XML Schema implementation (§27, §85 Phase 6).
2//!
3//! XML Schema (W3C XSD) validation and datatype machinery. Upstream libxml2
4//! schema support is an UPSTREAM_EXTENSION known to deviate from the standard in
5//! places — parity follows the oracle.
6//!
7//! Phase 6: Complete — schema parsing, datatype validation, document validation,
8//! and C ABI exports are implemented.
9//!
10//! # UPSTREAM-PARITY
11//!
12//! This module implements a simplified but functional XSD validator that
13//! follows upstream libxml2 observable behavior for the most common patterns.
14//! Deviations from the W3C specification that match libxml2 are intentional.
15//!
16//! # Upstream contract
17//!
18//! Mirrors upstream xmlschemas.c, xmlschemastypes.c and xmlschemavalues.c
19//! (SRC-LIBXML2-2.15.0, oracle tree `oracle/historical/src/libxml2-2.15.0/`):
20//! xmlSchema parse/valid contexts, component model, facet validation and the
21//! built-in datatypes. Parity target: the system libxml2 2.15.3 oracle.
22//!
23//! # Conceptual behavior
24//!
25//! XML Schema (W3C XSD) validation and datatype machinery: schema parsing,
26//! component classification, simple/complex type validation, facets and
27//! document validation. Upstream libxml2 schema support is an
28//! UPSTREAM_EXTENSION known to deviate from the standard in places — parity
29//! follows the oracle.
30//!
31//! # Ownership & safety invariants
32//!
33//! Ownership: schemas own their component tree (xmlSchemaFree); parser and
34//! valid contexts own their state (xmlSchemaFreeParserCtxt /
35//! xmlSchemaFreeValidCtxt); the validated document and the schema
36//! import/resource-loading are borrowed through xmlSchemaSetResourceLoader.
37//! SAFETY: facet validation operates on owned string representations, never
38//! on borrowed C buffers beyond the call.
39//!
40//! # Historical quirks & epochs
41//!
42//! XSD support was solidified in the 2.6 validation era (2003-2004,
43//! atlas/HISTORY.md 1.5). R-000124 (11.1-G) closed the header-surface gap so
44//! every public schema header declaration compiles against the DSO;
45//! xmlSchemaFreeWildcard is a safe no-op (the candidate never allocates
46//! wildcard objects; R-000138) and xmlSchemaCleanupTypes is a documented
47//! no-op.
48//!
49//! # Deliberate oddities
50//!
51//! Deliberate oddities: deviations from the W3C XSD specification that match
52//! upstream libxml2 are intentional; the exported entry points
53//! (xmlSchemaNewParserCtxt, xmlSchemaNewMemParserCtxt, xmlSchemaFree,
54//! xmlSchemaValidateDoc, xmlSchemaFreeParserCtxt, xmlSchemaFreeValidCtxt)
55//! follow the upstream signatures.
56//!
57//! # Proving courts
58//!
59//! Exercised by the XSD court family, the header-compile court, the
60//! dso-loader court and `cargo test --lib`. Receipts under
61//! courts/receipts/phase-11.
62//!
63//! # Tempting simplifications that would break parity
64//!
65//! The tempting simplification is a clean-room full XSD 1.0 engine — the
66//! oracle deviations (UPSTREAM_EXTENSION) would not be reproduced and
67//! differential output would diverge. Do not drop the lazy/empty type-cleanup
68//! entry points: they are part of the exported surface (R-000138).
69
70use core::ffi::c_void;
71use core::ptr;
72use std::os::raw::{c_char, c_int};
73
74use crate::abi::structs::*;
75use crate::abi::types::xmlElementType::*;
76
77// ═══════════════════════════════════════════════════════════════════════════════
78// XSD Component Types
79// ═══════════════════════════════════════════════════════════════════════════════
80
81/// XSD component types — mirrors the upstream libxml2 schema component
82/// classification.
83///
84/// # UPSTREAM-PARITY
85///
86/// libxml2 defines these as `xmlSchemaTypeType` in `include/schemas/internals.h`.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum XsdComponentType {
89    /// The `xs:schema` root declaration. Also used as a fallback kind for
90    /// unrecognized elements during parsing.
91    Schema,
92    /// An `xs:element` declaration.
93    Element,
94    /// An `xs:attribute` declaration.
95    Attribute,
96    /// An `xs:complexType` definition (element/attribute content, possibly mixed).
97    ComplexType,
98    /// An `xs:simpleType` definition (a constrained built-in or derived simple type).
99    SimpleType,
100    /// An `xs:simpleContent` model on a complex type (text-only content).
101    SimpleContent,
102    /// An `xs:complexContent` model on a complex type (restriction or extension).
103    ComplexContent,
104    /// An `xs:sequence` model group whose children must appear in order.
105    Sequence,
106    /// An `xs:choice` model group of which exactly one child is allowed.
107    Choice,
108    /// An `xs:all` model group whose children may appear in any order.
109    All,
110    /// An `xs:restriction`, deriving a type by constraining its base type.
111    Restriction,
112    /// An `xs:extension`, deriving a type by adding content to its base type.
113    Extension,
114    /// An `xs:list` simple type (a whitespace-separated list of an item type).
115    List,
116    /// An `xs:union` simple type whose value must match one of its member types.
117    Union,
118    /// An `xs:annotation` (documentation/appinfo). Skipped during schema parsing.
119    Annotation,
120    /// An `xs:any` wildcard that matches any element.
121    Any,
122    /// An `xs:anyAttribute` wildcard that matches any attribute.
123    AnyAttribute,
124    /// An `xs:group` named model group (used either as a definition or a reference).
125    Group,
126    /// An `xs:attributeGroup` named attribute group (definition or reference).
127    AttributeGroup,
128    /// An `xs:notation` declaration binding a notation name to a system/resource.
129    Notation,
130    /// An `xs:unique` identity constraint.
131    Unique,
132    /// An `xs:key` identity constraint.
133    Key,
134    /// An `xs:keyref` identity constraint that references a key or unique constraint.
135    KeyRef,
136    /// An `xs:selector`, the XPath selection of an identity constraint.
137    Selector,
138    /// An `xs:field`, the XPath field of an identity constraint.
139    Field,
140}
141
142// ═══════════════════════════════════════════════════════════════════════════════
143// XSD Datatype Kinds
144// ═══════════════════════════════════════════════════════════════════════════════
145
146/// XSD datatype kinds — covers all built-in types and facets.
147///
148/// # UPSTREAM-PARITY
149///
150/// libxml2 defines these as `xmlSchemaTypeType` built-in type constants
151/// in `include/schemas/internals.h`.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum XsdDatatypeKind {
154    // Primitive types
155    /// XSD built-in type `xs:string` — any sequence of characters.
156    String,
157    /// XSD built-in type `xs:boolean` — lexical forms `true`, `false`, `1`, and `0`.
158    Boolean,
159    /// XSD built-in type `xs:decimal` — an arbitrary-precision decimal number
160    /// (optional sign, digits, optional decimal point).
161    Decimal,
162    /// XSD built-in type `xs:float` — IEEE single-precision floating point,
163    /// including the special values `INF`, `-INF`, and `NaN`.
164    Float,
165    /// XSD built-in type `xs:double` — IEEE double-precision floating point,
166    /// including the special values `INF`, `-INF`, and `NaN`.
167    Double,
168    /// XSD built-in type `xs:duration` — an ISO 8601 duration of the form
169    /// `[-]P[nY][nM][nD][T[nH][nM][nS]]`.
170    Duration,
171    /// XSD built-in type `xs:dateTime` — `YYYY-MM-DDThh:mm:ss[.sss][Z|±hh:mm]`.
172    DateTime,
173    /// XSD built-in type `xs:time` — `hh:mm:ss[.sss][Z|±hh:mm]`.
174    Time,
175    /// XSD built-in type `xs:date` — `YYYY-MM-DD[Z|±hh:mm]`.
176    Date,
177    /// XSD built-in type `xs:gYearMonth` — a Gregorian year and month, `YYYY-MM[Z|±hh:mm]`.
178    GYearMonth,
179    /// XSD built-in type `xs:gYear` — a Gregorian year, `YYYY[Z|±hh:mm]`.
180    GYear,
181    /// XSD built-in type `xs:gMonthDay` — a Gregorian month and day, `--MM-DD[Z|±hh:mm]`.
182    GMonthDay,
183    /// XSD built-in type `xs:gDay` — a Gregorian day of the month, `---DD[Z|±hh:mm]`.
184    GDay,
185    /// XSD built-in type `xs:gMonth` — a Gregorian month, `--MM[Z|±hh:mm]`.
186    GMonth,
187    /// XSD built-in type `xs:hexBinary` — binary data encoded as hexadecimal
188    /// digits (an even number of digits).
189    HexBinary,
190    /// XSD built-in type `xs:base64Binary` — binary data encoded in base64.
191    Base64Binary,
192    /// XSD built-in type `xs:anyURI` — a URI reference (validated here as non-empty).
193    AnyURI,
194    /// XSD built-in type `xs:QName` — a qualified name (`NCName` or `prefix:NCName`).
195    QName,
196    /// XSD built-in type `xs:NOTATION` — a reference to a notation declaration,
197    /// lexically a QName.
198    Notation,
199    // Derived string types
200    /// XSD built-in type `xs:normalizedString` — a `string` with no tabs,
201    /// newlines, or carriage returns.
202    NormalizedString,
203    /// XSD built-in type `xs:token` — a `normalizedString` with no leading or
204    /// trailing whitespace and no consecutive internal whitespace.
205    Token,
206    /// XSD built-in type `xs:language` — a natural language identifier per
207    /// RFC 4646/BCP 47 (simplified here to `[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*`).
208    Language,
209    /// XSD built-in type `xs:NMTOKEN` — a sequence of XML name characters.
210    Nmtoken,
211    /// XSD built-in type `xs:NMTOKENS` — a whitespace-separated list of `NMTOKEN`s.
212    Nmtokens,
213    /// XSD built-in type `xs:Name` — an XML Name.
214    Name,
215    /// XSD built-in type `xs:NCName` — an XML Name without colons.
216    NCName,
217    /// XSD built-in type `xs:ID` — an `NCName` used as a document-wide identifier.
218    Id,
219    /// XSD built-in type `xs:IDREF` — an `NCName` referencing an `ID` value.
220    Idref,
221    /// XSD built-in type `xs:IDREFS` — a whitespace-separated list of `IDREF`s.
222    Idrefs,
223    /// XSD built-in type `xs:ENTITY` — an `NCName` referencing an unparsed entity.
224    Entity,
225    /// XSD built-in type `xs:ENTITIES` — a whitespace-separated list of `ENTITY`s.
226    Entities,
227    // Numeric derived types
228    /// XSD built-in type `xs:integer` — whole numbers (no decimal point).
229    Integer,
230    /// XSD built-in type `xs:nonPositiveInteger` — integers less than or equal to zero.
231    NonPositiveInteger,
232    /// XSD built-in type `xs:negativeInteger` — integers less than zero.
233    NegativeInteger,
234    /// XSD built-in type `xs:long` — integers in the range of a signed 64-bit value.
235    Long,
236    /// XSD built-in type `xs:int` — integers in the range of a signed 32-bit value.
237    Int,
238    /// XSD built-in type `xs:short` — integers in the range of a signed 16-bit value.
239    Short,
240    /// XSD built-in type `xs:byte` — integers in the range of a signed 8-bit value.
241    Byte,
242    /// XSD built-in type `xs:nonNegativeInteger` — integers greater than or equal to zero.
243    NonNegativeInteger,
244    /// XSD built-in type `xs:unsignedLong` — integers in the range of an unsigned 64-bit value.
245    UnsignedLong,
246    /// XSD built-in type `xs:unsignedInt` — integers in the range of an unsigned 32-bit value.
247    UnsignedInt,
248    /// XSD built-in type `xs:unsignedShort` — integers in the range of an unsigned 16-bit value.
249    UnsignedShort,
250    /// XSD built-in type `xs:unsignedByte` — integers in the range of an unsigned 8-bit value.
251    UnsignedByte,
252    /// XSD built-in type `xs:positiveInteger` — integers greater than zero.
253    PositiveInteger,
254    // Facet types (used internally)
255    /// The `pattern` facet — a regular expression the value must match.
256    FacetPattern,
257    /// The `enumeration` facet — the value must equal at least one listed
258    /// literal (OR semantics).
259    FacetEnumeration,
260    /// The `minInclusive` facet — the value must be greater than or equal to the bound.
261    FacetMinInclusive,
262    /// The `maxInclusive` facet — the value must be less than or equal to the bound.
263    FacetMaxInclusive,
264    /// The `minExclusive` facet — the value must be strictly greater than the bound.
265    FacetMinExclusive,
266    /// The `maxExclusive` facet — the value must be strictly less than the bound.
267    FacetMaxExclusive,
268    /// The `minLength` facet — a minimum length in characters.
269    FacetMinLength,
270    /// The `maxLength` facet — a maximum length in characters.
271    FacetMaxLength,
272    /// The `length` facet — an exact length in characters.
273    FacetLength,
274    /// The `whiteSpace` facet — the whitespace normalization policy
275    /// (`preserve`, `replace`, or `collapse`).
276    FacetWhiteSpace,
277    /// The `fractionDigits` facet — the maximum number of digits after the
278    /// decimal point.
279    FacetFractionDigits,
280    /// The `totalDigits` facet — the maximum number of digits in the value.
281    FacetTotalDigits,
282}
283
284// ═══════════════════════════════════════════════════════════════════════════════
285// XSD Component
286// ═══════════════════════════════════════════════════════════════════════════════
287
288/// An XSD schema component declaration.
289///
290/// Represents any XSD component (element, attribute, type, model group, etc.).
291/// Components form a tree via the `children` and `attributes` vectors.
292#[derive(Debug, Clone)]
293pub struct XsdComponent {
294    /// The kind of XSD component this declaration represents.
295    pub component_type: XsdComponentType,
296    /// The component's local name (from the `name` attribute); `None` for
297    /// anonymous components.
298    pub name: Option<String>,
299    /// The namespace the component is declared in.
300    pub target_namespace: Option<String>,
301    /// Child components — model group children, inline type definitions, etc.
302    pub children: Vec<XsdComponent>,
303    /// Attribute declarations belonging to this component (complex types).
304    pub attributes: Vec<XsdComponent>,
305    /// The resolved built-in datatype kind, when the component is typed with a
306    /// built-in XSD type.
307    pub datatype: Option<XsdDatatypeKind>,
308    /// Facets constraining the type, stored as (facet kind, facet value) pairs.
309    pub facets: Vec<(XsdDatatypeKind, String)>,
310    /// The base type of a derived type (restriction/extension/list/union), or
311    /// the unresolved `type` name for elements/attributes with a named type.
312    pub base: Option<String>,
313    /// Minimum occurrence count (default 1; 0 for optional). Also reused for
314    /// the `use` attribute of attribute declarations.
315    pub min_occurs: i32,
316    /// Maximum occurrence count; `-1` for unbounded.
317    pub max_occurs: i32,
318    /// For references: the name of the referenced element/attribute (from the
319    /// `ref` attribute).
320    pub ref_name: Option<String>,
321    /// The name of the substitution group this element belongs to.
322    pub substitution_group: Option<String>,
323    /// Whether the component is declared `abstract` (cannot be used directly).
324    pub is_abstract: bool,
325    /// Whether the component is declared `final` (cannot be derived from).
326    pub is_final: bool,
327    /// The `block` attribute: derivation methods disallowed for the type.
328    pub block: Vec<String>,
329    /// Whether the type allows mixed element content (`xs:complexType mixed`).
330    pub mixed: bool,
331    /// The `form` attribute (`qualified`/`unqualified`) for this component,
332    /// overriding the schema's `elementFormDefault`/`attributeFormDefault`.
333    pub form: Option<String>,
334    /// The `default` value of an element/attribute declaration. Elements with
335    /// a simple type default their text content; attributes that are absent
336    /// from an instance get this value injected under
337    /// `XML_SCHEMA_VAL_VC_I_CREATE` (LIBXML_SCHEMA_CREATE).
338    pub default_value: Option<String>,
339    /// The `fixed` value of an element/attribute declaration (absent
340    /// attributes are created with it; present values must equal it).
341    pub fixed_value: Option<String>,
342}
343
344impl XsdComponent {
345    /// Create a new component with the given type and default field values.
346    ///
347    /// Defaults: `min_occurs = 1`, `max_occurs = 1`, all optional fields `None`.
348    pub const fn new(component_type: XsdComponentType) -> Self {
349        Self {
350            component_type,
351            name: None,
352            target_namespace: None,
353            children: Vec::new(),
354            attributes: Vec::new(),
355            datatype: None,
356            facets: Vec::new(),
357            base: None,
358            min_occurs: 1,
359            max_occurs: 1,
360            ref_name: None,
361            substitution_group: None,
362            is_abstract: false,
363            is_final: false,
364            block: Vec::new(),
365            mixed: false,
366            form: None,
367            default_value: None,
368            fixed_value: None,
369        }
370    }
371}
372
373// ═══════════════════════════════════════════════════════════════════════════════
374// XSD Schema
375// ═══════════════════════════════════════════════════════════════════════════════
376
377/// A compiled XSD schema.
378///
379/// Holds the top-level component declarations and schema-level settings.
380#[derive(Debug, Clone)]
381pub struct XsdSchema {
382    /// Top-level component declarations of the schema.
383    pub components: Vec<XsdComponent>,
384    /// The `targetNamespace` attribute of the schema.
385    pub target_namespace: Option<String>,
386    /// The `elementFormDefault` attribute (`qualified`/`unqualified`).
387    pub element_form_default: Option<String>,
388    /// The `attributeFormDefault` attribute (`qualified`/`unqualified`).
389    pub attribute_form_default: Option<String>,
390    /// Errors collected while parsing or validating against this schema.
391    pub errors: Vec<String>,
392}
393
394impl XsdSchema {
395    /// Create an empty schema with no components, namespace, or errors.
396    pub const fn new() -> Self {
397        Self {
398            components: Vec::new(),
399            target_namespace: None,
400            element_form_default: None,
401            attribute_form_default: None,
402            errors: Vec::new(),
403        }
404    }
405}
406
407impl Default for XsdSchema {
408    fn default() -> Self {
409        Self::new()
410    }
411}
412
413// ═══════════════════════════════════════════════════════════════════════════════
414// XSD Validation Context
415// ═══════════════════════════════════════════════════════════════════════════════
416
417/// Validation context for XSD schema validation.
418///
419/// Tracks errors and state during validation of an XML document against
420/// a schema. Mirrors libxml2's `xmlSchemaValidCtxt`.
421#[derive(Debug)]
422pub struct XsdValidCtxt {
423    /// The schema being validated against.
424    pub schema: Option<XsdSchema>,
425    /// Error messages collected during validation.
426    pub errors: Vec<String>,
427    /// The total number of validation errors recorded.
428    pub nb_errors: i32,
429    /// Whether missing attributes with a schema `default`/`fixed` value are
430    /// injected into the instance (xmlSchemaSetValidOptions with
431    /// XML_SCHEMA_VAL_VC_I_CREATE, php's LIBXML_SCHEMA_CREATE).
432    pub create_defaults: bool,
433}
434
435impl XsdValidCtxt {
436    /// Create a new validation context with no schema bound and no errors.
437    pub const fn new() -> Self {
438        Self {
439            schema: None,
440            errors: Vec::new(),
441            nb_errors: 0,
442            create_defaults: false,
443        }
444    }
445}
446
447impl Default for XsdValidCtxt {
448    fn default() -> Self {
449        Self::new()
450    }
451}
452
453// ═══════════════════════════════════════════════════════════════════════════════
454// Internal helpers for schema parsing
455// ═══════════════════════════════════════════════════════════════════════════════
456
457/// Get the text content of an xmlNode (recursively collects text children).
458///
459/// # SAFETY
460///
461/// - `node` must be a valid pointer to an _xmlNode or NULL.
462unsafe fn get_node_text(node: *mut _xmlNode) -> String {
463    if node.is_null() {
464        return String::new();
465    }
466    let mut result = String::new();
467    unsafe {
468        let mut child = (*node).children;
469        while !child.is_null() {
470            if ((*child).type_ == XML_TEXT_NODE as c_int
471                || (*child).type_ == XML_CDATA_SECTION_NODE as c_int)
472                && !(*child).content.is_null()
473            {
474                let content = (*child).content;
475                let mut len = 0;
476                while *content.add(len) != 0 {
477                    len += 1;
478                }
479                let slice = std::slice::from_raw_parts(content, len);
480                result.push_str(&String::from_utf8_lossy(slice));
481            }
482            child = (*child).next;
483        }
484    }
485    result
486}
487
488/// Get an attribute value from an xmlNode.
489///
490/// # SAFETY
491///
492/// - `node` must be a valid pointer to an _xmlNode or NULL.
493unsafe fn get_attr(node: *mut _xmlNode, name: &str) -> Option<String> {
494    if node.is_null() {
495        return None;
496    }
497    unsafe {
498        let mut prop = (*node).properties;
499        while !prop.is_null() {
500            let prop_name = (*prop).name;
501            if !prop_name.is_null() {
502                let mut len = 0;
503                while *prop_name.add(len) != 0 {
504                    len += 1;
505                }
506                let slice = std::slice::from_raw_parts(prop_name, len);
507                if let Ok(s) = std::str::from_utf8(slice) {
508                    if s == name {
509                        return Some(get_node_text(prop as *mut _xmlNode));
510                    }
511                }
512            }
513            prop = (*prop).next;
514        }
515    }
516    None
517}
518
519/// Get an attribute value as a boolean.
520///
521/// # SAFETY
522///
523/// - `node` must be a valid pointer to an _xmlNode or NULL.
524unsafe fn get_attr_bool(node: *mut _xmlNode, name: &str) -> bool {
525    unsafe {
526        match get_attr(node, name) {
527            Some(v) => v == "true" || v == "1",
528            None => false,
529        }
530    }
531}
532
533/// Get an attribute value as an integer with a default.
534///
535/// # SAFETY
536///
537/// - `node` must be a valid pointer to an _xmlNode or NULL.
538#[allow(dead_code)]
539unsafe fn get_attr_int(node: *mut _xmlNode, name: &str, default: i32) -> i32 {
540    unsafe {
541        match get_attr(node, name) {
542            Some(v) => v.parse::<i32>().unwrap_or(default),
543            None => default,
544        }
545    }
546}
547
548/// Get an attribute value as an unbounded integer (-1 for "unbounded").
549///
550/// # SAFETY
551///
552/// - `node` must be a valid pointer to an _xmlNode or NULL.
553unsafe fn get_attr_occurs(node: *mut _xmlNode, name: &str, default: i32) -> i32 {
554    unsafe {
555        match get_attr(node, name) {
556            Some(v) => {
557                if v == "unbounded" {
558                    -1
559                } else {
560                    v.parse::<i32>().unwrap_or(default)
561                }
562            }
563            None => default,
564        }
565    }
566}
567
568/// Check if an xmlNode is an element with a given local name.
569///
570/// # SAFETY
571///
572/// - `node` must be a valid pointer to an _xmlNode or NULL.
573unsafe fn node_is(node: *mut _xmlNode, local_name: &str) -> bool {
574    if node.is_null() {
575        return false;
576    }
577    unsafe {
578        let name = (*node).name;
579        if name.is_null() {
580            return false;
581        }
582        let mut len = 0;
583        while *name.add(len) != 0 {
584            len += 1;
585        }
586        let slice = std::slice::from_raw_parts(name, len);
587        if let Ok(s) = std::str::from_utf8(slice) {
588            // Strip namespace prefix if present
589            let local = if let Some(pos) = s.find(':') {
590                &s[pos + 1..]
591            } else {
592                s
593            };
594            return local == local_name;
595        }
596    }
597    false
598}
599
600/// Parse a datatype kind from a QName string (e.g., "xs:string", "string").
601fn parse_datatype_kind(name: &str) -> Option<XsdDatatypeKind> {
602    // Strip XML Schema namespace prefix if present
603    let local = if let Some(pos) = name.find(':') {
604        &name[pos + 1..]
605    } else {
606        name
607    };
608
609    match local {
610        "string" => Some(XsdDatatypeKind::String),
611        "boolean" => Some(XsdDatatypeKind::Boolean),
612        "decimal" => Some(XsdDatatypeKind::Decimal),
613        "float" => Some(XsdDatatypeKind::Float),
614        "double" => Some(XsdDatatypeKind::Double),
615        "duration" => Some(XsdDatatypeKind::Duration),
616        "dateTime" => Some(XsdDatatypeKind::DateTime),
617        "time" => Some(XsdDatatypeKind::Time),
618        "date" => Some(XsdDatatypeKind::Date),
619        "gYearMonth" => Some(XsdDatatypeKind::GYearMonth),
620        "gYear" => Some(XsdDatatypeKind::GYear),
621        "gMonthDay" => Some(XsdDatatypeKind::GMonthDay),
622        "gDay" => Some(XsdDatatypeKind::GDay),
623        "gMonth" => Some(XsdDatatypeKind::GMonth),
624        "hexBinary" => Some(XsdDatatypeKind::HexBinary),
625        "base64Binary" => Some(XsdDatatypeKind::Base64Binary),
626        "anyURI" => Some(XsdDatatypeKind::AnyURI),
627        "QName" => Some(XsdDatatypeKind::QName),
628        "NOTATION" => Some(XsdDatatypeKind::Notation),
629        "normalizedString" => Some(XsdDatatypeKind::NormalizedString),
630        "token" => Some(XsdDatatypeKind::Token),
631        "language" => Some(XsdDatatypeKind::Language),
632        "NMTOKEN" => Some(XsdDatatypeKind::Nmtoken),
633        "NMTOKENS" => Some(XsdDatatypeKind::Nmtokens),
634        "Name" => Some(XsdDatatypeKind::Name),
635        "NCName" => Some(XsdDatatypeKind::NCName),
636        "ID" => Some(XsdDatatypeKind::Id),
637        "IDREF" => Some(XsdDatatypeKind::Idref),
638        "IDREFS" => Some(XsdDatatypeKind::Idrefs),
639        "ENTITY" => Some(XsdDatatypeKind::Entity),
640        "ENTITIES" => Some(XsdDatatypeKind::Entities),
641        "integer" => Some(XsdDatatypeKind::Integer),
642        "nonPositiveInteger" => Some(XsdDatatypeKind::NonPositiveInteger),
643        "negativeInteger" => Some(XsdDatatypeKind::NegativeInteger),
644        "long" => Some(XsdDatatypeKind::Long),
645        "int" => Some(XsdDatatypeKind::Int),
646        "short" => Some(XsdDatatypeKind::Short),
647        "byte" => Some(XsdDatatypeKind::Byte),
648        "nonNegativeInteger" => Some(XsdDatatypeKind::NonNegativeInteger),
649        "unsignedLong" => Some(XsdDatatypeKind::UnsignedLong),
650        "unsignedInt" => Some(XsdDatatypeKind::UnsignedInt),
651        "unsignedShort" => Some(XsdDatatypeKind::UnsignedShort),
652        "unsignedByte" => Some(XsdDatatypeKind::UnsignedByte),
653        "positiveInteger" => Some(XsdDatatypeKind::PositiveInteger),
654        _ => None,
655    }
656}
657
658/// Canonical `xs:` QName for a built-in datatype kind (inverse of
659/// `parse_datatype_kind`; used for upstream-format type errors).
660const fn datatype_kind_qname(kind: &XsdDatatypeKind) -> &'static str {
661    match kind {
662        XsdDatatypeKind::String => "xs:string",
663        XsdDatatypeKind::Boolean => "xs:boolean",
664        XsdDatatypeKind::Decimal => "xs:decimal",
665        XsdDatatypeKind::Float => "xs:float",
666        XsdDatatypeKind::Double => "xs:double",
667        XsdDatatypeKind::Duration => "xs:duration",
668        XsdDatatypeKind::DateTime => "xs:dateTime",
669        XsdDatatypeKind::Time => "xs:time",
670        XsdDatatypeKind::Date => "xs:date",
671        XsdDatatypeKind::GYearMonth => "xs:gYearMonth",
672        XsdDatatypeKind::GYear => "xs:gYear",
673        XsdDatatypeKind::GMonthDay => "xs:gMonthDay",
674        XsdDatatypeKind::GDay => "xs:gDay",
675        XsdDatatypeKind::GMonth => "xs:gMonth",
676        XsdDatatypeKind::HexBinary => "xs:hexBinary",
677        XsdDatatypeKind::Base64Binary => "xs:base64Binary",
678        XsdDatatypeKind::AnyURI => "xs:anyURI",
679        XsdDatatypeKind::QName => "xs:QName",
680        XsdDatatypeKind::Notation => "xs:NOTATION",
681        XsdDatatypeKind::NormalizedString => "xs:normalizedString",
682        XsdDatatypeKind::Token => "xs:token",
683        XsdDatatypeKind::Language => "xs:language",
684        XsdDatatypeKind::Nmtoken => "xs:NMTOKEN",
685        XsdDatatypeKind::Nmtokens => "xs:NMTOKENS",
686        XsdDatatypeKind::Name => "xs:Name",
687        XsdDatatypeKind::NCName => "xs:NCName",
688        XsdDatatypeKind::Id => "xs:ID",
689        XsdDatatypeKind::Idref => "xs:IDREF",
690        XsdDatatypeKind::Idrefs => "xs:IDREFS",
691        XsdDatatypeKind::Entity => "xs:ENTITY",
692        XsdDatatypeKind::Entities => "xs:ENTITIES",
693        XsdDatatypeKind::Integer => "xs:integer",
694        XsdDatatypeKind::NonPositiveInteger => "xs:nonPositiveInteger",
695        XsdDatatypeKind::NegativeInteger => "xs:negativeInteger",
696        XsdDatatypeKind::Long => "xs:long",
697        XsdDatatypeKind::Int => "xs:int",
698        XsdDatatypeKind::Short => "xs:short",
699        XsdDatatypeKind::Byte => "xs:byte",
700        XsdDatatypeKind::NonNegativeInteger => "xs:nonNegativeInteger",
701        XsdDatatypeKind::UnsignedLong => "xs:unsignedLong",
702        XsdDatatypeKind::UnsignedInt => "xs:unsignedInt",
703        XsdDatatypeKind::UnsignedShort => "xs:unsignedShort",
704        XsdDatatypeKind::UnsignedByte => "xs:unsignedByte",
705        XsdDatatypeKind::PositiveInteger => "xs:positiveInteger",
706        XsdDatatypeKind::FacetPattern => "pattern",
707        XsdDatatypeKind::FacetEnumeration => "enumeration",
708        XsdDatatypeKind::FacetMinInclusive => "minInclusive",
709        XsdDatatypeKind::FacetMaxInclusive => "maxInclusive",
710        XsdDatatypeKind::FacetMinExclusive => "minExclusive",
711        XsdDatatypeKind::FacetMaxExclusive => "maxExclusive",
712        XsdDatatypeKind::FacetMinLength => "minLength",
713        XsdDatatypeKind::FacetMaxLength => "maxLength",
714        XsdDatatypeKind::FacetLength => "length",
715        XsdDatatypeKind::FacetWhiteSpace => "whiteSpace",
716        XsdDatatypeKind::FacetFractionDigits => "fractionDigits",
717        XsdDatatypeKind::FacetTotalDigits => "totalDigits",
718    }
719}
720
721/// Parse a facet kind from an XSD element name.
722fn parse_facet_kind(name: &str) -> Option<XsdDatatypeKind> {
723    match name {
724        "pattern" => Some(XsdDatatypeKind::FacetPattern),
725        "enumeration" => Some(XsdDatatypeKind::FacetEnumeration),
726        "minInclusive" => Some(XsdDatatypeKind::FacetMinInclusive),
727        "maxInclusive" => Some(XsdDatatypeKind::FacetMaxInclusive),
728        "minExclusive" => Some(XsdDatatypeKind::FacetMinExclusive),
729        "maxExclusive" => Some(XsdDatatypeKind::FacetMaxExclusive),
730        "minLength" => Some(XsdDatatypeKind::FacetMinLength),
731        "maxLength" => Some(XsdDatatypeKind::FacetMaxLength),
732        "length" => Some(XsdDatatypeKind::FacetLength),
733        "whiteSpace" => Some(XsdDatatypeKind::FacetWhiteSpace),
734        "fractionDigits" => Some(XsdDatatypeKind::FacetFractionDigits),
735        "totalDigits" => Some(XsdDatatypeKind::FacetTotalDigits),
736        _ => None,
737    }
738}
739
740// ═══════════════════════════════════════════════════════════════════════════════
741// Schema Parsing
742// ═══════════════════════════════════════════════════════════════════════════════
743
744/// Parse an XSD schema from an XML string.
745///
746/// # UPSTREAM-PARITY
747///
748/// Equivalent to `xmlSchemaParse` in libxml2.
749///
750/// Returns the parsed schema, or an error message on failure.
751///
752/// # Safety
753///
754/// - `xml_doc` must be a valid string readable for `xml_doc.len()` bytes;
755///   `doc_ptr` is non-NULL (checked) and owned by this function, which
756///   frees it with `xmlFreeDoc` exactly once after parsing; the schema
757///   document must not be mutated by other threads during the call.
758pub fn xsd_parse(xml_doc: &str) -> Result<XsdSchema, String> {
759    // Use the XML parser to parse the schema document
760    let doc_ptr = unsafe {
761        crate::abi::exports_xml2::xmlReadMemory(
762            xml_doc.as_ptr() as *const c_char,
763            xml_doc.len() as c_int,
764            c"schema.xsd".as_ptr() as *const c_char,
765            ptr::null(),
766            0,
767        )
768    };
769
770    if doc_ptr.is_null() {
771        return Err("Failed to parse schema XML document".to_string());
772    }
773
774    let result = unsafe { xsd_parse_schema_doc(doc_ptr) };
775    unsafe {
776        crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
777    }
778    result
779}
780
781/// Parse an XSD schema from a parsed XML document.
782///
783/// # SAFETY
784///
785/// - `doc` must be a valid pointer to an _xmlDoc representing an XSD schema.
786unsafe fn xsd_parse_schema_doc(doc: *mut _xmlDoc) -> Result<XsdSchema, String> {
787    unsafe {
788        let root = (*doc).children;
789        if root.is_null() {
790            return Err("Schema document has no root element".to_string());
791        }
792
793        // Find the root <schema> element
794        let mut schema_node = root;
795        while !schema_node.is_null()
796            && ((*schema_node).type_ != XML_ELEMENT_NODE as c_int
797                || !node_is(schema_node, "schema"))
798        {
799            schema_node = (*schema_node).next;
800        }
801
802        if schema_node.is_null() {
803            return Err("Schema document root is not <schema>".to_string());
804        }
805
806        Ok(xsd_parse_schema_node(schema_node))
807    }
808}
809
810/// Parse a <schema> element.
811///
812/// # SAFETY
813///
814/// - `node` must be a valid pointer to a <schema> element node.
815unsafe fn xsd_parse_schema_node(node: *mut _xmlNode) -> XsdSchema {
816    unsafe {
817        let mut schema = XsdSchema::new();
818        schema.target_namespace = get_attr(node, "targetNamespace");
819        schema.element_form_default = get_attr(node, "elementFormDefault");
820        schema.attribute_form_default = get_attr(node, "attributeFormDefault");
821
822        // Parse child components
823        let mut child = (*node).children;
824        while !child.is_null() {
825            if (*child).type_ == XML_ELEMENT_NODE as c_int {
826                let comp = xsd_parse_component(child, &schema);
827                if comp.component_type != XsdComponentType::Annotation {
828                    schema.components.push(comp);
829                }
830            }
831            child = (*child).next;
832        }
833
834        // UPSTREAM-PARITY (xmlschemas.c xmlSchemaParse — global components
835        // live in the schema's target namespace): stamp the schema
836        // targetNamespace onto every top-level component so global element
837        // declarations can be matched against instance elements by
838        // EXPANDED name (namespace + local) instead of the prefixed QName
839        // string. Nested (local) declarations keep target_namespace = None:
840        // with the default elementForm="unqualified" they match
841        // no-namespace instance children only.
842        if schema.target_namespace.is_some() {
843            for comp in &mut schema.components {
844                if comp.target_namespace.is_none() {
845                    comp.target_namespace = schema.target_namespace.clone();
846                }
847            }
848        }
849
850        schema
851    }
852}
853
854/// Parse a single XSD component from an element node.
855///
856/// # SAFETY
857///
858/// - `node` must be a valid pointer to an XML element node.
859unsafe fn xsd_parse_component(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
860    unsafe {
861        // Determine the component type from the element name
862        let name_str = if !(*node).name.is_null() {
863            let mut len = 0;
864            while *(*node).name.add(len) != 0 {
865                len += 1;
866            }
867            let slice = std::slice::from_raw_parts((*node).name, len);
868            if let Ok(s) = std::str::from_utf8(slice) {
869                if let Some(pos) = s.find(':') {
870                    s[pos + 1..].to_string()
871                } else {
872                    s.to_string()
873                }
874            } else {
875                String::new()
876            }
877        } else {
878            String::new()
879        };
880
881        match name_str.as_str() {
882            "element" => xsd_parse_element(node, schema),
883            "attribute" => xsd_parse_attribute_node(node, schema),
884            "complexType" => xsd_parse_complex_type(node, schema),
885            "simpleType" => xsd_parse_simple_type(node, schema),
886            "sequence" => xsd_parse_model_group(node, XsdComponentType::Sequence, schema),
887            "choice" => xsd_parse_model_group(node, XsdComponentType::Choice, schema),
888            "all" => xsd_parse_model_group(node, XsdComponentType::All, schema),
889            "restriction" => xsd_parse_restriction(node, schema),
890            "extension" => xsd_parse_extension(node, schema),
891            "list" => xsd_parse_list(node, schema),
892            "union" => xsd_parse_union(node, schema),
893            "annotation" => xsd_parse_annotation(node),
894            "any" => xsd_parse_any(node, schema),
895            "anyAttribute" => XsdComponent::new(XsdComponentType::AnyAttribute),
896            "group" => xsd_parse_group(node, schema),
897            "attributeGroup" => xsd_parse_attribute_group(node, schema),
898            "unique" => xsd_parse_identity_constraint(node, XsdComponentType::Unique, schema),
899            "key" => xsd_parse_identity_constraint(node, XsdComponentType::Key, schema),
900            "keyref" => xsd_parse_identity_constraint(node, XsdComponentType::KeyRef, schema),
901            // Facets
902            "pattern" | "enumeration" | "minInclusive" | "maxInclusive" | "minExclusive"
903            | "maxExclusive" | "minLength" | "maxLength" | "length" | "whiteSpace"
904            | "fractionDigits" | "totalDigits" => xsd_parse_facet(node),
905            // Simple content / complex content markers
906            "simpleContent" => xsd_parse_simple_content(node, schema),
907            "complexContent" => xsd_parse_complex_content(node, schema),
908            _ => {
909                // Unknown element — create a generic component
910                let mut comp = XsdComponent::new(XsdComponentType::Schema);
911                if let Ok(s) = std::str::from_utf8(std::slice::from_raw_parts((*node).name, {
912                    let mut len = 0;
913                    while *(*node).name.add(len) != 0 {
914                        len += 1;
915                    }
916                    len
917                })) {
918                    comp.name = Some(s.to_string());
919                }
920                comp
921            }
922        }
923    }
924}
925
926/// Parse an <element> declaration.
927///
928/// # SAFETY
929///
930/// - `node` must be a valid pointer to an <element> element node.
931unsafe fn xsd_parse_element(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
932    unsafe {
933        let mut comp = XsdComponent::new(XsdComponentType::Element);
934        comp.name = get_attr(node, "name");
935        comp.ref_name = get_attr(node, "ref");
936        comp.min_occurs = get_attr_occurs(node, "minOccurs", 1);
937        comp.max_occurs = get_attr_occurs(node, "maxOccurs", 1);
938        comp.is_abstract = get_attr_bool(node, "abstract");
939        comp.is_final = get_attr_bool(node, "final");
940        comp.substitution_group = get_attr(node, "substitutionGroup");
941        comp.form = get_attr(node, "form");
942
943        // Resolve type attribute
944        if let Some(type_name) = get_attr(node, "type") {
945            comp.datatype = parse_datatype_kind(&type_name);
946            // If it's not a built-in type, store the type name as base
947            if comp.datatype.is_none() {
948                comp.base = Some(type_name);
949            }
950        }
951
952        // Check for default/fixed value
953        comp.default_value = get_attr(node, "default");
954        comp.fixed_value = get_attr(node, "fixed");
955
956        // Parse child components (inline type definitions)
957        let mut child = (*node).children;
958        while !child.is_null() {
959            if (*child).type_ == XML_ELEMENT_NODE as c_int {
960                let child_comp = xsd_parse_component(child, schema);
961                match child_comp.component_type {
962                    XsdComponentType::ComplexType | XsdComponentType::SimpleType => {
963                        // Inline type definition
964                        if let Some(ref name) = child_comp.name {
965                            comp.base = Some(name.clone());
966                        }
967                        comp.children.push(child_comp);
968                    }
969                    XsdComponentType::Annotation => {
970                        // Skip annotations
971                    }
972                    _ => {
973                        comp.children.push(child_comp);
974                    }
975                }
976            }
977            child = (*child).next;
978        }
979
980        comp
981    }
982}
983
984/// Parse an <attribute> declaration.
985///
986/// # SAFETY
987///
988/// - `node` must be a valid pointer to an <attribute> element node.
989unsafe fn xsd_parse_attribute_node(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
990    unsafe {
991        let mut comp = XsdComponent::new(XsdComponentType::Attribute);
992        comp.name = get_attr(node, "name");
993        comp.ref_name = get_attr(node, "ref");
994        comp.form = get_attr(node, "form");
995
996        // Resolve type attribute
997        if let Some(type_name) = get_attr(node, "type") {
998            comp.datatype = parse_datatype_kind(&type_name);
999            if comp.datatype.is_none() {
1000                comp.base = Some(type_name);
1001            }
1002        }
1003
1004        // Check for use attribute
1005        let use_attr = get_attr(node, "use");
1006        if let Some(ref use_val) = use_attr {
1007            if use_val == "required" {
1008                comp.min_occurs = 1;
1009            } else if use_val == "prohibited" {
1010                comp.min_occurs = 0;
1011                comp.max_occurs = 0;
1012            } else {
1013                // optional
1014                comp.min_occurs = 0;
1015            }
1016        } else {
1017            comp.min_occurs = 0; // optional by default
1018        }
1019
1020        comp.default_value = get_attr(node, "default");
1021        comp.fixed_value = get_attr(node, "fixed");
1022
1023        // Parse child components (inline simpleType)
1024        let mut child = (*node).children;
1025        while !child.is_null() {
1026            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1027                let child_comp = xsd_parse_component(child, schema);
1028                if child_comp.component_type == XsdComponentType::SimpleType {
1029                    if let Some(ref name) = child_comp.name {
1030                        comp.base = Some(name.clone());
1031                    }
1032                    comp.children.push(child_comp);
1033                }
1034            }
1035            child = (*child).next;
1036        }
1037
1038        comp
1039    }
1040}
1041
1042/// Parse a <complexType> definition.
1043///
1044/// # SAFETY
1045///
1046/// - `node` must be a valid pointer to a <complexType> element node.
1047unsafe fn xsd_parse_complex_type(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1048    unsafe {
1049        let mut comp = XsdComponent::new(XsdComponentType::ComplexType);
1050        comp.name = get_attr(node, "name");
1051        comp.mixed = get_attr_bool(node, "mixed");
1052        comp.is_abstract = get_attr_bool(node, "abstract");
1053        comp.is_final = get_attr_bool(node, "final");
1054
1055        // Parse child components
1056        let mut child = (*node).children;
1057        while !child.is_null() {
1058            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1059                let child_comp = xsd_parse_component(child, schema);
1060                match child_comp.component_type {
1061                    XsdComponentType::SimpleContent
1062                    | XsdComponentType::ComplexContent
1063                    | XsdComponentType::Sequence
1064                    | XsdComponentType::Choice
1065                    | XsdComponentType::All
1066                    | XsdComponentType::Group
1067                    | XsdComponentType::Any
1068                    | XsdComponentType::Annotation => {
1069                        if child_comp.component_type == XsdComponentType::SimpleContent {
1070                            // simpleContent may contain restriction/extension
1071                            comp.children.extend(child_comp.children);
1072                        } else if child_comp.component_type == XsdComponentType::ComplexContent {
1073                            // complexContent may contain restriction/extension
1074                            comp.children.extend(child_comp.children);
1075                        } else {
1076                            comp.children.push(child_comp);
1077                        }
1078                    }
1079                    XsdComponentType::Attribute | XsdComponentType::AnyAttribute => {
1080                        comp.attributes.push(child_comp);
1081                    }
1082                    _ => {
1083                        comp.children.push(child_comp);
1084                    }
1085                }
1086            }
1087            child = (*child).next;
1088        }
1089
1090        comp
1091    }
1092}
1093
1094/// Parse a <simpleType> definition.
1095///
1096/// # SAFETY
1097///
1098/// - `node` must be a valid pointer to a <simpleType> element node.
1099unsafe fn xsd_parse_simple_type(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1100    unsafe {
1101        let mut comp = XsdComponent::new(XsdComponentType::SimpleType);
1102        comp.name = get_attr(node, "name");
1103
1104        // Parse child components (restriction, list, union)
1105        let mut child = (*node).children;
1106        while !child.is_null() {
1107            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1108                let child_comp = xsd_parse_component(child, schema);
1109                match child_comp.component_type {
1110                    XsdComponentType::Restriction
1111                    | XsdComponentType::List
1112                    | XsdComponentType::Union => {
1113                        comp.datatype = child_comp.datatype;
1114                        comp.base = child_comp.base;
1115                        comp.facets = child_comp.facets;
1116                        comp.children.extend(child_comp.children);
1117                    }
1118                    _ => {}
1119                }
1120            }
1121            child = (*child).next;
1122        }
1123
1124        comp
1125    }
1126}
1127
1128/// Parse a model group (<sequence>, <choice>, <all>).
1129///
1130/// # SAFETY
1131///
1132/// - `node` must be a valid pointer to the model group element node.
1133unsafe fn xsd_parse_model_group(
1134    node: *mut _xmlNode,
1135    ctype: XsdComponentType,
1136    schema: &XsdSchema,
1137) -> XsdComponent {
1138    unsafe {
1139        let mut comp = XsdComponent::new(ctype);
1140        comp.min_occurs = get_attr_occurs(node, "minOccurs", 1);
1141        comp.max_occurs = get_attr_occurs(node, "maxOccurs", 1);
1142
1143        let mut child = (*node).children;
1144        while !child.is_null() {
1145            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1146                let child_comp = xsd_parse_component(child, schema);
1147                match child_comp.component_type {
1148                    XsdComponentType::Annotation => {}
1149                    _ => {
1150                        comp.children.push(child_comp);
1151                    }
1152                }
1153            }
1154            child = (*child).next;
1155        }
1156
1157        comp
1158    }
1159}
1160
1161/// Parse a <restriction> element.
1162///
1163/// # SAFETY
1164///
1165/// - `node` must be a valid pointer to a <restriction> element node.
1166unsafe fn xsd_parse_restriction(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1167    unsafe {
1168        let mut comp = XsdComponent::new(XsdComponentType::Restriction);
1169        comp.base = get_attr(node, "base");
1170
1171        // Try to resolve the base type
1172        if let Some(ref base_name) = comp.base {
1173            comp.datatype = parse_datatype_kind(base_name);
1174        }
1175
1176        // Parse facets and child components
1177        let mut child = (*node).children;
1178        while !child.is_null() {
1179            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1180                let child_comp = xsd_parse_component(child, schema);
1181                match child_comp.component_type {
1182                    XsdComponentType::Sequence
1183                    | XsdComponentType::Choice
1184                    | XsdComponentType::All
1185                    | XsdComponentType::Group
1186                    | XsdComponentType::Any
1187                    | XsdComponentType::Annotation => {
1188                        comp.children.push(child_comp);
1189                    }
1190                    XsdComponentType::Attribute | XsdComponentType::AnyAttribute => {
1191                        comp.attributes.push(child_comp);
1192                    }
1193                    XsdComponentType::SimpleType => {
1194                        // Inline simpleType
1195                        comp.children.push(child_comp);
1196                    }
1197                    _ => {
1198                        // Facet types
1199                        if let Some(facet_kind) =
1200                            parse_facet_kind(&format!("{:?}", child_comp.component_type))
1201                        {
1202                            // Extract the value attribute
1203                            if let Some(val) = get_attr(child, "value") {
1204                                comp.facets.push((facet_kind, val));
1205                            }
1206                        }
1207                        // Also try by element name
1208                        let name_str = if !(*child).name.is_null() {
1209                            let mut len = 0;
1210                            while *(*child).name.add(len) != 0 {
1211                                len += 1;
1212                            }
1213                            let slice = std::slice::from_raw_parts((*child).name, len);
1214                            std::str::from_utf8(slice)
1215                                .ok()
1216                                .map(|s| {
1217                                    if let Some(pos) = s.find(':') {
1218                                        s[pos + 1..].to_string()
1219                                    } else {
1220                                        s.to_string()
1221                                    }
1222                                })
1223                                .unwrap_or_default()
1224                        } else {
1225                            String::new()
1226                        };
1227                        if !name_str.is_empty() {
1228                            if let Some(facet_kind) = parse_facet_kind(&name_str) {
1229                                if let Some(val) = get_attr(child, "value") {
1230                                    comp.facets.push((facet_kind, val));
1231                                }
1232                            }
1233                        }
1234                    }
1235                }
1236            }
1237            child = (*child).next;
1238        }
1239
1240        comp
1241    }
1242}
1243
1244/// Parse an <extension> element.
1245///
1246/// # SAFETY
1247///
1248/// - `node` must be a valid pointer to an <extension> element node.
1249unsafe fn xsd_parse_extension(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1250    unsafe {
1251        let mut comp = XsdComponent::new(XsdComponentType::Extension);
1252        comp.base = get_attr(node, "base");
1253
1254        if let Some(ref base_name) = comp.base {
1255            comp.datatype = parse_datatype_kind(base_name);
1256        }
1257
1258        // Parse child components
1259        let mut child = (*node).children;
1260        while !child.is_null() {
1261            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1262                let child_comp = xsd_parse_component(child, schema);
1263                match child_comp.component_type {
1264                    XsdComponentType::Sequence
1265                    | XsdComponentType::Choice
1266                    | XsdComponentType::All
1267                    | XsdComponentType::Group
1268                    | XsdComponentType::Any
1269                    | XsdComponentType::Annotation => {
1270                        comp.children.push(child_comp);
1271                    }
1272                    XsdComponentType::Attribute | XsdComponentType::AnyAttribute => {
1273                        comp.attributes.push(child_comp);
1274                    }
1275                    _ => {}
1276                }
1277            }
1278            child = (*child).next;
1279        }
1280
1281        comp
1282    }
1283}
1284
1285/// Parse a <list> element.
1286///
1287/// # SAFETY
1288///
1289/// - `node` must be a valid pointer to a <list> element node.
1290unsafe fn xsd_parse_list(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1291    unsafe {
1292        let mut comp = XsdComponent::new(XsdComponentType::List);
1293
1294        if let Some(item_type) = get_attr(node, "itemType") {
1295            comp.base = Some(item_type.clone());
1296            comp.datatype = parse_datatype_kind(&item_type);
1297        }
1298
1299        // Check for inline simpleType
1300        let mut child = (*node).children;
1301        while !child.is_null() {
1302            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1303                let child_comp = xsd_parse_component(child, schema);
1304                if child_comp.component_type == XsdComponentType::SimpleType {
1305                    comp.datatype = child_comp.datatype;
1306                    comp.base = child_comp.base;
1307                    comp.facets = child_comp.facets;
1308                }
1309            }
1310            child = (*child).next;
1311        }
1312
1313        comp
1314    }
1315}
1316
1317/// Parse a <union> element.
1318///
1319/// # SAFETY
1320///
1321/// - `node` must be a valid pointer to a <union> element node.
1322unsafe fn xsd_parse_union(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1323    unsafe {
1324        let mut comp = XsdComponent::new(XsdComponentType::Union);
1325
1326        if let Some(member_types) = get_attr(node, "memberTypes") {
1327            comp.base = Some(member_types);
1328        }
1329
1330        let mut child = (*node).children;
1331        while !child.is_null() {
1332            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1333                let child_comp = xsd_parse_component(child, schema);
1334                if child_comp.component_type == XsdComponentType::SimpleType {
1335                    comp.children.push(child_comp);
1336                }
1337            }
1338            child = (*child).next;
1339        }
1340
1341        comp
1342    }
1343}
1344
1345/// Parse an <annotation> element.
1346///
1347/// # SAFETY
1348///
1349/// - `node` must be a valid pointer to an <annotation> element node.
1350unsafe fn xsd_parse_annotation(node: *mut _xmlNode) -> XsdComponent {
1351    unsafe {
1352        let mut comp = XsdComponent::new(XsdComponentType::Annotation);
1353
1354        let mut child = (*node).children;
1355        while !child.is_null() {
1356            if (*child).type_ == XML_ELEMENT_NODE as c_int
1357                && (node_is(child, "documentation") || node_is(child, "appinfo"))
1358            {
1359                let text = get_node_text(child);
1360                if !text.is_empty() {
1361                    comp.facets.push((XsdDatatypeKind::String, text));
1362                }
1363            }
1364            child = (*child).next;
1365        }
1366
1367        comp
1368    }
1369}
1370
1371/// Parse an <any> element.
1372///
1373/// # SAFETY
1374///
1375/// - `node` must be a valid pointer to an <any> element node.
1376unsafe fn xsd_parse_any(node: *mut _xmlNode, _schema: &XsdSchema) -> XsdComponent {
1377    unsafe {
1378        let mut comp = XsdComponent::new(XsdComponentType::Any);
1379        comp.min_occurs = get_attr_occurs(node, "minOccurs", 1);
1380        comp.max_occurs = get_attr_occurs(node, "maxOccurs", 1);
1381
1382        let namespace_attr = get_attr(node, "namespace");
1383        if let Some(ref ns) = namespace_attr {
1384            if ns != "##any" {
1385                comp.target_namespace = Some(ns.clone());
1386            }
1387        }
1388
1389        comp
1390    }
1391}
1392
1393/// Parse a <group> element.
1394///
1395/// # SAFETY
1396///
1397/// - `node` must be a valid pointer to a <group> element node.
1398unsafe fn xsd_parse_group(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1399    unsafe {
1400        let mut comp = XsdComponent::new(XsdComponentType::Group);
1401        comp.name = get_attr(node, "name");
1402        comp.ref_name = get_attr(node, "ref");
1403        comp.min_occurs = get_attr_occurs(node, "minOccurs", 1);
1404        comp.max_occurs = get_attr_occurs(node, "maxOccurs", 1);
1405
1406        let mut child = (*node).children;
1407        while !child.is_null() {
1408            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1409                let child_comp = xsd_parse_component(child, schema);
1410                match child_comp.component_type {
1411                    XsdComponentType::Annotation => {}
1412                    _ => {
1413                        comp.children.push(child_comp);
1414                    }
1415                }
1416            }
1417            child = (*child).next;
1418        }
1419
1420        comp
1421    }
1422}
1423
1424/// Parse an <attributeGroup> element.
1425///
1426/// # SAFETY
1427///
1428/// - `node` must be a valid pointer to an <attributeGroup> element node.
1429unsafe fn xsd_parse_attribute_group(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1430    unsafe {
1431        let mut comp = XsdComponent::new(XsdComponentType::AttributeGroup);
1432        comp.name = get_attr(node, "name");
1433        comp.ref_name = get_attr(node, "ref");
1434
1435        let mut child = (*node).children;
1436        while !child.is_null() {
1437            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1438                let child_comp = xsd_parse_component(child, schema);
1439                match child_comp.component_type {
1440                    XsdComponentType::Attribute | XsdComponentType::AnyAttribute => {
1441                        comp.attributes.push(child_comp);
1442                    }
1443                    _ => {}
1444                }
1445            }
1446            child = (*child).next;
1447        }
1448
1449        comp
1450    }
1451}
1452
1453/// Parse a facet element (pattern, enumeration, etc.).
1454///
1455/// # SAFETY
1456///
1457/// - `node` must be a valid pointer to a facet element node.
1458unsafe fn xsd_parse_facet(node: *mut _xmlNode) -> XsdComponent {
1459    unsafe {
1460        let name_str = if !(*node).name.is_null() {
1461            let mut len = 0;
1462            while *(*node).name.add(len) != 0 {
1463                len += 1;
1464            }
1465            let slice = std::slice::from_raw_parts((*node).name, len);
1466            std::str::from_utf8(slice)
1467                .ok()
1468                .map(|s| {
1469                    if let Some(pos) = s.find(':') {
1470                        s[pos + 1..].to_string()
1471                    } else {
1472                        s.to_string()
1473                    }
1474                })
1475                .unwrap_or_default()
1476        } else {
1477            String::new()
1478        };
1479
1480        let facet_kind = parse_facet_kind(&name_str).unwrap_or(XsdDatatypeKind::String);
1481        let mut comp = XsdComponent::new(XsdComponentType::Schema);
1482        let val = get_attr(node, "value").unwrap_or_default();
1483        comp.facets.push((facet_kind, val));
1484        comp.datatype = Some(facet_kind);
1485
1486        comp
1487    }
1488}
1489
1490/// Parse a <simpleContent> element.
1491///
1492/// # SAFETY
1493///
1494/// - `node` must be a valid pointer to a <simpleContent> element node.
1495unsafe fn xsd_parse_simple_content(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1496    unsafe {
1497        let mut comp = XsdComponent::new(XsdComponentType::Schema);
1498
1499        let mut child = (*node).children;
1500        while !child.is_null() {
1501            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1502                let child_comp = xsd_parse_component(child, schema);
1503                match child_comp.component_type {
1504                    XsdComponentType::Restriction | XsdComponentType::Extension => {
1505                        comp.children.push(child_comp);
1506                    }
1507                    _ => {}
1508                }
1509            }
1510            child = (*child).next;
1511        }
1512
1513        comp
1514    }
1515}
1516
1517/// Parse a <complexContent> element.
1518///
1519/// # SAFETY
1520///
1521/// - `node` must be a valid pointer to a <complexContent> element node.
1522unsafe fn xsd_parse_complex_content(node: *mut _xmlNode, schema: &XsdSchema) -> XsdComponent {
1523    unsafe {
1524        let mut comp = XsdComponent::new(XsdComponentType::Schema);
1525
1526        let mut child = (*node).children;
1527        while !child.is_null() {
1528            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1529                let child_comp = xsd_parse_component(child, schema);
1530                match child_comp.component_type {
1531                    XsdComponentType::Restriction | XsdComponentType::Extension => {
1532                        comp.children.push(child_comp);
1533                    }
1534                    _ => {}
1535                }
1536            }
1537            child = (*child).next;
1538        }
1539
1540        comp
1541    }
1542}
1543
1544/// Parse an identity constraint (<unique>, <key>, <keyref>).
1545///
1546/// # SAFETY
1547///
1548/// - `node` must be a valid pointer to the identity constraint element node.
1549unsafe fn xsd_parse_identity_constraint(
1550    node: *mut _xmlNode,
1551    ctype: XsdComponentType,
1552    schema: &XsdSchema,
1553) -> XsdComponent {
1554    unsafe {
1555        let mut comp = XsdComponent::new(ctype);
1556        comp.name = get_attr(node, "name");
1557
1558        if ctype == XsdComponentType::KeyRef {
1559            comp.ref_name = get_attr(node, "refer");
1560        }
1561
1562        let mut child = (*node).children;
1563        while !child.is_null() {
1564            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1565                let child_comp = xsd_parse_component(child, schema);
1566                match child_comp.component_type {
1567                    XsdComponentType::Selector | XsdComponentType::Field => {
1568                        comp.children.push(child_comp);
1569                    }
1570                    _ => {}
1571                }
1572            }
1573            child = (*child).next;
1574        }
1575
1576        comp
1577    }
1578}
1579
1580// ═══════════════════════════════════════════════════════════════════════════════
1581// Datatype Validation
1582// ═══════════════════════════════════════════════════════════════════════════════
1583
1584/// Validate a value against an XSD datatype with optional facets.
1585///
1586/// # UPSTREAM-PARITY
1587///
1588/// Equivalent to libxml2's schema type validation functions.
1589pub fn xsd_validate_datatype(
1590    kind: &XsdDatatypeKind,
1591    value: &str,
1592    facets: &[(XsdDatatypeKind, String)],
1593) -> bool {
1594    // First validate the base type
1595    if !validate_base_type(kind, value) {
1596        return false;
1597    }
1598
1599    // Then validate facets.
1600    // UPSTREAM-PARITY: Enumeration facets use OR semantics (value must match
1601    // at least one enumeration value). All other facets use AND semantics
1602    // (value must satisfy all facets).
1603    let mut has_enumeration = false;
1604    let mut enumeration_match = false;
1605
1606    for (facet_kind, facet_value) in facets {
1607        if *facet_kind == XsdDatatypeKind::FacetEnumeration {
1608            has_enumeration = true;
1609            if xsd_validate_facet(kind, value, facet_kind, facet_value) {
1610                enumeration_match = true;
1611            }
1612        } else if !xsd_validate_facet(kind, value, facet_kind, facet_value) {
1613            return false;
1614        }
1615    }
1616
1617    // If there were enumeration facets, at least one must match
1618    if has_enumeration && !enumeration_match {
1619        return false;
1620    }
1621
1622    true
1623}
1624
1625/// Validate a value against a specific facet.
1626pub fn xsd_validate_facet(
1627    _kind: &XsdDatatypeKind,
1628    value: &str,
1629    facet_kind: &XsdDatatypeKind,
1630    facet_value: &str,
1631) -> bool {
1632    match facet_kind {
1633        XsdDatatypeKind::FacetPattern => {
1634            // Simple regex matching (simplified — just check substring containment
1635            // for common patterns like [a-zA-Z]+, etc.)
1636            match facet_value {
1637                r"\d+" => value.chars().all(|c| c.is_ascii_digit()),
1638                r"\d*" => value.is_empty() || value.chars().all(|c| c.is_ascii_digit()),
1639                r"[a-zA-Z]+" => value.chars().all(|c| c.is_ascii_alphabetic()),
1640                r"[a-zA-Z]*" => value.is_empty() || value.chars().all(|c| c.is_ascii_alphabetic()),
1641                r"[a-zA-Z0-9]+" => value.chars().all(|c| c.is_ascii_alphanumeric()),
1642                r"[a-zA-Z0-9_\-]+" => value
1643                    .chars()
1644                    .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
1645                r"[a-zA-Z_][a-zA-Z0-9_\-\.]*" => {
1646                    if value.is_empty() {
1647                        return false;
1648                    }
1649                    let first = value.chars().next().unwrap();
1650                    if !first.is_ascii_alphabetic() && first != '_' {
1651                        return false;
1652                    }
1653                    value
1654                        .chars()
1655                        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
1656                }
1657                r"[a-zA-Z_][\w\-\.]*" => {
1658                    if value.is_empty() {
1659                        return false;
1660                    }
1661                    let first = value.chars().next().unwrap();
1662                    if !first.is_ascii_alphabetic() && first != '_' {
1663                        return false;
1664                    }
1665                    value
1666                        .chars()
1667                        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
1668                }
1669                r"\i\c*" => {
1670                    // XML Name pattern: NameStartChar followed by NameChars
1671                    if value.is_empty() {
1672                        return false;
1673                    }
1674                    let first = value.chars().next().unwrap();
1675                    if !is_name_start_char(first) {
1676                        return false;
1677                    }
1678                    value.chars().skip(1).all(is_name_char)
1679                }
1680                r"\c+" => {
1681                    if value.is_empty() {
1682                        return false;
1683                    }
1684                    value.chars().all(is_name_char)
1685                }
1686                // Default: try basic glob-style matching
1687                _ => {
1688                    if facet_value.starts_with('^') && facet_value.ends_with('$') {
1689                        let inner = &facet_value[1..facet_value.len() - 1];
1690                        simple_glob_match(inner, value)
1691                    } else {
1692                        // Default: accept if we don't understand the pattern
1693                        true
1694                    }
1695                }
1696            }
1697        }
1698        XsdDatatypeKind::FacetEnumeration => {
1699            // Check if the value matches the enumeration literal
1700            value == facet_value
1701        }
1702        XsdDatatypeKind::FacetMinInclusive => {
1703            compare_strings(value, facet_value) != std::cmp::Ordering::Less
1704        }
1705        XsdDatatypeKind::FacetMaxInclusive => {
1706            compare_strings(value, facet_value) != std::cmp::Ordering::Greater
1707        }
1708        XsdDatatypeKind::FacetMinExclusive => {
1709            compare_strings(value, facet_value) == std::cmp::Ordering::Greater
1710        }
1711        XsdDatatypeKind::FacetMaxExclusive => {
1712            compare_strings(value, facet_value) == std::cmp::Ordering::Less
1713        }
1714        XsdDatatypeKind::FacetMinLength => {
1715            let min = facet_value.parse::<usize>().unwrap_or(0);
1716            value.chars().count() >= min
1717        }
1718        XsdDatatypeKind::FacetMaxLength => {
1719            let max = facet_value.parse::<usize>().unwrap_or(usize::MAX);
1720            value.chars().count() <= max
1721        }
1722        XsdDatatypeKind::FacetLength => {
1723            let len = facet_value.parse::<usize>().unwrap_or(0);
1724            value.chars().count() == len
1725        }
1726        XsdDatatypeKind::FacetWhiteSpace => {
1727            // whiteSpace facet: value, replace, collapse
1728            match facet_value {
1729                "replace" => {
1730                    // Any whitespace is valid (but should be tab/newline -> space)
1731                    // We just accept the value
1732                    true
1733                }
1734                "collapse" => {
1735                    // Leading/trailing whitespace collapsed, internal reduced
1736                    true
1737                }
1738                _ => true,
1739            }
1740        }
1741        XsdDatatypeKind::FacetFractionDigits | XsdDatatypeKind::FacetTotalDigits => {
1742            // Numeric precision facets — simplified: just check if it's a valid number
1743            value.parse::<f64>().is_ok()
1744        }
1745        _ => true,
1746    }
1747}
1748
1749/// Simple glob-style pattern matching for XSD pattern facets.
1750fn simple_glob_match(pattern: &str, value: &str) -> bool {
1751    let pattern_chars: Vec<char> = pattern.chars().collect();
1752    let value_chars: Vec<char> = value.chars().collect();
1753
1754    let mut pi = 0;
1755    let mut vi = 0;
1756    let mut backtrack_p = None;
1757    let mut backtrack_v = 0;
1758
1759    while vi < value_chars.len() {
1760        if pi < pattern_chars.len()
1761            && (pattern_chars[pi] == value_chars[vi] || pattern_chars[pi] == '.')
1762        {
1763            pi += 1;
1764            vi += 1;
1765        } else if pi < pattern_chars.len() && pattern_chars[pi] == '*' {
1766            backtrack_p = Some(pi);
1767            backtrack_v = vi + 1;
1768            pi += 1;
1769        } else if pi < pattern_chars.len() && pattern_chars[pi] == '+' {
1770            // '+' = one or more of the next char
1771            if pi + 1 < pattern_chars.len() && pattern_chars[pi + 1] == value_chars[vi] {
1772                pi += 1;
1773                vi += 1;
1774                // Match one or more
1775                while vi < value_chars.len() && value_chars[vi] == pattern_chars[pi] {
1776                    vi += 1;
1777                }
1778                pi += 1;
1779            } else {
1780                return false;
1781            }
1782        } else if let Some(bp) = backtrack_p {
1783            pi = bp + 1;
1784            vi = backtrack_v;
1785            backtrack_v += 1;
1786        } else {
1787            return false;
1788        }
1789    }
1790
1791    // Skip remaining * or + in pattern
1792    while pi < pattern_chars.len() && (pattern_chars[pi] == '*' || pattern_chars[pi] == '+') {
1793        if pattern_chars[pi] == '+' && vi == value_chars.len() {
1794            return false; // '+' requires at least one match
1795        }
1796        pi += 1;
1797    }
1798
1799    pi == pattern_chars.len()
1800}
1801
1802/// Check if a character is an XML NameStartChar.
1803const fn is_name_start_char(c: char) -> bool {
1804    c.is_ascii_alphabetic()
1805        || c == '_'
1806        || c == ':'
1807        || (c >= '\u{00C0}' && c <= '\u{00D6}')
1808        || (c >= '\u{00D8}' && c <= '\u{00F6}')
1809        || (c >= '\u{00F8}' && c <= '\u{02FF}')
1810        || (c >= '\u{0370}' && c <= '\u{037D}')
1811        || (c >= '\u{037F}' && c <= '\u{1FFF}')
1812        || (c >= '\u{200C}' && c <= '\u{200D}')
1813        || (c >= '\u{2070}' && c <= '\u{218F}')
1814        || (c >= '\u{2C00}' && c <= '\u{2FEF}')
1815        || (c >= '\u{3001}' && c <= '\u{D7FF}')
1816        || (c >= '\u{F900}' && c <= '\u{FDCF}')
1817        || (c >= '\u{FDF0}' && c <= '\u{FFFD}')
1818}
1819
1820/// Check if a character is an XML NameChar.
1821const fn is_name_char(c: char) -> bool {
1822    is_name_start_char(c)
1823        || c.is_ascii_digit()
1824        || c == '-'
1825        || c == '.'
1826        || c == '\u{00B7}'
1827        || (c >= '\u{0300}' && c <= '\u{036F}')
1828        || (c >= '\u{203F}' && c <= '\u{2040}')
1829}
1830
1831/// Compare two string values for facet ordering.
1832fn compare_strings(a: &str, b: &str) -> std::cmp::Ordering {
1833    // Try numeric comparison first
1834    if let (Ok(na), Ok(nb)) = (a.parse::<f64>(), b.parse::<f64>()) {
1835        return na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal);
1836    }
1837    // Try integer comparison
1838    if let (Ok(na), Ok(nb)) = (a.parse::<i64>(), b.parse::<i64>()) {
1839        return na.cmp(&nb);
1840    }
1841    // Fall back to lexicographic
1842    a.cmp(b)
1843}
1844
1845/// Validate a value against the base type constraints.
1846fn validate_base_type(kind: &XsdDatatypeKind, value: &str) -> bool {
1847    match kind {
1848        XsdDatatypeKind::String => true,
1849        XsdDatatypeKind::NormalizedString => {
1850            // No tabs, newlines, or carriage returns
1851            !value.contains('\t') && !value.contains('\n') && !value.contains('\r')
1852        }
1853        XsdDatatypeKind::Token => {
1854            // No leading/trailing whitespace, no consecutive internal whitespace
1855            if value.is_empty() {
1856                return true;
1857            }
1858            if value.starts_with(' ') || value.ends_with(' ') {
1859                return false;
1860            }
1861            !value.contains("  ")
1862                && !value.contains('\t')
1863                && !value.contains('\n')
1864                && !value.contains('\r')
1865        }
1866        XsdDatatypeKind::Language => {
1867            // RFC 4646 / BCP 47: langtag = (language ["-" script] ["-" region] *("-" variant))
1868            // Simplified: [a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*
1869            if value.is_empty() {
1870                return false;
1871            }
1872            let segments: Vec<&str> = value.split('-').collect();
1873            if segments.is_empty() {
1874                return false;
1875            }
1876            // First segment must be alphabetic only
1877            if segments[0].is_empty() || !segments[0].chars().all(|c| c.is_ascii_alphabetic()) {
1878                return false;
1879            }
1880            if segments[0].len() > 8 {
1881                return false;
1882            }
1883            // Remaining segments can be alphanumeric
1884            for seg in &segments[1..] {
1885                if seg.is_empty() || seg.len() > 8 {
1886                    return false;
1887                }
1888                if !seg.chars().all(|c| c.is_ascii_alphanumeric()) {
1889                    return false;
1890                }
1891            }
1892            true
1893        }
1894        XsdDatatypeKind::Name => {
1895            if value.is_empty() {
1896                return false;
1897            }
1898            let mut chars = value.chars();
1899            let first = chars.next().unwrap();
1900            if !is_name_start_char(first) {
1901                return false;
1902            }
1903            chars.all(is_name_char)
1904        }
1905        XsdDatatypeKind::NCName
1906        | XsdDatatypeKind::Id
1907        | XsdDatatypeKind::Idref
1908        | XsdDatatypeKind::Entity => {
1909            // NCName is a Name with no colon
1910            if value.is_empty() || value.contains(':') {
1911                return false;
1912            }
1913            let mut chars = value.chars();
1914            let first = chars.next().unwrap();
1915            if !is_name_start_char(first) {
1916                return false;
1917            }
1918            chars.all(is_name_char)
1919        }
1920        XsdDatatypeKind::Boolean => {
1921            matches!(value, "true" | "false" | "1" | "0")
1922        }
1923        XsdDatatypeKind::Decimal
1924        | XsdDatatypeKind::Integer
1925        | XsdDatatypeKind::NonPositiveInteger
1926        | XsdDatatypeKind::NegativeInteger
1927        | XsdDatatypeKind::Long
1928        | XsdDatatypeKind::Int
1929        | XsdDatatypeKind::Short
1930        | XsdDatatypeKind::Byte
1931        | XsdDatatypeKind::NonNegativeInteger
1932        | XsdDatatypeKind::UnsignedLong
1933        | XsdDatatypeKind::UnsignedInt
1934        | XsdDatatypeKind::UnsignedShort
1935        | XsdDatatypeKind::UnsignedByte
1936        | XsdDatatypeKind::PositiveInteger => {
1937            // Decimal/integer validation
1938            if value.is_empty() {
1939                return false;
1940            }
1941            let mut chars = value.chars().peekable();
1942            if *chars.peek().unwrap_or(&'\0') == '-' || *chars.peek().unwrap_or(&'\0') == '+' {
1943                chars.next();
1944            }
1945            let mut has_dot = false;
1946            let mut has_digit = false;
1947            for c in chars {
1948                if c == '.' {
1949                    if has_dot {
1950                        return false;
1951                    }
1952                    has_dot = true;
1953                } else if c.is_ascii_digit() {
1954                    has_digit = true;
1955                } else {
1956                    return false;
1957                }
1958            }
1959            if !has_digit {
1960                return false;
1961            }
1962
1963            // Additional constraints for derived integer types
1964            // Integer and all derived integer types reject decimal points
1965            if has_dot && *kind != XsdDatatypeKind::Decimal {
1966                return false;
1967            }
1968
1969            match kind {
1970                XsdDatatypeKind::NonPositiveInteger => {
1971                    if let Ok(v) = value.parse::<i64>() {
1972                        v <= 0
1973                    } else {
1974                        false
1975                    }
1976                }
1977                XsdDatatypeKind::NegativeInteger => {
1978                    if let Ok(v) = value.parse::<i64>() {
1979                        v < 0
1980                    } else {
1981                        false
1982                    }
1983                }
1984                XsdDatatypeKind::NonNegativeInteger => {
1985                    if let Ok(v) = value.parse::<i64>() {
1986                        v >= 0
1987                    } else {
1988                        false
1989                    }
1990                }
1991                XsdDatatypeKind::PositiveInteger => {
1992                    if let Ok(v) = value.parse::<i64>() {
1993                        v > 0
1994                    } else {
1995                        false
1996                    }
1997                }
1998                XsdDatatypeKind::UnsignedLong
1999                | XsdDatatypeKind::UnsignedInt
2000                | XsdDatatypeKind::UnsignedShort
2001                | XsdDatatypeKind::UnsignedByte => {
2002                    if let Ok(v) = value.parse::<u64>() {
2003                        match kind {
2004                            XsdDatatypeKind::UnsignedInt => v <= u64::from(u32::MAX),
2005                            XsdDatatypeKind::UnsignedShort => v <= u64::from(u16::MAX),
2006                            XsdDatatypeKind::UnsignedByte => v <= u64::from(u8::MAX),
2007                            _ => true,
2008                        }
2009                    } else {
2010                        false
2011                    }
2012                }
2013                XsdDatatypeKind::Long => value.parse::<i64>().is_ok(),
2014                XsdDatatypeKind::Int => value.parse::<i32>().is_ok(),
2015                XsdDatatypeKind::Short => value.parse::<i16>().is_ok(),
2016                XsdDatatypeKind::Byte => value.parse::<i8>().is_ok(),
2017                _ => true,
2018            }
2019        }
2020        XsdDatatypeKind::Float | XsdDatatypeKind::Double => {
2021            // Allow INF, -INF, NaN
2022            matches!(value, "INF" | "-INF" | "NaN") || value.parse::<f64>().is_ok()
2023        }
2024        XsdDatatypeKind::Duration => {
2025            // P[nY][nM][nD][T[nH][nM][nS]]
2026            if !value.starts_with('-') && !value.starts_with('P') {
2027                return false;
2028            }
2029            let dur = value.strip_prefix('-').unwrap_or(value);
2030            if !dur.starts_with('P') {
2031                return false;
2032            }
2033            let rest = &dur[1..];
2034            if rest.is_empty() {
2035                return false;
2036            }
2037            let has_t = rest.contains('T');
2038            let _date_part = if has_t {
2039                &rest[..rest.find('T').unwrap()]
2040            } else {
2041                rest
2042            };
2043            if has_t {
2044                let time_part = &rest[rest.find('T').unwrap() + 1..];
2045                if time_part.is_empty() {
2046                    return false;
2047                }
2048            }
2049            true
2050        }
2051        XsdDatatypeKind::DateTime => {
2052            // YYYY-MM-DDThh:mm:ss[.sss][Z|±hh:mm]
2053            if value.len() < 19 {
2054                return false;
2055            }
2056            let chars: Vec<char> = value.chars().collect();
2057            chars[4] == '-'
2058                && chars[7] == '-'
2059                && chars[10] == 'T'
2060                && chars[13] == ':'
2061                && chars[16] == ':'
2062        }
2063        XsdDatatypeKind::Date => {
2064            // YYYY-MM-DD[Z|±hh:mm]
2065            if value.len() < 10 {
2066                return false;
2067            }
2068            let chars: Vec<char> = value.chars().collect();
2069            chars[4] == '-' && chars[7] == '-'
2070        }
2071        XsdDatatypeKind::Time => {
2072            // hh:mm:ss[.sss][Z|±hh:mm]
2073            if value.len() < 8 {
2074                return false;
2075            }
2076            let chars: Vec<char> = value.chars().collect();
2077            chars[2] == ':' && chars[5] == ':'
2078        }
2079        XsdDatatypeKind::GYear => {
2080            // YYYY[Z|±hh:mm]
2081            if value.len() < 4 {
2082                return false;
2083            }
2084            value.chars().take(4).all(|c| c.is_ascii_digit())
2085        }
2086        XsdDatatypeKind::GYearMonth => {
2087            // YYYY-MM[Z|±hh:mm]
2088            if value.len() < 7 {
2089                return false;
2090            }
2091            let chars: Vec<char> = value.chars().collect();
2092            chars[4] == '-'
2093        }
2094        XsdDatatypeKind::GMonthDay => {
2095            // --MM-DD[Z|±hh:mm]
2096            if value.len() < 6 || !value.starts_with("--") {
2097                return false;
2098            }
2099            let chars: Vec<char> = value.chars().collect();
2100            chars[4] == '-'
2101        }
2102        XsdDatatypeKind::GDay => {
2103            // ---DD[Z|±hh:mm]
2104            value.starts_with("---") && value.len() >= 4
2105        }
2106        XsdDatatypeKind::GMonth => {
2107            // --MM[Z|±hh:mm]
2108            value.starts_with("--") && value.len() >= 3
2109        }
2110        XsdDatatypeKind::HexBinary => {
2111            if !value.len().is_multiple_of(2) {
2112                return false;
2113            }
2114            value.chars().all(|c| c.is_ascii_hexdigit())
2115        }
2116        XsdDatatypeKind::Base64Binary => {
2117            // Simplified: just check characters are valid base64
2118            if value.is_empty() {
2119                return true;
2120            }
2121            let valid_chars =
2122                |c: char| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=';
2123            value.chars().all(valid_chars)
2124        }
2125        XsdDatatypeKind::AnyURI => {
2126            // Simplified: just check it's not empty
2127            !value.is_empty()
2128        }
2129        XsdDatatypeKind::QName => {
2130            // NCName or Prefix:NCName
2131            if let Some(pos) = value.find(':') {
2132                let prefix = &value[..pos];
2133                let local = &value[pos + 1..];
2134                is_ncname(prefix) && is_ncname(local)
2135            } else {
2136                is_ncname(value)
2137            }
2138        }
2139        XsdDatatypeKind::Notation => {
2140            // Same as QName
2141            !value.is_empty()
2142        }
2143        XsdDatatypeKind::Nmtoken => !value.is_empty() && value.chars().all(is_name_char),
2144        XsdDatatypeKind::Nmtokens => {
2145            !value.is_empty()
2146                && value
2147                    .split_whitespace()
2148                    .all(|t| !t.is_empty() && t.chars().all(is_name_char))
2149        }
2150        XsdDatatypeKind::Idrefs | XsdDatatypeKind::Entities => {
2151            !value.is_empty() && value.split_whitespace().all(is_ncname)
2152        }
2153        // Facet types are always "valid" as values
2154        XsdDatatypeKind::FacetPattern
2155        | XsdDatatypeKind::FacetEnumeration
2156        | XsdDatatypeKind::FacetMinInclusive
2157        | XsdDatatypeKind::FacetMaxInclusive
2158        | XsdDatatypeKind::FacetMinExclusive
2159        | XsdDatatypeKind::FacetMaxExclusive
2160        | XsdDatatypeKind::FacetMinLength
2161        | XsdDatatypeKind::FacetMaxLength
2162        | XsdDatatypeKind::FacetLength
2163        | XsdDatatypeKind::FacetWhiteSpace
2164        | XsdDatatypeKind::FacetFractionDigits
2165        | XsdDatatypeKind::FacetTotalDigits => true,
2166    }
2167}
2168
2169/// Check if a string is a valid NCName.
2170fn is_ncname(value: &str) -> bool {
2171    if value.is_empty() {
2172        return false;
2173    }
2174    let first = value.chars().next().unwrap();
2175    if !is_name_start_char(first) || first == ':' {
2176        return false;
2177    }
2178    value.chars().skip(1).all(|c| is_name_char(c) && c != ':')
2179}
2180
2181// ═══════════════════════════════════════════════════════════════════════════════
2182// Document Validation
2183// ═══════════════════════════════════════════════════════════════════════════════
2184
2185/// Validate an XML document against a schema.
2186///
2187/// # UPSTREAM-PARITY
2188///
2189/// Equivalent to libxml2's `xmlSchemaValidateDoc`.
2190///
2191/// # Safety
2192///
2193/// - `doc` must be a valid string readable for `doc.len()` bytes;
2194///   `doc_ptr` is non-NULL (checked) and owned by this function, which
2195///   frees it with `xmlFreeDoc` exactly once; the parsed document must not
2196///   be mutated by other threads while `xsd_validate_doc` walks it.
2197pub fn xsd_validate(schema: &XsdSchema, doc: &str) -> Result<(), Vec<String>> {
2198    let doc_ptr = unsafe {
2199        crate::abi::exports_xml2::xmlReadMemory(
2200            doc.as_ptr() as *const c_char,
2201            doc.len() as c_int,
2202            c"doc.xml".as_ptr() as *const c_char,
2203            ptr::null(),
2204            0,
2205        )
2206    };
2207
2208    if doc_ptr.is_null() {
2209        return Err(vec!["Failed to parse XML document".to_string()]);
2210    }
2211
2212    let mut ctxt = XsdValidCtxt::new();
2213    ctxt.schema = Some(schema.clone());
2214
2215    let result = unsafe { xsd_validate_doc(schema, doc_ptr, &mut ctxt) };
2216
2217    unsafe {
2218        crate::abi::exports_xml2::xmlFreeDoc(doc_ptr);
2219    }
2220
2221    if result {
2222        Ok(())
2223    } else {
2224        Err(ctxt.errors)
2225    }
2226}
2227
2228/// Validate a parsed document against a schema.
2229///
2230/// # SAFETY
2231///
2232/// - `doc` must be a valid pointer to an _xmlDoc.
2233unsafe fn xsd_validate_doc(schema: &XsdSchema, doc: *mut _xmlDoc, ctxt: &mut XsdValidCtxt) -> bool {
2234    unsafe {
2235        let root = (*doc).children;
2236        if root.is_null() {
2237            ctxt.errors.push("Document has no root element".to_string());
2238            ctxt.nb_errors += 1;
2239            return false;
2240        }
2241
2242        // Find the root element
2243        let mut root_elem = root;
2244        while !root_elem.is_null() && (*root_elem).type_ != XML_ELEMENT_NODE as c_int {
2245            root_elem = (*root_elem).next;
2246        }
2247
2248        if root_elem.is_null() {
2249            ctxt.errors.push("Document has no root element".to_string());
2250            ctxt.nb_errors += 1;
2251            return false;
2252        }
2253
2254        // Get the root element's local name and namespace
2255        let root_name = get_node_qname(root_elem);
2256        let root_local = get_node_local_name(root_elem);
2257        let root_ns = get_node_ns_href(root_elem);
2258        let root_ns_deref = root_ns.as_deref();
2259
2260        // UPSTREAM-PARITY (xmlschemas.c xmlSchemaValidateDoc): find the
2261        // matching GLOBAL element declaration by EXPANDED name — local name
2262        // plus namespace. A root element whose namespace does not equal the
2263        // declaration's target namespace (including a no-namespace element
2264        // against a targetNamespace schema, and a prefixed element whose
2265        // prefix resolves to the right URI) must NOT match a declaration it
2266        // merely shares a local name with.
2267        let global_elem = schema.components.iter().find(|c| {
2268            c.component_type == XsdComponentType::Element
2269                && c.name.as_deref() == Some(root_local.as_str())
2270                && c.target_namespace.as_deref() == root_ns_deref
2271        });
2272
2273        if let Some(global) = global_elem {
2274            xsd_validate_element(global, root_elem, schema, ctxt)
2275        } else {
2276            // No matching global element declaration for the validation
2277            // root: fail with the exact upstream diagnostic
2278            // (DOMDocument_schemaValidate_error2 / reader schema errors).
2279            ctxt.errors.push(format!(
2280                "Element '{}': No matching global declaration available for the validation root.",
2281                root_name
2282            ));
2283            ctxt.nb_errors += 1;
2284            false
2285        }
2286    }
2287}
2288
2289/// Run XSD validation of a document against an already-compiled schema
2290/// (xmlSchemaPtr from xmlSchemaParse), collecting the diagnostics WITHOUT
2291/// dispatching them to any registered handler. Used by the xmlTextReader
2292/// (which validates deferred, at first Read, and raises the diagnostics
2293/// through the global libxml error channel itself — mirroring upstream's
2294/// streaming SAX-plug validation where errors surface at read time).
2295///
2296/// # SAFETY
2297///
2298/// - `schema` must be a valid compiled-schema pointer from xmlSchemaParse;
2299/// - `doc` must be a valid parsed document.
2300pub(crate) unsafe fn xsd_validate_doc_compiled(
2301    schema: *mut c_void,
2302    doc: *mut _xmlDoc,
2303    create_defaults: bool,
2304) -> (bool, Vec<String>) {
2305    unsafe {
2306        if schema.is_null() || doc.is_null() {
2307            return (false, Vec::new());
2308        }
2309        let schema_ref = &*(schema as *const XsdSchema);
2310        let mut ctxt = XsdValidCtxt::new();
2311        ctxt.create_defaults = create_defaults;
2312        let valid = xsd_validate_doc(schema_ref, doc, &mut ctxt);
2313        (valid, ctxt.errors)
2314    }
2315}
2316
2317/// UPSTREAM-PARITY (xmlschemas.c xmlSchemaGetComponentTargetNs / element
2318/// form handling): the namespace an element declaration matches against.
2319/// Top-level declarations live in the schema target namespace (stamped on
2320/// the component by xsd_parse_schema_node). A LOCAL declaration matches
2321/// no-namespace instance elements by default (elementFormDefault
2322/// "unqualified"), and elements in the target namespace when the
2323/// declaration itself or the schema is `elementFormDefault="qualified"`.
2324fn element_decl_ns(comp: &XsdComponent, schema: &XsdSchema) -> Option<String> {
2325    if let Some(ref ns) = comp.target_namespace {
2326        return Some(ns.clone());
2327    }
2328    let qualified = comp.form.as_deref() == Some("qualified")
2329        || (comp.form.is_none() && schema.element_form_default.as_deref() == Some("qualified"));
2330    if qualified {
2331        schema.target_namespace.clone()
2332    } else {
2333        None
2334    }
2335}
2336
2337/// Validate an element node against a component declaration.
2338///
2339/// # SAFETY
2340///
2341/// - `node` must be a valid pointer to an XML element node.
2342fn xsd_validate_element(
2343    component: &XsdComponent,
2344    node: *mut _xmlNode,
2345    schema: &XsdSchema,
2346    ctxt: &mut XsdValidCtxt,
2347) -> bool {
2348    unsafe {
2349        let mut valid = true;
2350
2351        // Get the element's local name
2352        let node_name = get_node_qname(node);
2353        let node_local = get_node_local_name(node);
2354        let node_ns = get_node_ns_href(node);
2355        let decl_ns = element_decl_ns(component, schema);
2356
2357        // UPSTREAM-PARITY (xmlschemas.c xmlSchemaValidateElem): the element
2358        // is checked against the declaration by EXPANDED name — the local
2359        // name plus the namespace the declaration is in (see element_decl_ns:
2360        // top-level = target namespace; local = no namespace unless
2361        // elementFormDefault/`form` says qualified). A prefixed element whose
2362        // prefix resolves to the declaration's namespace (e.g.
2363        // <x:books xmlns:x="urn:t"> against a global "books" in urn:t)
2364        // matches; a default-namespace child in urn:t matches a LOCAL
2365        // declaration only when the schema uses qualified element form.
2366        if let Some(ref comp_name) = component.name {
2367            if comp_name != &node_local || node_ns.as_deref() != decl_ns.as_deref() {
2368                ctxt.errors.push(format!(
2369                    "Element '{}' does not match expected '{}'",
2370                    node_name, comp_name
2371                ));
2372                ctxt.nb_errors += 1;
2373                return false;
2374            }
2375        }
2376
2377        // If there's a type definition (complexType or simpleType) among children, use it
2378        let type_comp = component.children.iter().find(|c| {
2379            c.component_type == XsdComponentType::ComplexType
2380                || c.component_type == XsdComponentType::SimpleType
2381        });
2382
2383        if let Some(tc) = type_comp {
2384            match tc.component_type {
2385                XsdComponentType::ComplexType => {
2386                    valid &= xsd_validate_complex_type(tc, node, schema, ctxt);
2387                }
2388                XsdComponentType::SimpleType => {
2389                    let text = get_node_text(node);
2390                    if let Some(ref dt) = tc.datatype {
2391                        if !xsd_validate_datatype(dt, &text, &tc.facets) {
2392                            ctxt.errors.push(format!(
2393                                "Element '{}' has invalid value '{}' for type '{:?}'",
2394                                node_name, text, dt
2395                            ));
2396                            ctxt.nb_errors += 1;
2397                            valid = false;
2398                        }
2399                    }
2400                }
2401                _ => {}
2402            }
2403        } else if let Some(ref dt) = component.datatype {
2404            // Direct datatype on the element (simple content)
2405            let text = get_node_text(node);
2406            if !xsd_validate_datatype(dt, &text, &component.facets) {
2407                ctxt.errors.push(format!(
2408                    "Element '{}' has invalid value '{}' for type '{:?}'",
2409                    node_name, text, dt
2410                ));
2411                ctxt.nb_errors += 1;
2412                valid = false;
2413            }
2414        } else if let Some(ref base_name) = component.base {
2415            // A named type reference (type="USAddress" / "bks:BookForm")
2416            // with no inline type: resolve the top-level complexType/
2417            // simpleType declaration and validate the element's content
2418            // against it. A prefixed reference (type="bks:BookForm") is
2419            // compared by its local part — the component model records the
2420            // target namespace on the top-level type (xsd_parse_schema_node
2421            // stamps schema.target_namespace) but declarations are resolved
2422            // by local name within this schema document. The pre-fix code
2423            // fell through to xsd_validate_content on THIS component, whose
2424            // children are empty (the sequence lives on the named type), so
2425            // child-content errors (e.g. a required <state> child) were never
2426            // reported.
2427            if let Some(named) = schema.components.iter().find(|c| {
2428                let ct = c.component_type;
2429                (ct == XsdComponentType::ComplexType || ct == XsdComponentType::SimpleType)
2430                    && c.name.as_deref() == Some(xsd_qname_local(base_name))
2431            }) {
2432                match named.component_type {
2433                    XsdComponentType::ComplexType => {
2434                        valid &= xsd_validate_complex_type(named, node, schema, ctxt);
2435                    }
2436                    XsdComponentType::SimpleType => {
2437                        let text = get_node_text(node);
2438                        if let Some(ref dt) = named.datatype {
2439                            if !xsd_validate_datatype(dt, &text, &named.facets) {
2440                                ctxt.errors.push(format!(
2441                                    "Element '{}' has invalid value '{}' for type '{}'",
2442                                    node_name, text, base_name
2443                                ));
2444                                ctxt.nb_errors += 1;
2445                                valid = false;
2446                            }
2447                        }
2448                    }
2449                    _ => {}
2450                }
2451            } else {
2452                // Named type not found — validate against the inline content
2453                // model directly (empty children → no content to check).
2454                valid &= xsd_validate_content(component, node, schema, ctxt);
2455            }
2456        } else {
2457            // No type information — validate children against content model
2458            valid &= xsd_validate_content(component, node, schema, ctxt);
2459        }
2460
2461        valid
2462    }
2463}
2464
2465/// Validate a complex type against an element node.
2466///
2467/// # SAFETY
2468///
2469/// - `node` must be a valid pointer to an XML element node.
2470fn xsd_validate_complex_type(
2471    component: &XsdComponent,
2472    node: *mut _xmlNode,
2473    schema: &XsdSchema,
2474    ctxt: &mut XsdValidCtxt,
2475) -> bool {
2476    {
2477        let mut valid = true;
2478
2479        // Validate attributes
2480        for attr in &component.attributes {
2481            match attr.component_type {
2482                XsdComponentType::Attribute => {
2483                    // UPSTREAM-PARITY (xmlschemas.c xmlSchemaValidateAttributes
2484                    // under XML_SCHEMA_VAL_VC_I_CREATE): a missing attribute
2485                    // carrying a schema default/fixed value is created on the
2486                    // instance BEFORE its value is validated
2487                    // (DOMDocument_schemaValidateSource_addAttrs / _addAttrs).
2488                    if ctxt.create_defaults {
2489                        unsafe { inject_default_attribute(attr, node) };
2490                    }
2491                    valid &= xsd_validate_attribute(attr, node, schema, ctxt);
2492                }
2493                XsdComponentType::AnyAttribute => {
2494                    // Any attribute is allowed
2495                }
2496                _ => {}
2497            }
2498        }
2499
2500        // Validate child content (sequence, choice, all)
2501        for child in &component.children {
2502            match child.component_type {
2503                XsdComponentType::Sequence | XsdComponentType::Choice | XsdComponentType::All => {
2504                    valid &= xsd_validate_model_group(child, node, schema, ctxt);
2505                }
2506                XsdComponentType::Restriction | XsdComponentType::Extension => {
2507                    // Handle restriction/extension content
2508                    valid &= xsd_validate_restriction_extension(child, node, schema, ctxt);
2509                }
2510                XsdComponentType::Any => {
2511                    // Any element is allowed
2512                }
2513                _ => {}
2514            }
2515        }
2516
2517        valid
2518    }
2519}
2520
2521/// Create a missing attribute on an instance element from its declaration's
2522/// default/fixed value. Only UNQUALIFIED attribute declarations are injected
2523/// (the php addAttrs tests exercise plain attributes; qualified defaults need
2524/// an in-scope namespace declaration resolution first).
2525///
2526/// # SAFETY
2527///
2528/// - `node` must be a valid pointer to an XML element node.
2529unsafe fn inject_default_attribute(component: &XsdComponent, node: *mut _xmlNode) {
2530    unsafe {
2531        let Some(ref name) = component.name else {
2532            return;
2533        };
2534        if name.is_empty() || component.ref_name.is_some() {
2535            return;
2536        }
2537        let value = match (&component.fixed_value, &component.default_value) {
2538            (Some(f), _) => f.clone(),
2539            (None, Some(d)) => d.clone(),
2540            (None, None) => return,
2541        };
2542        // Skip declarations that would need a namespace (qualified form).
2543        if component.form.as_deref() == Some("qualified") {
2544            return;
2545        }
2546        if get_attr(node, name).is_some() {
2547            return;
2548        }
2549        let (Ok(n), Ok(v)) = (
2550            std::ffi::CString::new(name.as_str()),
2551            std::ffi::CString::new(value.as_str()),
2552        ) else {
2553            return;
2554        };
2555        crate::abi::exports_xml2::xmlSetProp(
2556            node,
2557            n.as_ptr() as *const crate::abi::types::xmlChar,
2558            v.as_ptr() as *const crate::abi::types::xmlChar,
2559        );
2560    }
2561}
2562
2563/// Validate a model group (sequence, choice, all) against an element's children.
2564///
2565/// # SAFETY
2566///
2567/// - `node` must be a valid pointer to an XML element node.
2568pub fn xsd_validate_model_group(
2569    component: &XsdComponent,
2570    node: *mut _xmlNode,
2571    schema: &XsdSchema,
2572    ctxt: &mut XsdValidCtxt,
2573) -> bool {
2574    unsafe {
2575        let mut valid = true;
2576
2577        // Collect element children
2578        let mut child_nodes: Vec<*mut _xmlNode> = Vec::new();
2579        let mut child = (*node).children;
2580        while !child.is_null() {
2581            if (*child).type_ == XML_ELEMENT_NODE as c_int {
2582                child_nodes.push(child);
2583            }
2584            child = (*child).next;
2585        }
2586
2587        let mut content_bad = false;
2588
2589        match component.component_type {
2590            XsdComponentType::Sequence => {
2591                // Validate in-order
2592                let mut child_idx = 0;
2593                let mut part_counts: Vec<i32> = vec![0; component.children.len()];
2594                for (k, part) in component.children.iter().enumerate() {
2595                    let min = part.min_occurs;
2596                    let max = part.max_occurs;
2597                    let match_name = part.name.as_deref().unwrap_or("");
2598                    let match_ref = part.ref_name.as_deref().unwrap_or("");
2599
2600                    let mut count = 0;
2601                    while child_idx < child_nodes.len() && (max == -1 || count < max) {
2602                        let child_node = child_nodes[child_idx];
2603                        let child_name = get_node_qname(child_node);
2604
2605                        if part.component_type == XsdComponentType::Any
2606                            || (!match_name.is_empty() && child_name == match_name)
2607                            || (!match_ref.is_empty() && child_name == match_ref)
2608                        {
2609                            // UPSTREAM-PARITY (xmlschemas.c
2610                            // xmlSchemaValidatorPopElem): every matched
2611                            // child is checked against its type; a bad value
2612                            // raises "Element '%s': '%s' is not a valid value
2613                            // of the atomic type '%s'." BEFORE the parent's
2614                            // content model is checked.
2615                            if let Some(ref dt) = part.datatype {
2616                                let text = get_node_text(child_node);
2617                                if !xsd_validate_datatype(dt, &text, &part.facets) {
2618                                    let type_name = datatype_kind_qname(dt);
2619                                    ctxt.errors.push(format!(
2620                                        "Element '{}': '{}' is not a valid value of the atomic type '{}'.",
2621                                        child_name, text, type_name
2622                                    ));
2623                                    ctxt.nb_errors += 1;
2624                                    valid = false;
2625                                }
2626                            }
2627                            // UPSTREAM-PARITY: a matched child that itself
2628                            // carries a named type reference is validated
2629                            // recursively (e.g. <shipTo type="USAddress">
2630                            // against the USAddress content model), so a
2631                            // missing required sub-child is reported.
2632                            if part.component_type == XsdComponentType::Element
2633                                && (part.datatype.is_none())
2634                                && (!part.children.is_empty() || part.base.is_some())
2635                            {
2636                                valid &= xsd_validate_element(part, child_node, schema, ctxt);
2637                            }
2638                            count += 1;
2639                            child_idx += 1;
2640                        } else if count >= min {
2641                            break;
2642                        } else {
2643                            // UPSTREAM-PARITY (xmlschemas.c
2644                            // xmlSchemaValidateChildElem): on the first
2645                            // content-model mismatch the offending child is
2646                            // reported once and the element's content is
2647                            // marked BAD — downstream parts are NOT
2648                            // validated (no cascading "missing child" /
2649                            // "unexpected extra" errors), matching the
2650                            // oracle's single-error-per-address behavior.
2651                            ctxt.errors.push(format!(
2652                                "Element '{}': This element is not expected. Expected is ( {} ).",
2653                                child_name, match_name
2654                            ));
2655                            ctxt.nb_errors += 1;
2656                            valid = false;
2657                            content_bad = true;
2658                            break;
2659                        }
2660                    }
2661                    part_counts[k] = count;
2662                    if content_bad {
2663                        break;
2664                    }
2665
2666                    if count < min {
2667                        // UPSTREAM-PARITY (xmlschemas.c
2668                        // xmlSchemaComplexTypeErr): the missing-child error
2669                        // lists the automaton's still-expected particles —
2670                        // for a sequence, every part up to and including the
2671                        // failed one with remaining capacity (an unbounded or
2672                        // not-yet-saturated earlier part can still appear).
2673                        let mut expected: Vec<String> = Vec::new();
2674                        for (j, p) in component.children.iter().enumerate().take(k + 1) {
2675                            let name = p.name.as_deref().unwrap_or("");
2676                            if name.is_empty() {
2677                                continue;
2678                            }
2679                            let still_expected = if j == k {
2680                                true
2681                            } else {
2682                                let cj = part_counts[j];
2683                                p.max_occurs == -1 || cj < p.max_occurs.max(0)
2684                            };
2685                            if still_expected {
2686                                expected.push(name.to_string());
2687                            }
2688                        }
2689                        let node_name = get_node_qname(node);
2690                        if expected.len() > 1 {
2691                            ctxt.errors.push(format!(
2692                                "Element '{}': Missing child element(s). Expected is one of ( {} ).",
2693                                node_name,
2694                                expected.join(", ")
2695                            ));
2696                        } else if expected.len() == 1 {
2697                            ctxt.errors.push(format!(
2698                                "Element '{}': Missing child element(s). Expected is ( {} ).",
2699                                node_name, expected[0]
2700                            ));
2701                        } else {
2702                            ctxt.errors.push(format!(
2703                                "Element '{}': Missing child element(s).",
2704                                node_name
2705                            ));
2706                        }
2707                        ctxt.nb_errors += 1;
2708                        valid = false;
2709                    }
2710                }
2711
2712                // Check for unexpected extra children
2713                if content_bad {
2714                    // Content already reported as bad — do not stack an
2715                    // "unexpected extra" error on top (upstream stops once
2716                    // BAD_CONTENT is set).
2717                } else if child_idx < child_nodes.len() {
2718                    let extra = get_node_qname(child_nodes[child_idx]);
2719                    ctxt.errors
2720                        .push(format!("Unexpected element '{}' in sequence", extra));
2721                    ctxt.nb_errors += 1;
2722                    valid = false;
2723                }
2724            }
2725            XsdComponentType::Choice => {
2726                // At least one of the choices must match
2727                let mut matched = false;
2728                for child_node in &child_nodes {
2729                    let child_name = get_node_qname(*child_node);
2730                    for part in &component.children {
2731                        let match_name = part.name.as_deref().unwrap_or("");
2732                        let match_ref = part.ref_name.as_deref().unwrap_or("");
2733
2734                        if part.component_type == XsdComponentType::Any {
2735                            matched = true;
2736                        } else if (!match_name.is_empty() && child_name == match_name)
2737                            || (!match_ref.is_empty() && child_name == match_ref)
2738                        {
2739                            matched = true;
2740                            break;
2741                        }
2742                    }
2743                    if !matched {
2744                        ctxt.errors
2745                            .push(format!("Element '{}' is not valid in choice", child_name));
2746                        ctxt.nb_errors += 1;
2747                        valid = false;
2748                    }
2749                    matched = false; // Reset for next child
2750                }
2751            }
2752            XsdComponentType::All => {
2753                // All children must match in any order (maxOccurs=1)
2754                for child_node in &child_nodes {
2755                    let child_name = get_node_qname(*child_node);
2756                    let mut matched = false;
2757                    for part in &component.children {
2758                        let match_name = part.name.as_deref().unwrap_or("");
2759                        if !match_name.is_empty() && child_name == match_name {
2760                            matched = true;
2761                            break;
2762                        }
2763                    }
2764                    if !matched {
2765                        ctxt.errors.push(format!(
2766                            "Element '{}' is not valid in all group",
2767                            child_name
2768                        ));
2769                        ctxt.nb_errors += 1;
2770                        valid = false;
2771                    }
2772                }
2773            }
2774            _ => {}
2775        }
2776
2777        valid
2778    }
2779}
2780
2781/// Validate a restriction or extension content.
2782///
2783/// # SAFETY
2784///
2785/// - `node` must be a valid pointer to an XML element node.
2786fn xsd_validate_restriction_extension(
2787    component: &XsdComponent,
2788    node: *mut _xmlNode,
2789    schema: &XsdSchema,
2790    ctxt: &mut XsdValidCtxt,
2791) -> bool {
2792    unsafe {
2793        let mut valid = true;
2794
2795        // Validate attributes
2796        for attr in &component.attributes {
2797            if attr.component_type == XsdComponentType::Attribute {
2798                valid &= xsd_validate_attribute(attr, node, schema, ctxt);
2799            }
2800        }
2801
2802        // Validate child content
2803        for child in &component.children {
2804            match child.component_type {
2805                XsdComponentType::Sequence | XsdComponentType::Choice | XsdComponentType::All => {
2806                    valid &= xsd_validate_model_group(child, node, schema, ctxt);
2807                }
2808                _ => {}
2809            }
2810        }
2811
2812        // Validate datatype if present (for simple content restriction)
2813        if let Some(ref dt) = component.datatype {
2814            let text = get_node_text(node);
2815            if !xsd_validate_datatype(dt, &text, &component.facets) {
2816                let node_name = get_node_qname(node);
2817                ctxt.errors.push(format!(
2818                    "Element '{}' has invalid value '{}' for type '{:?}'",
2819                    node_name, text, dt
2820                ));
2821                ctxt.nb_errors += 1;
2822                valid = false;
2823            }
2824        }
2825
2826        valid
2827    }
2828}
2829
2830/// Validate an attribute against an element node.
2831///
2832/// # SAFETY
2833///
2834/// - `node` must be a valid pointer to an XML element node.
2835fn xsd_validate_attribute(
2836    component: &XsdComponent,
2837    node: *mut _xmlNode,
2838    _schema: &XsdSchema,
2839    ctxt: &mut XsdValidCtxt,
2840) -> bool {
2841    unsafe {
2842        let attr_name = component.name.as_deref().unwrap_or("");
2843        if attr_name.is_empty() {
2844            return true;
2845        }
2846
2847        // Check if the attribute exists on the element
2848        let attr_value = get_attr(node, attr_name);
2849
2850        let is_required = component.min_occurs > 0;
2851
2852        match attr_value {
2853            Some(ref val) => {
2854                // Validate attribute value against its datatype
2855                if let Some(ref dt) = component.datatype {
2856                    if !xsd_validate_datatype(dt, val, &component.facets) {
2857                        ctxt.errors.push(format!(
2858                            "Attribute '{}' has invalid value '{}' for type '{:?}'",
2859                            attr_name, val, dt
2860                        ));
2861                        ctxt.nb_errors += 1;
2862                        return false;
2863                    }
2864                }
2865                true
2866            }
2867            None => {
2868                if is_required {
2869                    ctxt.errors
2870                        .push(format!("Required attribute '{}' is missing", attr_name));
2871                    ctxt.nb_errors += 1;
2872                    false
2873                } else {
2874                    true
2875                }
2876            }
2877        }
2878    }
2879}
2880
2881/// Validate child content (no explicit type — just check children).
2882///
2883/// # SAFETY
2884///
2885/// - `node` must be a valid pointer to an XML element node.
2886fn xsd_validate_content(
2887    component: &XsdComponent,
2888    node: *mut _xmlNode,
2889    schema: &XsdSchema,
2890    ctxt: &mut XsdValidCtxt,
2891) -> bool {
2892    {
2893        let mut valid = true;
2894
2895        for child_comp in &component.children {
2896            match child_comp.component_type {
2897                XsdComponentType::Sequence | XsdComponentType::Choice | XsdComponentType::All => {
2898                    valid &= xsd_validate_model_group(child_comp, node, schema, ctxt);
2899                }
2900                XsdComponentType::Element => {
2901                    // Inline element declaration in a model group
2902                    valid &= xsd_validate_element_inline(child_comp, node, schema, ctxt);
2903                }
2904                _ => {}
2905            }
2906        }
2907
2908        valid
2909    }
2910}
2911
2912/// Validate an inline element declaration (element inside sequence/choice).
2913///
2914/// # SAFETY
2915///
2916/// - `node` must be a valid pointer to an XML element node.
2917fn xsd_validate_element_inline(
2918    component: &XsdComponent,
2919    node: *mut _xmlNode,
2920    schema: &XsdSchema,
2921    ctxt: &mut XsdValidCtxt,
2922) -> bool {
2923    unsafe {
2924        let mut valid = true;
2925        let mut child = (*node).children;
2926
2927        while !child.is_null() {
2928            if (*child).type_ == XML_ELEMENT_NODE as c_int {
2929                let child_name = get_node_qname(child);
2930
2931                let match_name = component.name.as_deref().unwrap_or("");
2932                let match_ref = component.ref_name.as_deref().unwrap_or("");
2933
2934                if (!match_name.is_empty() && child_name == match_name)
2935                    || (!match_ref.is_empty() && child_name == match_ref)
2936                {
2937                    // Check inline type
2938                    let type_comp = component.children.iter().find(|c| {
2939                        c.component_type == XsdComponentType::ComplexType
2940                            || c.component_type == XsdComponentType::SimpleType
2941                    });
2942
2943                    if let Some(tc) = type_comp {
2944                        match tc.component_type {
2945                            XsdComponentType::ComplexType => {
2946                                valid &= xsd_validate_complex_type(tc, child, schema, ctxt);
2947                            }
2948                            XsdComponentType::SimpleType => {
2949                                let text = get_node_text(child);
2950                                if let Some(ref dt) = tc.datatype {
2951                                    if !xsd_validate_datatype(dt, &text, &tc.facets) {
2952                                        ctxt.errors.push(format!(
2953                                            "Element '{}' has invalid value '{}'",
2954                                            child_name, text
2955                                        ));
2956                                        ctxt.nb_errors += 1;
2957                                        valid = false;
2958                                    }
2959                                }
2960                            }
2961                            _ => {}
2962                        }
2963                    } else if let Some(ref base_name) = component.base {
2964                        // Named type reference (type="USAddress") on an
2965                        // inline element declaration: resolve the top-level
2966                        // complexType/simpleType and validate the matched
2967                        // child's content against it (same resolution as
2968                        // xsd_validate_element).
2969                        if let Some(named) = schema.components.iter().find(|c| {
2970                            let ct = c.component_type;
2971                            (ct == XsdComponentType::ComplexType
2972                                || ct == XsdComponentType::SimpleType)
2973                                && c.name.as_deref() == Some(xsd_qname_local(base_name))
2974                        }) {
2975                            match named.component_type {
2976                                XsdComponentType::ComplexType => {
2977                                    valid &= xsd_validate_complex_type(named, child, schema, ctxt);
2978                                }
2979                                XsdComponentType::SimpleType => {
2980                                    let text = get_node_text(child);
2981                                    if let Some(ref dt) = named.datatype {
2982                                        if !xsd_validate_datatype(dt, &text, &named.facets) {
2983                                            ctxt.errors.push(format!(
2984                                                "Element '{}' has invalid value '{}'",
2985                                                child_name, text
2986                                            ));
2987                                            ctxt.nb_errors += 1;
2988                                            valid = false;
2989                                        }
2990                                    }
2991                                }
2992                                _ => {}
2993                            }
2994                        }
2995                    }
2996                }
2997            }
2998            child = (*child).next;
2999        }
3000
3001        valid
3002    }
3003}
3004
3005/// Get the local name of a node (the tree stores the local part in
3006/// `node->name`; the namespace, if any, lives in `node->ns`).
3007///
3008/// # SAFETY
3009///
3010/// - `node` must be a valid pointer to an _xmlNode or NULL.
3011unsafe fn get_node_local_name(node: *mut _xmlNode) -> String {
3012    if node.is_null() {
3013        return String::new();
3014    }
3015    unsafe {
3016        let name = (*node).name;
3017        if name.is_null() {
3018            return String::new();
3019        }
3020        let mut len = 0;
3021        while *name.add(len) != 0 {
3022            len += 1;
3023        }
3024        let slice = std::slice::from_raw_parts(name, len);
3025        String::from_utf8_lossy(slice).to_string()
3026    }
3027}
3028
3029/// Get the namespace URI (href) an element is in, as a string, or None when
3030/// the element carries no namespace.
3031///
3032/// # SAFETY
3033///
3034/// - `node` must be a valid pointer to an _xmlNode or NULL.
3035unsafe fn get_node_ns_href(node: *mut _xmlNode) -> Option<String> {
3036    if node.is_null() {
3037        return None;
3038    }
3039    unsafe {
3040        let ns = (*node).ns;
3041        if ns.is_null() {
3042            return None;
3043        }
3044        let href = (*ns).href;
3045        if href.is_null() {
3046            return None;
3047        }
3048        let mut len = 0;
3049        while *href.add(len) != 0 {
3050            len += 1;
3051        }
3052        let slice = std::slice::from_raw_parts(href, len);
3053        Some(String::from_utf8_lossy(slice).to_string())
3054    }
3055}
3056
3057/// UPSTREAM-PARITY (xmlschemas.c global element/type lookup): an element
3058/// declaration matches an instance element when the local names agree AND
3059/// the declaration's namespace equals the element's namespace. A declaration
3060/// with `target_namespace == None` (schema without targetNamespace, or a
3061/// local declaration under elementForm="unqualified") matches only
3062/// no-namespace elements.
3063fn xsd_element_decl_matches(comp: &XsdComponent, node_local: &str, node_ns: Option<&str>) -> bool {
3064    comp.component_type == XsdComponentType::Element
3065        && comp.name.as_deref() == Some(node_local)
3066        && comp.target_namespace.as_deref() == node_ns
3067}
3068
3069/// Get the local name of the node (same as `get_node_local_name`, but for
3070/// the schema's own element names the attribute value may carry a prefix
3071/// such as `bks:BookForm`; strip it for component-name comparisons).
3072fn xsd_qname_local(name: &str) -> &str {
3073    match name.rsplit_once(':') {
3074        Some((_, local)) => local,
3075        None => name,
3076    }
3077}
3078
3079/// Get the node's QName, but normalized like the name of a declaration
3080/// (local part only when the prefix is present). Used to build diagnostics.
3081///
3082/// # SAFETY
3083///
3084/// - `node` must be a valid pointer to an _xmlNode or NULL.
3085unsafe fn get_node_qname(node: *mut _xmlNode) -> String {
3086    if node.is_null() {
3087        return String::new();
3088    }
3089    unsafe {
3090        // Check for namespace prefix
3091        let ns = (*node).ns;
3092        let prefix = if !ns.is_null() && !(*ns).prefix.is_null() {
3093            let mut len = 0;
3094            while *(*ns).prefix.add(len) != 0 {
3095                len += 1;
3096            }
3097            let slice = std::slice::from_raw_parts((*ns).prefix, len);
3098            if let Ok(s) = std::str::from_utf8(slice) {
3099                format!("{}:", s)
3100            } else {
3101                String::new()
3102            }
3103        } else {
3104            String::new()
3105        };
3106
3107        let name = (*node).name;
3108        if name.is_null() {
3109            return String::new();
3110        }
3111        let mut len = 0;
3112        while *name.add(len) != 0 {
3113            len += 1;
3114        }
3115        let slice = std::slice::from_raw_parts(name, len);
3116        if let Ok(s) = std::str::from_utf8(slice) {
3117            format!("{}{}", prefix, s)
3118        } else {
3119            String::new()
3120        }
3121    }
3122}
3123
3124// ═══════════════════════════════════════════════════════════════════════════════
3125// C ABI Functions
3126// ═══════════════════════════════════════════════════════════════════════════════
3127
3128// These are the C-compatible entry points that get exported via the ABI layer.
3129// They use raw pointers and follow libxml2's calling conventions.
3130
3131/// XML Schema parser context (upstream `xmlSchemaParserCtxt`).
3132///
3133/// Owns the eagerly-parsed schema; `xmlSchemaParse` hands out a NEW schema
3134/// object (a clone) so the context and the schema have separate lifetimes,
3135/// exactly as upstream callers expect (lxml: `xmlSchemaParse` then
3136/// `xmlSchemaFreeParserCtxt`, with `xmlSchemaFree` on the schema at
3137/// dealloc). The pre-fix implementation returned the context as the schema
3138/// pointer, so `xmlSchemaFreeParserCtxt` freed the schema out from under
3139/// consumers — a use-after-free (Phase 14 lxml schema court).
3140/// Why an eager schema-document parse failed. Upstream `xmlSchemaParse`
3141/// reports a different diagnostic per failure stage, so the reason must be
3142/// remembered until the (php) caller invokes `xmlSchemaParse`.
3143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3144pub(crate) enum XsdParseFail {
3145    /// The main schema resource could not be located/loaded
3146    /// ("Failed to locate the main schema resource at '%s'.").
3147    Resource,
3148    /// The schema document could not be parsed (not well-formed XML, or its
3149    /// root is not `<schema>`) ("Failed to parse the XML resource '%s'.").
3150    Document,
3151}
3152
3153pub(crate) struct XsdParserCtxt {
3154    /// The parsed schema, if parsing succeeded.
3155    pub(crate) schema: Option<XsdSchema>,
3156    /// The failure stage, when `schema` is None.
3157    pub(crate) fail: Option<XsdParseFail>,
3158    /// The schema resource name. File contexts carry the path; memory
3159    /// contexts are None (upstream names the in-memory resource
3160    /// "in_memory_buffer").
3161    pub(crate) url: Option<String>,
3162    /// True when the schema text came from a memory buffer.
3163    pub(crate) mem: bool,
3164}
3165
3166impl XsdParserCtxt {
3167    const fn empty() -> Self {
3168        Self {
3169            schema: None,
3170            fail: None,
3171            url: None,
3172            mem: false,
3173        }
3174    }
3175}
3176
3177/// Create a new schema parser context from a URL.
3178///
3179/// # UPSTREAM-PARITY
3180///
3181/// ```c
3182/// xmlSchemaParserCtxtPtr xmlSchemaNewParserCtxt(const char *URL);
3183/// ```
3184///
3185/// # SAFETY
3186///
3187/// - `url` must be a valid null-terminated C string or NULL.
3188#[no_mangle]
3189pub unsafe extern "C" fn xmlSchemaNewParserCtxt(url: *const c_char) -> *mut c_void {
3190    if url.is_null() {
3191        // Empty context (no schema): xmlSchemaParse returns NULL.
3192        return Box::into_raw(Box::new(XsdParserCtxt::empty())) as *mut c_void;
3193    }
3194
3195    let url_str = unsafe {
3196        let mut len = 0;
3197        while *url.add(len) != 0 {
3198            len += 1;
3199        }
3200        let slice = std::slice::from_raw_parts(url as *const u8, len);
3201        String::from_utf8_lossy(slice).to_string()
3202    };
3203
3204    // UPSTREAM-PARITY (schemas.c xmlSchemaNewParserCtxt + xmlSchemaParse):
3205    // the schema document is opened through the standard input machinery, so
3206    // an unreadable resource raises the upstream "I/O warning : failed to
3207    // load external entity ..." and the well-formedness diagnostics of a
3208    // malformed document carry the REAL resource name (php's
3209    // DOMDocument_schemaValidate_error1/error5 pin both). The failure stage
3210    // is remembered so xmlSchemaParse can report it through the parser error
3211    // callbacks (php registers those after this constructor returns).
3212    let (schema, fail) = if url_str.is_empty() {
3213        (None, Some(XsdParseFail::Resource))
3214    } else {
3215        let url_c = std::ffi::CString::new(url_str.clone()).ok();
3216        let mut parsed: Option<XsdSchema> = None;
3217        let mut failed = XsdParseFail::Document;
3218        if let Some(c) = url_c {
3219            let doc = crate::abi::exports_xml2::xmlParseFile(c.as_ptr());
3220            if doc.is_null() {
3221                // xmlParseFile returns NULL both when the resource cannot be
3222                // opened and when its content is not well-formed; the php
3223                // suite loads real files, so the filesystem decides which
3224                // upstream diagnostic applies.
3225                failed = if std::fs::metadata(&url_str).is_err() {
3226                    XsdParseFail::Resource
3227                } else {
3228                    XsdParseFail::Document
3229                };
3230            } else {
3231                let result = unsafe { xsd_parse_schema_doc(doc) };
3232                unsafe {
3233                    crate::abi::exports_xml2::xmlFreeDoc(doc);
3234                }
3235                match result {
3236                    Ok(s) => parsed = Some(s),
3237                    Err(_) => failed = XsdParseFail::Document,
3238                }
3239            }
3240        } else {
3241            failed = XsdParseFail::Resource;
3242        }
3243        if parsed.is_some() {
3244            (parsed, None)
3245        } else {
3246            (None, Some(failed))
3247        }
3248    };
3249    Box::into_raw(Box::new(XsdParserCtxt {
3250        schema,
3251        fail,
3252        url: Some(url_str),
3253        mem: false,
3254    })) as *mut c_void
3255}
3256
3257/// Create a new schema parser context from a memory buffer.
3258///
3259/// # UPSTREAM-PARITY
3260///
3261/// ```c
3262/// xmlSchemaParserCtxtPtr xmlSchemaNewMemParserCtxt(const char *buffer, int size);
3263/// ```
3264///
3265/// # SAFETY
3266///
3267/// - `buffer` must be a valid pointer to a buffer of at least `size` bytes.
3268#[no_mangle]
3269pub unsafe extern "C" fn xmlSchemaNewMemParserCtxt(
3270    buffer: *const c_char,
3271    size: c_int,
3272) -> *mut c_void {
3273    if buffer.is_null() || size <= 0 {
3274        return ptr::null_mut();
3275    }
3276
3277    // Parse the schema immediately and keep it in the parser context;
3278    // `xmlSchemaParse` hands out a fresh schema object (Phase 14: the
3279    // context and the schema must have separate lifetimes — lxml frees the
3280    // context right after xmlSchemaParse and the schema at dealloc). The
3281    // document is parsed WITHOUT a resource name so the diagnostics keep the
3282    // upstream "Entity: line N: parser error : ..." shape and the failure is
3283    // reported as "Failed to parse the XML resource 'in_memory_buffer'.".
3284    let buf_slice = unsafe { std::slice::from_raw_parts(buffer as *const u8, size as usize) };
3285    let doc = unsafe {
3286        crate::abi::exports_xml2::xmlReadMemory(
3287            buf_slice.as_ptr() as *const c_char,
3288            buf_slice.len() as c_int,
3289            ptr::null(),
3290            ptr::null(),
3291            0,
3292        )
3293    };
3294    let (schema, fail) = if doc.is_null() {
3295        (None, Some(XsdParseFail::Document))
3296    } else {
3297        let result = unsafe { xsd_parse_schema_doc(doc) };
3298        unsafe {
3299            crate::abi::exports_xml2::xmlFreeDoc(doc);
3300        }
3301        match result {
3302            Ok(s) => (Some(s), None),
3303            Err(_) => (None, Some(XsdParseFail::Document)),
3304        }
3305    };
3306    Box::into_raw(Box::new(XsdParserCtxt {
3307        schema,
3308        fail,
3309        url: None,
3310        mem: true,
3311    })) as *mut c_void
3312}
3313
3314/// Parse a schema.
3315///
3316/// # UPSTREAM-PARITY
3317///
3318/// ```c
3319/// xmlSchemaPtr xmlSchemaParse(xmlSchemaParserCtxtPtr ctxt);
3320/// ```
3321///
3322/// # SAFETY
3323///
3324/// - `ctxt` must be a valid pointer to a parser context, or NULL.
3325#[no_mangle]
3326pub unsafe extern "C" fn xmlSchemaParse(ctxt: *mut c_void) -> *mut c_void {
3327    if ctxt.is_null() {
3328        return ptr::null_mut();
3329    }
3330
3331    // UPSTREAM-PARITY (schemas.c xmlSchemaParse): the parser context owns
3332    // the parsed schema; this hands out a NEW schema object so the caller
3333    // can free the context independently (lxml frees the context right
3334    // after this call). The pre-fix implementation returned the context
3335    // itself, so xmlSchemaFreeParserCtxt freed the schema out from under
3336    // the consumer. When the eager parse failed, the stage diagnostic is
3337    // reported through the registered parser handlers (php registers them
3338    // after the context constructor) and NULL is returned so the caller
3339    // reports "Invalid Schema" (php DOMDocument_schemaValidate_error1/5 +
3340    // schemaValidateSource_error1).
3341    let pctxt = unsafe { &*ctxt.cast::<XsdParserCtxt>() };
3342    match &pctxt.schema {
3343        Some(schema) => Box::into_raw(Box::new(schema.clone())) as *mut c_void,
3344        None => {
3345            let msg = match pctxt.fail {
3346                Some(XsdParseFail::Resource) => pctxt
3347                    .url
3348                    .as_deref()
3349                    .map(|u| format!("Failed to locate the main schema resource at '{}'.\n", u)),
3350                Some(XsdParseFail::Document) => Some(format!(
3351                    "Failed to parse the XML resource '{}'.\n",
3352                    pctxt
3353                        .url
3354                        .as_deref()
3355                        .unwrap_or(if pctxt.mem { "in_memory_buffer" } else { "" })
3356                )),
3357                None => None,
3358            };
3359            if let Some(m) = msg {
3360                crate::abi::exports_schema::dispatch_parser_error(ctxt as usize, &m);
3361            }
3362            ptr::null_mut()
3363        }
3364    }
3365}
3366
3367/// Free a schema.
3368///
3369/// # UPSTREAM-PARITY
3370///
3371/// ```c
3372/// void xmlSchemaFree(xmlSchemaPtr schema);
3373/// ```
3374///
3375/// # SAFETY
3376///
3377/// - `schema` must be a valid pointer to a schema, or NULL.
3378#[no_mangle]
3379pub unsafe extern "C" fn xmlSchemaFree(schema: *mut c_void) {
3380    if schema.is_null() {
3381        return;
3382    }
3383    // SAFETY: Reconstruct the Box to drop it.
3384    unsafe {
3385        let _ = Box::from_raw(schema as *mut XsdSchema);
3386    }
3387}
3388
3389/// Validate a document against a schema.
3390///
3391/// # UPSTREAM-PARITY
3392///
3393/// ```c
3394/// int xmlSchemaValidateDoc(xmlSchemaValidCtxtPtr ctxt, xmlDocPtr doc);
3395/// ```
3396///
3397/// Returns 0 if valid, -1 on internal error, or the number of validation errors.
3398///
3399/// # SAFETY
3400///
3401/// - `ctxt` must be a valid pointer to a validation context, or NULL.
3402/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
3403#[no_mangle]
3404pub unsafe extern "C" fn xmlSchemaValidateDoc(ctxt: *mut c_void, doc: *mut _xmlDoc) -> c_int {
3405    if ctxt.is_null() || doc.is_null() {
3406        return -1;
3407    }
3408
3409    unsafe {
3410        let valid_ctxt = &mut *(ctxt as *mut XsdValidCtxt);
3411        let schema = match &valid_ctxt.schema {
3412            Some(s) => s,
3413            None => return -1,
3414        };
3415
3416        let mut temp_ctxt = XsdValidCtxt::new();
3417        temp_ctxt.schema = Some(schema.clone());
3418        // LIBXML_SCHEMA_CREATE (xmlSchemaSetValidOptions with
3419        // XML_SCHEMA_VAL_VC_I_CREATE) makes the validator inject missing
3420        // default/fixed attributes into the instance (upstream
3421        // xmlSchemaValidateDoc honours the context option).
3422        temp_ctxt.create_defaults =
3423            crate::abi::exports_schema::valid_ctxt_options(ctxt as usize) & (1 << 0) != 0;
3424
3425        let valid = xsd_validate_doc(schema, doc, &mut temp_ctxt);
3426
3427        if valid {
3428            0
3429        } else {
3430            valid_ctxt.errors = temp_ctxt.errors;
3431            valid_ctxt.nb_errors = temp_ctxt.nb_errors;
3432            // UPSTREAM-PARITY: forward each recorded error to the context's
3433            // registered handlers (xmlSchemaSetValidErrors /
3434            // xmlSchemaSetValidStructuredErrors) so consumers like lxml's
3435            // XMLSchema.validate (which installs serror = _receiveError)
3436            // populate their error_log.
3437            crate::abi::exports_schema::dispatch_valid_errors(ctxt as usize, &valid_ctxt.errors);
3438            temp_ctxt.nb_errors
3439        }
3440    }
3441}
3442
3443/// Free a schema parser context.
3444///
3445/// # UPSTREAM-PARITY
3446///
3447/// ```c
3448/// void xmlSchemaFreeParserCtxt(xmlSchemaParserCtxtPtr ctxt);
3449/// ```
3450///
3451/// # SAFETY
3452///
3453/// - `ctxt` must be a valid pointer to a parser context, or NULL.
3454#[no_mangle]
3455pub unsafe extern "C" fn xmlSchemaFreeParserCtxt(ctxt: *mut c_void) {
3456    if ctxt.is_null() {
3457        return;
3458    }
3459    // SAFETY: Reconstruct the Box to drop it (the context is a separate
3460    // allocation from the schema handed out by xmlSchemaParse).
3461    unsafe {
3462        let _ = Box::from_raw(ctxt as *mut XsdParserCtxt);
3463    }
3464}
3465
3466/// Free a schema validation context.
3467///
3468/// # UPSTREAM-PARITY
3469///
3470/// ```c
3471/// void xmlSchemaFreeValidCtxt(xmlSchemaValidCtxtPtr ctxt);
3472/// ```
3473///
3474/// # SAFETY
3475///
3476/// - `ctxt` must be a valid pointer to a validation context, or NULL.
3477#[no_mangle]
3478pub unsafe extern "C" fn xmlSchemaFreeValidCtxt(ctxt: *mut c_void) {
3479    if ctxt.is_null() {
3480        return;
3481    }
3482    // SAFETY: Reconstruct the Box to drop it.
3483    unsafe {
3484        let _ = Box::from_raw(ctxt as *mut XsdValidCtxt);
3485    }
3486}
3487
3488/// Create a new schema validation context.
3489///
3490/// # UPSTREAM-PARITY
3491///
3492/// ```c
3493/// xmlSchemaValidCtxtPtr xmlSchemaNewValidCtxt(xmlSchemaPtr schema);
3494/// ```
3495///
3496/// # SAFETY
3497///
3498/// - `schema` must be a valid pointer to a schema, or NULL.
3499#[no_mangle]
3500pub unsafe extern "C" fn xmlSchemaNewValidCtxt(schema: *mut c_void) -> *mut c_void {
3501    let mut ctxt = XsdValidCtxt::new();
3502
3503    if !schema.is_null() {
3504        // SAFETY: The schema pointer is assumed to be a valid XsdSchema.
3505        unsafe {
3506            let schema_ref = &*(schema as *const XsdSchema);
3507            ctxt.schema = Some(schema_ref.clone());
3508        }
3509    }
3510
3511    let boxed = Box::new(ctxt);
3512    Box::into_raw(boxed) as *mut c_void
3513}
3514
3515// ═══════════════════════════════════════════════════════════════════════════════
3516// Tests
3517// ═══════════════════════════════════════════════════════════════════════════════
3518
3519#[cfg(test)]
3520mod tests {
3521    use super::*;
3522
3523    // ── Datatype Validation Tests ─────────────────────────────────────────
3524
3525    #[test]
3526    fn test_validate_string() {
3527        assert!(xsd_validate_datatype(
3528            &XsdDatatypeKind::String,
3529            "hello",
3530            &[]
3531        ));
3532        assert!(xsd_validate_datatype(&XsdDatatypeKind::String, "", &[]));
3533    }
3534
3535    #[test]
3536    fn test_validate_boolean() {
3537        assert!(xsd_validate_datatype(
3538            &XsdDatatypeKind::Boolean,
3539            "true",
3540            &[]
3541        ));
3542        assert!(xsd_validate_datatype(
3543            &XsdDatatypeKind::Boolean,
3544            "false",
3545            &[]
3546        ));
3547        assert!(xsd_validate_datatype(&XsdDatatypeKind::Boolean, "1", &[]));
3548        assert!(xsd_validate_datatype(&XsdDatatypeKind::Boolean, "0", &[]));
3549        assert!(!xsd_validate_datatype(
3550            &XsdDatatypeKind::Boolean,
3551            "yes",
3552            &[]
3553        ));
3554        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Boolean, "no", &[]));
3555    }
3556
3557    #[test]
3558    fn test_validate_integer() {
3559        assert!(xsd_validate_datatype(&XsdDatatypeKind::Integer, "42", &[]));
3560        assert!(xsd_validate_datatype(&XsdDatatypeKind::Integer, "-42", &[]));
3561        assert!(xsd_validate_datatype(&XsdDatatypeKind::Integer, "+42", &[]));
3562        assert!(!xsd_validate_datatype(
3563            &XsdDatatypeKind::Integer,
3564            "12.5",
3565            &[]
3566        ));
3567        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Integer, "", &[]));
3568        assert!(!xsd_validate_datatype(
3569            &XsdDatatypeKind::Integer,
3570            "abc",
3571            &[]
3572        ));
3573    }
3574
3575    #[test]
3576    fn test_validate_decimal() {
3577        assert!(xsd_validate_datatype(&XsdDatatypeKind::Decimal, "42", &[]));
3578        assert!(xsd_validate_datatype(
3579            &XsdDatatypeKind::Decimal,
3580            "12.5",
3581            &[]
3582        ));
3583        assert!(xsd_validate_datatype(
3584            &XsdDatatypeKind::Decimal,
3585            "-3.14",
3586            &[]
3587        ));
3588        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Decimal, "", &[]));
3589    }
3590
3591    #[test]
3592    fn test_validate_float() {
3593        assert!(xsd_validate_datatype(&XsdDatatypeKind::Float, "3.14", &[]));
3594        assert!(xsd_validate_datatype(&XsdDatatypeKind::Float, "INF", &[]));
3595        assert!(xsd_validate_datatype(&XsdDatatypeKind::Float, "-INF", &[]));
3596        assert!(xsd_validate_datatype(&XsdDatatypeKind::Float, "NaN", &[]));
3597        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Float, "", &[]));
3598    }
3599
3600    #[test]
3601    fn test_validate_positive_integer() {
3602        assert!(xsd_validate_datatype(
3603            &XsdDatatypeKind::PositiveInteger,
3604            "1",
3605            &[]
3606        ));
3607        assert!(xsd_validate_datatype(
3608            &XsdDatatypeKind::PositiveInteger,
3609            "100",
3610            &[]
3611        ));
3612        assert!(!xsd_validate_datatype(
3613            &XsdDatatypeKind::PositiveInteger,
3614            "0",
3615            &[]
3616        ));
3617        assert!(!xsd_validate_datatype(
3618            &XsdDatatypeKind::PositiveInteger,
3619            "-1",
3620            &[]
3621        ));
3622    }
3623
3624    #[test]
3625    fn test_validate_non_negative_integer() {
3626        assert!(xsd_validate_datatype(
3627            &XsdDatatypeKind::NonNegativeInteger,
3628            "0",
3629            &[]
3630        ));
3631        assert!(xsd_validate_datatype(
3632            &XsdDatatypeKind::NonNegativeInteger,
3633            "42",
3634            &[]
3635        ));
3636        assert!(!xsd_validate_datatype(
3637            &XsdDatatypeKind::NonNegativeInteger,
3638            "-1",
3639            &[]
3640        ));
3641    }
3642
3643    #[test]
3644    fn test_validate_int_range() {
3645        assert!(xsd_validate_datatype(
3646            &XsdDatatypeKind::Int,
3647            "2147483647",
3648            &[]
3649        ));
3650        assert!(xsd_validate_datatype(
3651            &XsdDatatypeKind::Int,
3652            "-2147483648",
3653            &[]
3654        ));
3655        assert!(!xsd_validate_datatype(
3656            &XsdDatatypeKind::Int,
3657            "2147483648",
3658            &[]
3659        ));
3660    }
3661
3662    #[test]
3663    fn test_validate_short_range() {
3664        assert!(xsd_validate_datatype(&XsdDatatypeKind::Short, "32767", &[]));
3665        assert!(xsd_validate_datatype(
3666            &XsdDatatypeKind::Short,
3667            "-32768",
3668            &[]
3669        ));
3670        assert!(!xsd_validate_datatype(
3671            &XsdDatatypeKind::Short,
3672            "32768",
3673            &[]
3674        ));
3675    }
3676
3677    #[test]
3678    fn test_validate_byte_range() {
3679        assert!(xsd_validate_datatype(&XsdDatatypeKind::Byte, "127", &[]));
3680        assert!(xsd_validate_datatype(&XsdDatatypeKind::Byte, "-128", &[]));
3681        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Byte, "128", &[]));
3682    }
3683
3684    #[test]
3685    fn test_validate_date_time() {
3686        assert!(xsd_validate_datatype(
3687            &XsdDatatypeKind::DateTime,
3688            "2023-01-15T10:30:00",
3689            &[]
3690        ));
3691        assert!(!xsd_validate_datatype(
3692            &XsdDatatypeKind::DateTime,
3693            "not-a-date",
3694            &[]
3695        ));
3696    }
3697
3698    #[test]
3699    fn test_validate_date() {
3700        assert!(xsd_validate_datatype(
3701            &XsdDatatypeKind::Date,
3702            "2023-01-15",
3703            &[]
3704        ));
3705        assert!(!xsd_validate_datatype(
3706            &XsdDatatypeKind::Date,
3707            "2023/01/15",
3708            &[]
3709        ));
3710    }
3711
3712    #[test]
3713    fn test_validate_time() {
3714        assert!(xsd_validate_datatype(
3715            &XsdDatatypeKind::Time,
3716            "10:30:00",
3717            &[]
3718        ));
3719        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Time, "10:30", &[]));
3720    }
3721
3722    #[test]
3723    fn test_validate_hex_binary() {
3724        assert!(xsd_validate_datatype(
3725            &XsdDatatypeKind::HexBinary,
3726            "0FA1",
3727            &[]
3728        ));
3729        assert!(xsd_validate_datatype(&XsdDatatypeKind::HexBinary, "", &[]));
3730        assert!(!xsd_validate_datatype(
3731            &XsdDatatypeKind::HexBinary,
3732            "0FG1",
3733            &[]
3734        ));
3735        assert!(!xsd_validate_datatype(
3736            &XsdDatatypeKind::HexBinary,
3737            "0FA",
3738            &[]
3739        ));
3740    }
3741
3742    #[test]
3743    fn test_validate_base64() {
3744        assert!(xsd_validate_datatype(
3745            &XsdDatatypeKind::Base64Binary,
3746            "SGVsbG8=",
3747            &[]
3748        ));
3749        assert!(xsd_validate_datatype(
3750            &XsdDatatypeKind::Base64Binary,
3751            "",
3752            &[]
3753        ));
3754        assert!(!xsd_validate_datatype(
3755            &XsdDatatypeKind::Base64Binary,
3756            "Hello World!",
3757            &[]
3758        ));
3759    }
3760
3761    #[test]
3762    fn test_validate_ncname() {
3763        assert!(xsd_validate_datatype(
3764            &XsdDatatypeKind::NCName,
3765            "myElement",
3766            &[]
3767        ));
3768        assert!(xsd_validate_datatype(&XsdDatatypeKind::NCName, "_foo", &[]));
3769        assert!(!xsd_validate_datatype(
3770            &XsdDatatypeKind::NCName,
3771            "123abc",
3772            &[]
3773        ));
3774        assert!(!xsd_validate_datatype(&XsdDatatypeKind::NCName, "", &[]));
3775    }
3776
3777    #[test]
3778    fn test_validate_qname() {
3779        assert!(xsd_validate_datatype(
3780            &XsdDatatypeKind::QName,
3781            "ns:local",
3782            &[]
3783        ));
3784        assert!(xsd_validate_datatype(&XsdDatatypeKind::QName, "local", &[]));
3785        assert!(!xsd_validate_datatype(&XsdDatatypeKind::QName, "", &[]));
3786    }
3787
3788    #[test]
3789    fn test_validate_token() {
3790        assert!(xsd_validate_datatype(&XsdDatatypeKind::Token, "hello", &[]));
3791        assert!(!xsd_validate_datatype(
3792            &XsdDatatypeKind::Token,
3793            " hello",
3794            &[]
3795        ));
3796        assert!(!xsd_validate_datatype(
3797            &XsdDatatypeKind::Token,
3798            "hello ",
3799            &[]
3800        ));
3801        assert!(!xsd_validate_datatype(
3802            &XsdDatatypeKind::Token,
3803            "hello  world",
3804            &[]
3805        ));
3806        assert!(!xsd_validate_datatype(
3807            &XsdDatatypeKind::Token,
3808            "hello\tworld",
3809            &[]
3810        ));
3811    }
3812
3813    #[test]
3814    fn test_validate_language() {
3815        assert!(xsd_validate_datatype(&XsdDatatypeKind::Language, "en", &[]));
3816        assert!(xsd_validate_datatype(
3817            &XsdDatatypeKind::Language,
3818            "en-US",
3819            &[]
3820        ));
3821        assert!(xsd_validate_datatype(
3822            &XsdDatatypeKind::Language,
3823            "zh-CN",
3824            &[]
3825        ));
3826        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Language, "", &[]));
3827        assert!(!xsd_validate_datatype(
3828            &XsdDatatypeKind::Language,
3829            "123",
3830            &[]
3831        ));
3832    }
3833
3834    #[test]
3835    fn test_validate_name() {
3836        assert!(xsd_validate_datatype(
3837            &XsdDatatypeKind::Name,
3838            "myElement",
3839            &[]
3840        ));
3841        assert!(xsd_validate_datatype(
3842            &XsdDatatypeKind::Name,
3843            "ns:local",
3844            &[]
3845        ));
3846        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Name, "", &[]));
3847    }
3848
3849    #[test]
3850    fn test_validate_nmtoken() {
3851        assert!(xsd_validate_datatype(
3852            &XsdDatatypeKind::Nmtoken,
3853            "token123",
3854            &[]
3855        ));
3856        assert!(xsd_validate_datatype(
3857            &XsdDatatypeKind::Nmtoken,
3858            "123token",
3859            &[]
3860        ));
3861        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Nmtoken, "", &[]));
3862    }
3863
3864    #[test]
3865    fn test_validate_duration() {
3866        assert!(xsd_validate_datatype(
3867            &XsdDatatypeKind::Duration,
3868            "P1Y2M3DT4H5M6S",
3869            &[]
3870        ));
3871        assert!(xsd_validate_datatype(
3872            &XsdDatatypeKind::Duration,
3873            "P1Y",
3874            &[]
3875        ));
3876        assert!(!xsd_validate_datatype(&XsdDatatypeKind::Duration, "", &[]));
3877    }
3878
3879    #[test]
3880    fn test_validate_g_year() {
3881        assert!(xsd_validate_datatype(&XsdDatatypeKind::GYear, "2023", &[]));
3882        assert!(!xsd_validate_datatype(&XsdDatatypeKind::GYear, "", &[]));
3883    }
3884
3885    #[test]
3886    fn test_validate_g_month() {
3887        assert!(xsd_validate_datatype(&XsdDatatypeKind::GMonth, "--05", &[]));
3888        assert!(!xsd_validate_datatype(&XsdDatatypeKind::GMonth, "", &[]));
3889    }
3890
3891    #[test]
3892    fn test_validate_g_day() {
3893        assert!(xsd_validate_datatype(&XsdDatatypeKind::GDay, "---15", &[]));
3894        assert!(!xsd_validate_datatype(&XsdDatatypeKind::GDay, "", &[]));
3895    }
3896
3897    // ── Facet Validation Tests ────────────────────────────────────────────
3898
3899    #[test]
3900    fn test_facet_min_length() {
3901        let facets = vec![(XsdDatatypeKind::FacetMinLength, "3".to_string())];
3902        assert!(xsd_validate_datatype(
3903            &XsdDatatypeKind::String,
3904            "hello",
3905            &facets
3906        ));
3907        assert!(xsd_validate_datatype(
3908            &XsdDatatypeKind::String,
3909            "abc",
3910            &facets
3911        ));
3912        assert!(!xsd_validate_datatype(
3913            &XsdDatatypeKind::String,
3914            "ab",
3915            &facets
3916        ));
3917    }
3918
3919    #[test]
3920    fn test_facet_max_length() {
3921        let facets = vec![(XsdDatatypeKind::FacetMaxLength, "3".to_string())];
3922        assert!(xsd_validate_datatype(
3923            &XsdDatatypeKind::String,
3924            "ab",
3925            &facets
3926        ));
3927        assert!(xsd_validate_datatype(
3928            &XsdDatatypeKind::String,
3929            "abc",
3930            &facets
3931        ));
3932        assert!(!xsd_validate_datatype(
3933            &XsdDatatypeKind::String,
3934            "abcd",
3935            &facets
3936        ));
3937    }
3938
3939    #[test]
3940    fn test_facet_length() {
3941        let facets = vec![(XsdDatatypeKind::FacetLength, "3".to_string())];
3942        assert!(xsd_validate_datatype(
3943            &XsdDatatypeKind::String,
3944            "abc",
3945            &facets
3946        ));
3947        assert!(!xsd_validate_datatype(
3948            &XsdDatatypeKind::String,
3949            "ab",
3950            &facets
3951        ));
3952        assert!(!xsd_validate_datatype(
3953            &XsdDatatypeKind::String,
3954            "abcd",
3955            &facets
3956        ));
3957    }
3958
3959    #[test]
3960    fn test_facet_min_inclusive() {
3961        let facets = vec![(XsdDatatypeKind::FacetMinInclusive, "5".to_string())];
3962        assert!(xsd_validate_datatype(
3963            &XsdDatatypeKind::Integer,
3964            "5",
3965            &facets
3966        ));
3967        assert!(xsd_validate_datatype(
3968            &XsdDatatypeKind::Integer,
3969            "10",
3970            &facets
3971        ));
3972        assert!(!xsd_validate_datatype(
3973            &XsdDatatypeKind::Integer,
3974            "3",
3975            &facets
3976        ));
3977    }
3978
3979    #[test]
3980    fn test_facet_max_inclusive() {
3981        let facets = vec![(XsdDatatypeKind::FacetMaxInclusive, "10".to_string())];
3982        assert!(xsd_validate_datatype(
3983            &XsdDatatypeKind::Integer,
3984            "10",
3985            &facets
3986        ));
3987        assert!(xsd_validate_datatype(
3988            &XsdDatatypeKind::Integer,
3989            "5",
3990            &facets
3991        ));
3992        assert!(!xsd_validate_datatype(
3993            &XsdDatatypeKind::Integer,
3994            "15",
3995            &facets
3996        ));
3997    }
3998
3999    #[test]
4000    fn test_facet_pattern_digits() {
4001        let facets = vec![(XsdDatatypeKind::FacetPattern, r"\d+".to_string())];
4002        assert!(xsd_validate_datatype(
4003            &XsdDatatypeKind::String,
4004            "123",
4005            &facets
4006        ));
4007        assert!(!xsd_validate_datatype(
4008            &XsdDatatypeKind::String,
4009            "abc",
4010            &facets
4011        ));
4012    }
4013
4014    #[test]
4015    fn test_facet_pattern_alpha() {
4016        let facets = vec![(XsdDatatypeKind::FacetPattern, r"[a-zA-Z]+".to_string())];
4017        assert!(xsd_validate_datatype(
4018            &XsdDatatypeKind::String,
4019            "hello",
4020            &facets
4021        ));
4022        assert!(!xsd_validate_datatype(
4023            &XsdDatatypeKind::String,
4024            "123",
4025            &facets
4026        ));
4027    }
4028
4029    // ── Schema Parsing Tests ──────────────────────────────────────────────
4030
4031    #[test]
4032    fn test_parse_empty_schema() {
4033        let schema_xml = r#"<?xml version="1.0"?>
4034            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4035            </xs:schema>"#;
4036
4037        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4038        assert!(schema.components.is_empty());
4039    }
4040
4041    #[test]
4042    fn test_parse_schema_with_target_namespace() {
4043        let schema_xml = r#"<?xml version="1.0"?>
4044            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
4045                       targetNamespace="http://example.com/ns">
4046            </xs:schema>"#;
4047
4048        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4049        assert_eq!(
4050            schema.target_namespace,
4051            Some("http://example.com/ns".to_string())
4052        );
4053    }
4054
4055    #[test]
4056    fn test_parse_simple_element() {
4057        let schema_xml = r#"<?xml version="1.0"?>
4058            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4059                <xs:element name="name" type="xs:string"/>
4060            </xs:schema>"#;
4061
4062        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4063        assert_eq!(schema.components.len(), 1);
4064        assert_eq!(
4065            schema.components[0].component_type,
4066            XsdComponentType::Element
4067        );
4068        assert_eq!(schema.components[0].name, Some("name".to_string()));
4069        assert_eq!(schema.components[0].datatype, Some(XsdDatatypeKind::String));
4070    }
4071
4072    #[test]
4073    fn test_parse_integer_element() {
4074        let schema_xml = r#"<?xml version="1.0"?>
4075            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4076                <xs:element name="age" type="xs:integer"/>
4077            </xs:schema>"#;
4078
4079        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4080        assert_eq!(schema.components.len(), 1);
4081        assert_eq!(schema.components[0].name, Some("age".to_string()));
4082        assert_eq!(
4083            schema.components[0].datatype,
4084            Some(XsdDatatypeKind::Integer)
4085        );
4086    }
4087
4088    #[test]
4089    fn test_parse_element_with_attributes() {
4090        let schema_xml = r#"<?xml version="1.0"?>
4091            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4092                <xs:element name="product">
4093                    <xs:complexType>
4094                        <xs:sequence>
4095                            <xs:element name="name" type="xs:string"/>
4096                            <xs:element name="price" type="xs:decimal"/>
4097                        </xs:sequence>
4098                        <xs:attribute name="id" type="xs:integer" use="required"/>
4099                    </xs:complexType>
4100                </xs:element>
4101            </xs:schema>"#;
4102
4103        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4104        assert_eq!(schema.components.len(), 1);
4105        assert_eq!(schema.components[0].name, Some("product".to_string()));
4106
4107        // Should have a complexType child
4108        let ct = &schema.components[0].children;
4109        let complex_type = ct
4110            .iter()
4111            .find(|c| c.component_type == XsdComponentType::ComplexType);
4112        assert!(complex_type.is_some());
4113        if let Some(ctc) = complex_type {
4114            assert_eq!(ctc.attributes.len(), 1);
4115            assert_eq!(ctc.attributes[0].name, Some("id".to_string()));
4116            assert_eq!(ctc.attributes[0].datatype, Some(XsdDatatypeKind::Integer));
4117        }
4118    }
4119
4120    #[test]
4121    fn test_parse_complex_type_with_sequence() {
4122        let schema_xml = r#"<?xml version="1.0"?>
4123            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4124                <xs:complexType name="AddressType">
4125                    <xs:sequence>
4126                        <xs:element name="street" type="xs:string"/>
4127                        <xs:element name="city" type="xs:string"/>
4128                        <xs:element name="zip" type="xs:string"/>
4129                    </xs:sequence>
4130                </xs:complexType>
4131            </xs:schema>"#;
4132
4133        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4134        assert_eq!(schema.components.len(), 1);
4135        assert_eq!(
4136            schema.components[0].component_type,
4137            XsdComponentType::ComplexType
4138        );
4139        assert_eq!(schema.components[0].name, Some("AddressType".to_string()));
4140    }
4141
4142    #[test]
4143    fn test_parse_restriction() {
4144        let schema_xml = r#"<?xml version="1.0"?>
4145            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4146                <xs:simpleType name="AgeType">
4147                    <xs:restriction base="xs:integer">
4148                        <xs:minInclusive value="0"/>
4149                        <xs:maxInclusive value="150"/>
4150                    </xs:restriction>
4151                </xs:simpleType>
4152            </xs:schema>"#;
4153
4154        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4155        assert_eq!(schema.components.len(), 1);
4156        assert_eq!(
4157            schema.components[0].component_type,
4158            XsdComponentType::SimpleType
4159        );
4160
4161        // Should have facets from the restriction
4162        let st = &schema.components[0];
4163        assert_eq!(st.datatype, Some(XsdDatatypeKind::Integer));
4164        assert!(!st.facets.is_empty());
4165    }
4166
4167    #[test]
4168    fn test_parse_enumeration() {
4169        let schema_xml = r#"<?xml version="1.0"?>
4170            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4171                <xs:simpleType name="ColorType">
4172                    <xs:restriction base="xs:string">
4173                        <xs:enumeration value="red"/>
4174                        <xs:enumeration value="green"/>
4175                        <xs:enumeration value="blue"/>
4176                    </xs:restriction>
4177                </xs:simpleType>
4178            </xs:schema>"#;
4179
4180        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4181        assert_eq!(schema.components.len(), 1);
4182        assert_eq!(schema.components[0].name, Some("ColorType".to_string()));
4183    }
4184
4185    #[test]
4186    fn test_parse_min_max_occurs() {
4187        let schema_xml = r#"<?xml version="1.0"?>
4188            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4189                <xs:element name="items">
4190                    <xs:complexType>
4191                        <xs:sequence>
4192                            <xs:element name="item" type="xs:string"
4193                                        minOccurs="0" maxOccurs="unbounded"/>
4194                        </xs:sequence>
4195                    </xs:complexType>
4196                </xs:element>
4197            </xs:schema>"#;
4198
4199        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4200        assert_eq!(schema.components.len(), 1);
4201
4202        // Check the item element inside the sequence
4203        let elem = &schema.components[0];
4204        let ct = elem
4205            .children
4206            .iter()
4207            .find(|c| c.component_type == XsdComponentType::ComplexType);
4208        assert!(ct.is_some());
4209        if let Some(ctc) = ct {
4210            let seq = ctc
4211                .children
4212                .iter()
4213                .find(|c| c.component_type == XsdComponentType::Sequence);
4214            assert!(seq.is_some());
4215            if let Some(seqc) = seq {
4216                assert!(!seqc.children.is_empty());
4217                let item = &seqc.children[0];
4218                assert_eq!(item.min_occurs, 0);
4219                assert_eq!(item.max_occurs, -1);
4220            }
4221        }
4222    }
4223
4224    #[test]
4225    fn test_parse_attribute_default() {
4226        let schema_xml = r#"<?xml version="1.0"?>
4227            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4228                <xs:element name="book">
4229                    <xs:complexType>
4230                        <xs:sequence>
4231                            <xs:element name="title" type="xs:string"/>
4232                        </xs:sequence>
4233                        <xs:attribute name="lang" type="xs:string" default="en"/>
4234                        <xs:attribute name="id" type="xs:integer" use="required"/>
4235                    </xs:complexType>
4236                </xs:element>
4237            </xs:schema>"#;
4238
4239        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4240        let elem = &schema.components[0];
4241        let ct = elem
4242            .children
4243            .iter()
4244            .find(|c| c.component_type == XsdComponentType::ComplexType);
4245        assert!(ct.is_some());
4246        if let Some(ctc) = ct {
4247            let lang_attr = ctc
4248                .attributes
4249                .iter()
4250                .find(|a| a.name.as_deref() == Some("lang"));
4251            assert!(lang_attr.is_some());
4252            if let Some(la) = lang_attr {
4253                assert_eq!(la.min_occurs, 0); // optional
4254            }
4255
4256            let id_attr = ctc
4257                .attributes
4258                .iter()
4259                .find(|a| a.name.as_deref() == Some("id"));
4260            assert!(id_attr.is_some());
4261        }
4262    }
4263
4264    #[test]
4265    fn test_parse_element_with_ref() {
4266        let schema_xml = r#"<?xml version="1.0"?>
4267            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4268                <xs:element name="root">
4269                    <xs:complexType>
4270                        <xs:sequence>
4271                            <xs:element ref="child" minOccurs="0"/>
4272                        </xs:sequence>
4273                    </xs:complexType>
4274                </xs:element>
4275                <xs:element name="child" type="xs:string"/>
4276            </xs:schema>"#;
4277
4278        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4279        assert_eq!(schema.components.len(), 2);
4280        // The ref element inside the sequence
4281        let root = &schema.components[0];
4282        let ct = root
4283            .children
4284            .iter()
4285            .find(|c| c.component_type == XsdComponentType::ComplexType);
4286        assert!(ct.is_some());
4287        if let Some(ctc) = ct {
4288            let seq = ctc
4289                .children
4290                .iter()
4291                .find(|c| c.component_type == XsdComponentType::Sequence);
4292            assert!(seq.is_some());
4293            if let Some(seqc) = seq {
4294                assert!(!seqc.children.is_empty());
4295                let ref_elem = &seqc.children[0];
4296                assert_eq!(ref_elem.ref_name, Some("child".to_string()));
4297            }
4298        }
4299    }
4300
4301    // ── Document Validation Tests ─────────────────────────────────────────
4302
4303    #[test]
4304    fn test_validate_simple_element() {
4305        let schema_xml = r#"<?xml version="1.0"?>
4306            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4307                <xs:element name="name" type="xs:string"/>
4308            </xs:schema>"#;
4309
4310        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4311
4312        let doc = r#"<?xml version="1.0"?>
4313            <name>John Doe</name>"#;
4314
4315        assert!(xsd_validate(&schema, doc).is_ok());
4316    }
4317
4318    #[test]
4319    fn test_validate_integer_element() {
4320        let schema_xml = r#"<?xml version="1.0"?>
4321            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4322                <xs:element name="age" type="xs:integer"/>
4323            </xs:schema>"#;
4324
4325        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4326
4327        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><age>25</age>"#).is_ok());
4328        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><age>not-a-number</age>"#).is_err());
4329    }
4330
4331    #[test]
4332    fn test_validate_complex_element() {
4333        let schema_xml = r#"<?xml version="1.0"?>
4334            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4335                <xs:element name="product">
4336                    <xs:complexType>
4337                        <xs:sequence>
4338                            <xs:element name="name" type="xs:string"/>
4339                            <xs:element name="price" type="xs:decimal"/>
4340                        </xs:sequence>
4341                        <xs:attribute name="id" type="xs:integer" use="required"/>
4342                    </xs:complexType>
4343                </xs:element>
4344            </xs:schema>"#;
4345
4346        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4347
4348        let valid_doc = r#"<?xml version="1.0"?>
4349            <product id="123">
4350                <name>Widget</name>
4351                <price>9.99</price>
4352            </product>"#;
4353
4354        assert!(xsd_validate(&schema, valid_doc).is_ok());
4355    }
4356
4357    #[test]
4358    fn test_validate_missing_required_attribute() {
4359        let schema_xml = r#"<?xml version="1.0"?>
4360            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4361                <xs:element name="product">
4362                    <xs:complexType>
4363                        <xs:sequence>
4364                            <xs:element name="name" type="xs:string"/>
4365                        </xs:sequence>
4366                        <xs:attribute name="id" type="xs:integer" use="required"/>
4367                    </xs:complexType>
4368                </xs:element>
4369            </xs:schema>"#;
4370
4371        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4372
4373        let invalid_doc = r#"<?xml version="1.0"?>
4374            <product>
4375                <name>Widget</name>
4376            </product>"#;
4377
4378        assert!(xsd_validate(&schema, invalid_doc).is_err());
4379    }
4380
4381    #[test]
4382    fn test_validate_enumeration_facet() {
4383        let schema_xml = r#"<?xml version="1.0"?>
4384            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4385                <xs:element name="color">
4386                    <xs:simpleType>
4387                        <xs:restriction base="xs:string">
4388                            <xs:enumeration value="red"/>
4389                            <xs:enumeration value="green"/>
4390                            <xs:enumeration value="blue"/>
4391                        </xs:restriction>
4392                    </xs:simpleType>
4393                </xs:element>
4394            </xs:schema>"#;
4395
4396        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4397
4398        // Note: enumeration validation is currently simplified - the facet
4399        // matches each individual value
4400        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><color>red</color>"#).is_ok());
4401    }
4402
4403    #[test]
4404    fn test_validate_boolean_element() {
4405        let schema_xml = r#"<?xml version="1.0"?>
4406            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4407                <xs:element name="active" type="xs:boolean"/>
4408            </xs:schema>"#;
4409
4410        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4411
4412        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><active>true</active>"#).is_ok());
4413        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><active>false</active>"#).is_ok());
4414        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><active>1</active>"#).is_ok());
4415        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><active>yes</active>"#).is_err());
4416    }
4417
4418    #[test]
4419    fn test_validate_element_with_range_constraint() {
4420        let schema_xml = r#"<?xml version="1.0"?>
4421            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4422                <xs:element name="age">
4423                    <xs:simpleType>
4424                        <xs:restriction base="xs:integer">
4425                            <xs:minInclusive value="0"/>
4426                            <xs:maxInclusive value="150"/>
4427                        </xs:restriction>
4428                    </xs:simpleType>
4429                </xs:element>
4430            </xs:schema>"#;
4431
4432        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4433
4434        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><age>25</age>"#).is_ok());
4435        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><age>0</age>"#).is_ok());
4436        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><age>150</age>"#).is_ok());
4437        // Note: minInclusive/maxInclusive validation currently works for facets
4438    }
4439
4440    #[test]
4441    fn test_validate_optional_element() {
4442        let schema_xml = r#"<?xml version="1.0"?>
4443            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4444                <xs:element name="person">
4445                    <xs:complexType>
4446                        <xs:sequence>
4447                            <xs:element name="name" type="xs:string"/>
4448                            <xs:element name="nickname" type="xs:string" minOccurs="0"/>
4449                        </xs:sequence>
4450                    </xs:complexType>
4451                </xs:element>
4452            </xs:schema>"#;
4453
4454        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4455
4456        let doc_with_nick = r#"<?xml version="1.0"?>
4457            <person>
4458                <name>John</name>
4459                <nickname>Johnny</nickname>
4460            </person>"#;
4461
4462        let doc_without_nick = r#"<?xml version="1.0"?>
4463            <person>
4464                <name>John</name>
4465            </person>"#;
4466
4467        assert!(xsd_validate(&schema, doc_with_nick).is_ok());
4468        assert!(xsd_validate(&schema, doc_without_nick).is_ok());
4469    }
4470
4471    #[test]
4472    fn test_validate_unbounded_element() {
4473        let schema_xml = r#"<?xml version="1.0"?>
4474            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4475                <xs:element name="items">
4476                    <xs:complexType>
4477                        <xs:sequence>
4478                            <xs:element name="item" type="xs:string"
4479                                        minOccurs="0" maxOccurs="unbounded"/>
4480                        </xs:sequence>
4481                    </xs:complexType>
4482                </xs:element>
4483            </xs:schema>"#;
4484
4485        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4486
4487        let doc = r#"<?xml version="1.0"?>
4488            <items>
4489                <item>one</item>
4490                <item>two</item>
4491                <item>three</item>
4492            </items>"#;
4493
4494        assert!(xsd_validate(&schema, doc).is_ok());
4495    }
4496
4497    #[test]
4498    fn test_validate_date_element() {
4499        let schema_xml = r#"<?xml version="1.0"?>
4500            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4501                <xs:element name="birthDate" type="xs:date"/>
4502            </xs:schema>"#;
4503
4504        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4505
4506        assert!(xsd_validate(
4507            &schema,
4508            r#"<?xml version="1.0"?><birthDate>1990-01-15</birthDate>"#
4509        )
4510        .is_ok());
4511        assert!(xsd_validate(
4512            &schema,
4513            r#"<?xml version="1.0"?><birthDate>not-a-date</birthDate>"#
4514        )
4515        .is_err());
4516    }
4517
4518    #[test]
4519    fn test_validate_choice() {
4520        let schema_xml = r#"<?xml version="1.0"?>
4521            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4522                <xs:element name="contact">
4523                    <xs:complexType>
4524                        <xs:choice>
4525                            <xs:element name="email" type="xs:string"/>
4526                            <xs:element name="phone" type="xs:string"/>
4527                        </xs:choice>
4528                    </xs:complexType>
4529                </xs:element>
4530            </xs:schema>"#;
4531
4532        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4533
4534        assert!(xsd_validate(
4535            &schema,
4536            r#"<?xml version="1.0"?><contact><email>a@b.com</email></contact>"#
4537        )
4538        .is_ok());
4539        assert!(xsd_validate(
4540            &schema,
4541            r#"<?xml version="1.0"?><contact><phone>555-1234</phone></contact>"#
4542        )
4543        .is_ok());
4544    }
4545
4546    #[test]
4547    fn test_validate_positive_integer_constraint() {
4548        let schema_xml = r#"<?xml version="1.0"?>
4549            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4550                <xs:element name="quantity" type="xs:positiveInteger"/>
4551            </xs:schema>"#;
4552
4553        let schema = xsd_parse(schema_xml).expect("Failed to parse schema");
4554
4555        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><quantity>1</quantity>"#).is_ok());
4556        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><quantity>0</quantity>"#).is_err());
4557        assert!(xsd_validate(&schema, r#"<?xml version="1.0"?><quantity>-1</quantity>"#).is_err());
4558    }
4559
4560    // ── C ABI Tests ───────────────────────────────────────────────────────
4561
4562    /// Create a memory parser context and parse a schema from it.
4563    ///
4564    /// # Safety
4565    ///
4566    /// - `schema_xml` is a static string valid for the call; `ctxt` is
4567    ///   non-NULL (asserted) and valid until `xmlSchemaParse` consumes it;
4568    ///   `schema` is non-NULL (asserted) and freed with `xmlSchemaFree`
4569    ///   exactly once.
4570    #[test]
4571    fn test_xml_schema_new_mem_parser_ctxt() {
4572        let schema_xml = r#"<?xml version="1.0"?>
4573            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4574                <xs:element name="name" type="xs:string"/>
4575            </xs:schema>"#;
4576
4577        let ctxt = unsafe {
4578            xmlSchemaNewMemParserCtxt(
4579                schema_xml.as_ptr() as *const c_char,
4580                schema_xml.len() as c_int,
4581            )
4582        };
4583        assert!(!ctxt.is_null());
4584
4585        let schema = unsafe { xmlSchemaParse(ctxt) };
4586        assert!(!schema.is_null());
4587
4588        unsafe {
4589            xmlSchemaFree(schema);
4590        }
4591    }
4592
4593    /// Validate a well-formed document against a parsed schema.
4594    ///
4595    /// # Safety
4596    ///
4597    /// - The schema/doc strings are static and valid for the calls; the
4598    ///   parser context, schema, valid context and document are non-NULL
4599    ///   (asserted) and each freed exactly once with its matching free
4600    ///   function; the document stays alive until `xmlSchemaValidateDoc`
4601    ///   and the final `xmlFreeDoc`.
4602    #[test]
4603    fn test_xml_schema_validate_doc() {
4604        let schema_xml = r#"<?xml version="1.0"?>
4605            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4606                <xs:element name="name" type="xs:string"/>
4607            </xs:schema>"#;
4608
4609        let doc_xml = r#"<?xml version="1.0"?>
4610            <name>John Doe</name>"#;
4611
4612        let ctxt = unsafe {
4613            xmlSchemaNewMemParserCtxt(
4614                schema_xml.as_ptr() as *const c_char,
4615                schema_xml.len() as c_int,
4616            )
4617        };
4618        let schema = unsafe { xmlSchemaParse(ctxt) };
4619        let valid_ctxt = unsafe { xmlSchemaNewValidCtxt(schema) };
4620
4621        let doc = unsafe {
4622            crate::abi::exports_xml2::xmlReadMemory(
4623                doc_xml.as_ptr() as *const c_char,
4624                doc_xml.len() as c_int,
4625                c"test.xml".as_ptr() as *const c_char,
4626                ptr::null(),
4627                0,
4628            )
4629        };
4630
4631        let result = unsafe { xmlSchemaValidateDoc(valid_ctxt, doc) };
4632        assert_eq!(result, 0);
4633
4634        unsafe {
4635            xmlSchemaFreeValidCtxt(valid_ctxt);
4636            xmlSchemaFree(schema);
4637            crate::abi::exports_xml2::xmlFreeDoc(doc);
4638        }
4639    }
4640
4641    /// Validate a document that violates the schema's type constraints.
4642    ///
4643    /// # Safety
4644    ///
4645    /// - The schema/doc strings are static and valid for the calls; the
4646    ///   contexts, schema and document are non-NULL (asserted) and each
4647    ///   freed exactly once with its matching free function; the document
4648    ///   stays alive until `xmlSchemaValidateDoc` and the final
4649    ///   `xmlFreeDoc`.
4650    #[test]
4651    fn test_xml_schema_validate_invalid_doc() {
4652        let schema_xml = r#"<?xml version="1.0"?>
4653            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4654                <xs:element name="age" type="xs:integer"/>
4655            </xs:schema>"#;
4656
4657        let doc_xml = r#"<?xml version="1.0"?>
4658            <age>not-a-number</age>"#;
4659
4660        let ctxt = unsafe {
4661            xmlSchemaNewMemParserCtxt(
4662                schema_xml.as_ptr() as *const c_char,
4663                schema_xml.len() as c_int,
4664            )
4665        };
4666        let schema = unsafe { xmlSchemaParse(ctxt) };
4667        let valid_ctxt = unsafe { xmlSchemaNewValidCtxt(schema) };
4668
4669        let doc = unsafe {
4670            crate::abi::exports_xml2::xmlReadMemory(
4671                doc_xml.as_ptr() as *const c_char,
4672                doc_xml.len() as c_int,
4673                c"test.xml".as_ptr() as *const c_char,
4674                ptr::null(),
4675                0,
4676            )
4677        };
4678
4679        let result = unsafe { xmlSchemaValidateDoc(valid_ctxt, doc) };
4680        assert_ne!(result, 0); // Should have errors
4681
4682        unsafe {
4683            xmlSchemaFreeValidCtxt(valid_ctxt);
4684            xmlSchemaFree(schema);
4685            crate::abi::exports_xml2::xmlFreeDoc(doc);
4686        }
4687    }
4688
4689    /// A document whose root element matches NO global element declaration
4690    /// fails validation with the exact upstream diagnostic
4691    /// (DOMDocument_schemaValidate_error2 parity; upstream xmlschemas.c
4692    /// xmlSchemaValidateDoc reports "No matching global declaration
4693    /// available for the validation root.").
4694    ///
4695    /// # Safety
4696    ///
4697    /// - The doc XML string is static and valid for the calls; `doc` is
4698    ///   non-NULL (asserted) and freed exactly once with `xmlFreeDoc`; the
4699    ///   context is stack-local.
4700    #[test]
4701    fn test_xml_schema_validate_root_without_global_decl() {
4702        let doc = unsafe {
4703            crate::abi::exports_xml2::xmlReadMemory(
4704                c"<root/>".as_ptr() as *const c_char,
4705                7,
4706                c"test.xml".as_ptr() as *const c_char,
4707                ptr::null(),
4708                0,
4709            )
4710        };
4711        assert!(!doc.is_null());
4712
4713        // Schema declares a global element that is NOT the document root.
4714        let mut schema = XsdSchema::new();
4715        schema.components.push(XsdComponent {
4716            component_type: XsdComponentType::Element,
4717            name: Some("other".to_string()),
4718            ..XsdComponent::new(XsdComponentType::Element)
4719        });
4720
4721        let mut ctxt = XsdValidCtxt::new();
4722        let valid = unsafe { xsd_validate_doc(&schema, doc, &mut ctxt) };
4723        assert!(!valid);
4724        assert_eq!(ctxt.errors.len(), 1);
4725        assert!(
4726            ctxt.errors[0]
4727                .contains("No matching global declaration available for the validation root."),
4728            "unexpected diagnostic: {:?}",
4729            ctxt.errors[0]
4730        );
4731        assert!(
4732            ctxt.errors[0].contains("'root'"),
4733            "root name missing: {:?}",
4734            ctxt.errors[0]
4735        );
4736
4737        unsafe {
4738            crate::abi::exports_xml2::xmlFreeDoc(doc);
4739        }
4740    }
4741
4742    /// Validating with XML_SCHEMA_VAL_VC_I_CREATE (php LIBXML_SCHEMA_CREATE)
4743    /// injects a missing attribute's schema default into the instance
4744    /// (DOMDocument_schemaValidateSource_addAttrs parity: the book without an
4745    /// is-hardback attribute gets default "false"). Without the option the
4746    /// instance stays untouched.
4747    ///
4748    /// # Safety
4749    ///
4750    /// - The schema/doc XML strings are static and valid for the calls; the
4751    ///   contexts, schema and document are non-NULL (asserted) and each freed
4752    ///   exactly once with its matching free function; the documents stay
4753    ///   alive until the final `xmlFreeDoc`.
4754    #[test]
4755    fn test_xml_schema_validate_creates_default_attrs() {
4756        let schema_xml = r#"<?xml version="1.0"?>
4757            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
4758              <xs:element name="books">
4759                <xs:complexType>
4760                  <xs:sequence>
4761                    <xs:element name="book" minOccurs="1" maxOccurs="unbounded">
4762                      <xs:complexType>
4763                        <xs:attribute name="is-hardback" type="xs:boolean" default="false"/>
4764                      </xs:complexType>
4765                    </xs:element>
4766                  </xs:sequence>
4767                </xs:complexType>
4768              </xs:element>
4769            </xs:schema>"#;
4770        let doc_xml = r#"<?xml version="1.0"?>
4771            <books><book><title>a</title></book></books>"#;
4772
4773        // Validating with XML_SCHEMA_VAL_VC_I_CREATE (php LIBXML_SCHEMA_CREATE)
4774        // injects a missing attribute's schema default into the instance
4775        // (DOMDocument_schemaValidateSource_addAttrs parity). Without the
4776        // option the instance stays untouched.
4777
4778        let ctxt = unsafe {
4779            xmlSchemaNewMemParserCtxt(
4780                schema_xml.as_ptr() as *const c_char,
4781                schema_xml.len() as c_int,
4782            )
4783        };
4784        let schema = unsafe { xmlSchemaParse(ctxt) };
4785        assert!(!schema.is_null());
4786        let valid_ctxt = unsafe { xmlSchemaNewValidCtxt(schema) };
4787        assert!(!valid_ctxt.is_null());
4788
4789        let make_doc = || unsafe {
4790            crate::abi::exports_xml2::xmlReadMemory(
4791                doc_xml.as_ptr() as *const c_char,
4792                doc_xml.len() as c_int,
4793                c"test.xml".as_ptr() as *const c_char,
4794                ptr::null(),
4795                0,
4796            )
4797        };
4798        let book_of = |doc: *mut _xmlDoc| -> *mut _xmlNode {
4799            unsafe {
4800                // doc -> books element -> its first child (the single book)
4801                let books = (*doc).children;
4802                assert!(!books.is_null());
4803                let book = (*books).children;
4804                assert!(!book.is_null());
4805                book
4806            }
4807        };
4808
4809        // Without the create option the attribute is NOT injected.
4810        let doc = make_doc();
4811        assert_eq!(unsafe { xmlSchemaValidateDoc(valid_ctxt, doc) }, 0);
4812        let missing = unsafe {
4813            crate::abi::exports_xml2::xmlGetProp(
4814                book_of(doc),
4815                c"is-hardback".as_ptr() as *const crate::abi::types::xmlChar,
4816            )
4817        };
4818        assert!(
4819            missing.is_null(),
4820            "attr must NOT be created without the option"
4821        );
4822        unsafe {
4823            crate::abi::exports_xml2::xmlFreeDoc(doc);
4824        }
4825
4826        // With the option the default is injected and the doc validates.
4827        let r = unsafe {
4828            crate::abi::exports_schema::xmlSchemaSetValidOptions(
4829                valid_ctxt as *mut crate::abi::exports_schema::xmlSchemaValidCtxt,
4830                1,
4831            )
4832        };
4833        assert_eq!(r, 0);
4834        let doc = make_doc();
4835        assert_eq!(unsafe { xmlSchemaValidateDoc(valid_ctxt, doc) }, 0);
4836        let got = unsafe {
4837            crate::abi::exports_xml2::xmlGetProp(
4838                book_of(doc),
4839                c"is-hardback".as_ptr() as *const crate::abi::types::xmlChar,
4840            )
4841        };
4842        assert!(!got.is_null(), "default attribute must be created");
4843        let val = unsafe { crate::xml::string::xmlstr_to_string(got) };
4844        assert_eq!(val, "false");
4845
4846        unsafe {
4847            crate::abi::exports_xml2::xmlFreeDoc(doc);
4848            xmlSchemaFreeValidCtxt(valid_ctxt);
4849            xmlSchemaFree(schema);
4850            xmlSchemaFreeParserCtxt(ctxt);
4851        }
4852    }
4853
4854    /// A NULL schema argument still yields a usable valid context.
4855    ///
4856    /// # Safety
4857    ///
4858    /// - `xmlSchemaNewValidCtxt` accepts a NULL schema and returns a
4859    ///   non-NULL context (asserted) that must be freed with
4860    ///   `xmlSchemaFreeValidCtxt` exactly once.
4861    #[test]
4862    fn test_xml_schema_new_valid_ctxt_null() {
4863        let ctxt = unsafe { xmlSchemaNewValidCtxt(ptr::null_mut()) };
4864        assert!(!ctxt.is_null());
4865        unsafe { xmlSchemaFreeValidCtxt(ctxt) };
4866    }
4867
4868    /// Freeing NULL schema/context pointers must not crash.
4869    ///
4870    /// # Safety
4871    ///
4872    /// - `xmlSchemaFree`, `xmlSchemaFreeParserCtxt` and
4873    ///   `xmlSchemaFreeValidCtxt` handle NULL as documented no-ops; no
4874    ///   pointer is dereferenced.
4875    #[test]
4876    fn test_xml_schema_free_null() {
4877        unsafe {
4878            xmlSchemaFree(ptr::null_mut());
4879            xmlSchemaFreeParserCtxt(ptr::null_mut());
4880            xmlSchemaFreeValidCtxt(ptr::null_mut());
4881        }
4882    }
4883
4884    /// A NULL filename still yields a parser context.
4885    ///
4886    /// # Safety
4887    ///
4888    /// - `xmlSchemaNewParserCtxt` accepts a NULL filename and returns a
4889    ///   non-NULL context (asserted) that is allocator-owned and freed with
4890    ///   `xmlFreeImpl` exactly once.
4891    #[test]
4892    fn test_xml_schema_new_parser_ctxt_null() {
4893        let ctxt = unsafe { xmlSchemaNewParserCtxt(ptr::null()) };
4894        assert!(!ctxt.is_null());
4895        // Clean up
4896        unsafe {
4897            crate::abi::allocator::xmlFreeImpl(ctxt);
4898        }
4899    }
4900
4901    #[test]
4902    fn test_datatype_parse_kind() {
4903        assert_eq!(
4904            parse_datatype_kind("xs:string"),
4905            Some(XsdDatatypeKind::String)
4906        );
4907        assert_eq!(parse_datatype_kind("string"), Some(XsdDatatypeKind::String));
4908        assert_eq!(
4909            parse_datatype_kind("xs:integer"),
4910            Some(XsdDatatypeKind::Integer)
4911        );
4912        assert_eq!(
4913            parse_datatype_kind("xs:boolean"),
4914            Some(XsdDatatypeKind::Boolean)
4915        );
4916        assert_eq!(
4917            parse_datatype_kind("xs:decimal"),
4918            Some(XsdDatatypeKind::Decimal)
4919        );
4920        assert_eq!(
4921            parse_datatype_kind("xs:float"),
4922            Some(XsdDatatypeKind::Float)
4923        );
4924        assert_eq!(
4925            parse_datatype_kind("xs:double"),
4926            Some(XsdDatatypeKind::Double)
4927        );
4928        assert_eq!(parse_datatype_kind("xs:date"), Some(XsdDatatypeKind::Date));
4929        assert_eq!(
4930            parse_datatype_kind("xs:dateTime"),
4931            Some(XsdDatatypeKind::DateTime)
4932        );
4933        assert_eq!(parse_datatype_kind("xs:time"), Some(XsdDatatypeKind::Time));
4934        assert_eq!(
4935            parse_datatype_kind("xs:hexBinary"),
4936            Some(XsdDatatypeKind::HexBinary)
4937        );
4938        assert_eq!(
4939            parse_datatype_kind("xs:base64Binary"),
4940            Some(XsdDatatypeKind::Base64Binary)
4941        );
4942        assert_eq!(
4943            parse_datatype_kind("xs:anyURI"),
4944            Some(XsdDatatypeKind::AnyURI)
4945        );
4946        assert_eq!(
4947            parse_datatype_kind("xs:QName"),
4948            Some(XsdDatatypeKind::QName)
4949        );
4950        assert_eq!(
4951            parse_datatype_kind("xs:normalizedString"),
4952            Some(XsdDatatypeKind::NormalizedString)
4953        );
4954        assert_eq!(
4955            parse_datatype_kind("xs:token"),
4956            Some(XsdDatatypeKind::Token)
4957        );
4958        assert_eq!(
4959            parse_datatype_kind("xs:language"),
4960            Some(XsdDatatypeKind::Language)
4961        );
4962        assert_eq!(parse_datatype_kind("xs:Name"), Some(XsdDatatypeKind::Name));
4963        assert_eq!(
4964            parse_datatype_kind("xs:NCName"),
4965            Some(XsdDatatypeKind::NCName)
4966        );
4967        assert_eq!(parse_datatype_kind("xs:ID"), Some(XsdDatatypeKind::Id));
4968        assert_eq!(
4969            parse_datatype_kind("xs:IDREF"),
4970            Some(XsdDatatypeKind::Idref)
4971        );
4972        assert_eq!(
4973            parse_datatype_kind("xs:integer"),
4974            Some(XsdDatatypeKind::Integer)
4975        );
4976        assert_eq!(parse_datatype_kind("xs:long"), Some(XsdDatatypeKind::Long));
4977        assert_eq!(parse_datatype_kind("xs:int"), Some(XsdDatatypeKind::Int));
4978        assert_eq!(
4979            parse_datatype_kind("xs:short"),
4980            Some(XsdDatatypeKind::Short)
4981        );
4982        assert_eq!(parse_datatype_kind("xs:byte"), Some(XsdDatatypeKind::Byte));
4983        assert_eq!(
4984            parse_datatype_kind("xs:positiveInteger"),
4985            Some(XsdDatatypeKind::PositiveInteger)
4986        );
4987        assert_eq!(
4988            parse_datatype_kind("xs:negativeInteger"),
4989            Some(XsdDatatypeKind::NegativeInteger)
4990        );
4991        assert_eq!(parse_datatype_kind("unknown"), None);
4992    }
4993}