Skip to main content

libxml_rs/abi/
exports_schema.rs

1//! exports_schema — C ABI exports for the XML Schema family.
2//!
3//! Implements the public ABI of the upstream headers `xmlschemas.h`,
4//! `xmlschemastypes.h` and `schematron.h` (family closure 11.1-I).
5//!
6//! The internal XSD engine lives in `src/xml/schemas` (`XsdSchema`,
7//! `xsd_parse`, `xsd_validate`, `xsd_validate_datatype`). It powers
8//! `xmllint --schema` and is oracle-verified, so every function here that
9//! needs real schema behavior is wrapped around that engine. Datatype-value
10//! machinery (`xmlSchemaVal`, `xmlSchemaFacet`, built-in `xmlSchemaType`
11//! descriptors) does not exist in the internal engine and is provided here
12//! as small `repr(C)` structs allocated through the crate allocator.
13//!
14//! # UPSTREAM-PARITY
15//!
16//! All 57 functions below follow the exact oracle signatures. Behavior notes:
17//!
18//! - Parser/validation context *state* (error callbacks, options, filename,
19//!   locator) is stored in side registries keyed by context address, because
20//!   the internal engine's `xmlSchemaNewParserCtxt`/`xmlSchemaNewValidCtxt`
21//!   (defined in `src/xml/schemas/mod.rs`) box the schema/validation structs
22//!   directly and cannot be extended without breaking that module's layout.
23//! - `xmlSchemaNewDocParserCtxt` boxes a parsed `XsdSchema`, matching the
24//!   internal engine's convention that `xmlSchemaParse` returns its context
25//!   as the schema pointer (`schema == pctxt`).
26//! - Functions requiring streaming/SAX interception the internal DOM engine
27//!   cannot provide (`xmlSchemaSAXPlug`, `xmlSchemaValidateStream` without a
28//!   readable input buffer) are simplified and documented inline.
29
30#![allow(missing_docs, non_snake_case, non_camel_case_types, non_upper_case_globals)]
31#![allow(clippy::missing_safety_doc)]
32#![allow(clippy::not_unsafe_ptr_arg_deref)]
33#![allow(missing_debug_implementations)]
34
35use core::ffi::c_void;
36use core::ptr;
37use once_cell::sync::Lazy;
38use parking_lot::Mutex;
39use std::collections::HashMap;
40use std::ffi::{CStr, CString};
41use std::os::raw::{c_char, c_int, c_ulong};
42
43use crate::abi::allocator::{xmlFreeImpl, xmlMemStrdupImpl};
44use crate::abi::callbacks::{xmlStructuredErrorFunc, xmlValidityErrorFunc, xmlValidityWarningFunc};
45use crate::abi::structs::{_xmlDoc, _xmlError, _xmlNode, _xmlParserInputBuffer, _xmlSAXHandler};
46use crate::abi::types::{xmlChar, xmlCharEncoding, xmlErrorLevel, XML_FROM_SCHEMASV};
47use crate::xml::schemas::{
48    xsd_parse, xsd_validate, xsd_validate_datatype, xsd_validate_facet, XsdDatatypeKind,
49    XsdSchema, XsdValidCtxt,
50};
51use crate::xml::schematron::schematron_parse;
52
53// ═══════════════════════════════════════════════════════════════════════════════
54// Opaque upstream types
55// ═══════════════════════════════════════════════════════════════════════════════
56
57// These mirror the forward-declared opaque structs in the upstream headers
58// (`typedef struct _xmlSchema xmlSchema;` etc.). They are only ever used
59// through pointers, so uninhabited enums are ABI-identical to the opaque
60// C structs.
61
62pub enum xmlSchema {}
63pub enum xmlSchemaParserCtxt {}
64pub enum xmlSchemaValidCtxt {}
65pub enum xmlSchemaVal {}
66pub enum xmlSchemaFacet {}
67pub enum xmlSchemaType {}
68pub enum xmlSchemaWildcard {}
69pub enum xmlSchemaSAXPlugStruct {}
70pub enum xmlSchematronParserCtxt {}
71pub enum xmlSchematronValidCtxt {}
72
73/// `int (*xmlSchemaValidityLocatorFunc)(void *ctx, const char **file, unsigned long *line);`
74/// (xmlschemas.h)
75pub type xmlSchemaValidityLocatorFunc =
76    unsafe extern "C" fn(ctx: *mut c_void, file: *mut *const c_char, line: *mut c_ulong) -> c_int;
77
78// ═══════════════════════════════════════════════════════════════════════════════
79// Internal representations (allocated with Box / the crate allocator)
80// ═══════════════════════════════════════════════════════════════════════════════
81
82/// An `xmlSchemaVal` — a typed simple value.
83///
84/// `value`/`ns` are NUL-terminated heap strings (xmlMalloc'd). List values
85/// (NMTOKENS, IDREFS, ENTITIES) are chained through `next`.
86#[repr(C)]
87struct XsdVal {
88    val_type: c_int,      // xmlSchemaValType
89    value: *mut xmlChar,  // canonical/lexical value
90    ns: *mut xmlChar,     // namespace URI for QName/NOTATION
91    next: *mut XsdVal,    // next item for list values
92}
93
94/// An `xmlSchemaFacet` — a facet declaration.
95#[repr(C)]
96struct XsdFacet {
97    facet_type: c_int, // xmlSchemaFacetType (1000..1011)
98    value: *mut xmlChar,
99    next: *mut XsdFacet,
100}
101
102/// A built-in `xmlSchemaType` descriptor (static, registry-owned).
103#[repr(C)]
104struct XsdType {
105    val_type: c_int,       // xmlSchemaValType
106    item_type: *mut XsdType, // item type for the built-in list types
107    name: *const c_char,   // static type name ("string", "int", ...)
108}
109
110/// State kept for a parser context (side registry).
111#[derive(Default, Clone, Copy)]
112struct ParserState {
113    err: Option<xmlValidityErrorFunc>,
114    warn: Option<xmlValidityWarningFunc>,
115    ctx: usize,
116    serror: Option<xmlStructuredErrorFunc>,
117    sctx: usize,
118}
119
120/// State kept for a validation context (side registry).
121#[derive(Default, Clone, Copy)]
122struct ValidState {
123    err: Option<xmlValidityErrorFunc>,
124    warn: Option<xmlValidityWarningFunc>,
125    ctx: usize,
126    serror: Option<xmlStructuredErrorFunc>,
127    sctx: usize,
128    options: c_int,
129    filename: usize, // raw const char* (upstream stores the pointer, not a copy)
130    locator: Option<xmlSchemaValidityLocatorFunc>,
131    locator_ctx: usize,
132}
133
134/// State kept for a Schematron validation context (side registry).
135#[derive(Default, Clone, Copy)]
136struct SchematronValidState {
137    serror: Option<xmlStructuredErrorFunc>,
138    sctx: usize,
139}
140
141/// The SAX plug allocated by `xmlSchemaSAXPlug`.
142#[repr(C)]
143struct XsdSaxPlug {
144    sax: *const _xmlSAXHandler,
145    user_data: *mut c_void,
146}
147
148static PARSER_STATES: Lazy<Mutex<HashMap<usize, ParserState>>> =
149    Lazy::new(|| Mutex::new(HashMap::new()));
150static VALID_STATES: Lazy<Mutex<HashMap<usize, ValidState>>> =
151    Lazy::new(|| Mutex::new(HashMap::new()));
152static SCHEMATRON_VALID_STATES: Lazy<Mutex<HashMap<usize, SchematronValidState>>> =
153    Lazy::new(|| Mutex::new(HashMap::new()));
154static TYPE_REGISTRY: Lazy<Mutex<HashMap<c_int, usize>>> =
155    Lazy::new(|| Mutex::new(HashMap::new()));
156
157// ═══════════════════════════════════════════════════════════════════════════════
158// Constants (upstream values)
159// ═══════════════════════════════════════════════════════════════════════════════
160
161// xmlSchemaValType (schemasInternals.h)
162const VAL_UNKNOWN: c_int = 0;
163const VAL_STRING: c_int = 1;
164const VAL_NORMSTRING: c_int = 2;
165const VAL_DECIMAL: c_int = 3;
166const VAL_TIME: c_int = 4;
167const VAL_GDAY: c_int = 5;
168const VAL_GMONTH: c_int = 6;
169const VAL_GMONTHDAY: c_int = 7;
170const VAL_GYEAR: c_int = 8;
171const VAL_GYEARMONTH: c_int = 9;
172const VAL_DATE: c_int = 10;
173const VAL_DATETIME: c_int = 11;
174const VAL_DURATION: c_int = 12;
175const VAL_FLOAT: c_int = 13;
176const VAL_DOUBLE: c_int = 14;
177const VAL_BOOLEAN: c_int = 15;
178const VAL_TOKEN: c_int = 16;
179const VAL_LANGUAGE: c_int = 17;
180const VAL_NMTOKEN: c_int = 18;
181const VAL_NMTOKENS: c_int = 19;
182const VAL_NAME: c_int = 20;
183const VAL_QNAME: c_int = 21;
184const VAL_NCNAME: c_int = 22;
185const VAL_ID: c_int = 23;
186const VAL_IDREF: c_int = 24;
187const VAL_IDREFS: c_int = 25;
188const VAL_ENTITY: c_int = 26;
189const VAL_ENTITIES: c_int = 27;
190const VAL_NOTATION: c_int = 28;
191const VAL_ANYURI: c_int = 29;
192const VAL_INTEGER: c_int = 30;
193const VAL_NPINTEGER: c_int = 31;
194const VAL_NINTEGER: c_int = 32;
195const VAL_NNINTEGER: c_int = 33;
196const VAL_PINTEGER: c_int = 34;
197const VAL_INT: c_int = 35;
198const VAL_UINT: c_int = 36;
199const VAL_LONG: c_int = 37;
200const VAL_ULONG: c_int = 38;
201const VAL_SHORT: c_int = 39;
202const VAL_USHORT: c_int = 40;
203const VAL_BYTE: c_int = 41;
204const VAL_UBYTE: c_int = 42;
205const VAL_HEXBINARY: c_int = 43;
206const VAL_BASE64BINARY: c_int = 44;
207const VAL_ANYTYPE: c_int = 45;
208const VAL_ANYSIMPLETYPE: c_int = 46;
209
210// xmlSchemaFacetType (schemasInternals.h)
211const FACET_MININCLUSIVE: c_int = 1000;
212const FACET_MINEXCLUSIVE: c_int = 1001;
213const FACET_MAXINCLUSIVE: c_int = 1002;
214const FACET_MAXEXCLUSIVE: c_int = 1003;
215const FACET_TOTALDIGITS: c_int = 1004;
216const FACET_FRACTIONDIGITS: c_int = 1005;
217const FACET_PATTERN: c_int = 1006;
218const FACET_ENUMERATION: c_int = 1007;
219const FACET_WHITESPACE: c_int = 1008;
220const FACET_LENGTH: c_int = 1009;
221const FACET_MAXLENGTH: c_int = 1010;
222const FACET_MINLENGTH: c_int = 1011;
223
224// xmlSchemaWhitespaceValueType (xmlschemastypes.h)
225const WS_PRESERVE: c_int = 1;
226const WS_REPLACE: c_int = 2;
227const WS_COLLAPSE: c_int = 3;
228
229// xmlSchemaValidOptions (xmlschemas.h)
230const VAL_VC_I_CREATE: c_int = 1 << 0;
231const VAL_XSI_ASSEMBLE: c_int = 1 << 1;
232const VAL_OPTIONS_MASK: c_int = VAL_VC_I_CREATE | VAL_XSI_ASSEMBLE;
233
234/// The W3C XML Schema namespace.
235const XSD_NS: &[u8] = b"http://www.w3.org/2001/XMLSchema";
236
237// ═══════════════════════════════════════════════════════════════════════════════
238// Small helpers
239// ═══════════════════════════════════════════════════════════════════════════════
240
241/// Read a NUL-terminated C string as UTF-8 lossy.
242///
243/// # SAFETY
244///
245/// - `s` must be a valid NUL-terminated C string or NULL.
246unsafe fn cstr_to_str(s: *const c_char) -> Option<String> {
247    if s.is_null() {
248        return None;
249    }
250    // SAFETY: Caller guarantees a valid NUL-terminated C string.
251    let c = unsafe { CStr::from_ptr(s) };
252    Some(String::from_utf8_lossy(c.to_bytes()).to_string())
253}
254
255/// Compare a C string against a byte slice (no trailing NUL in the slice).
256///
257/// # SAFETY
258///
259/// - `s` must be a valid NUL-terminated C string or NULL.
260unsafe fn cstr_eq(s: *const c_char, bytes: &[u8]) -> bool {
261    if s.is_null() {
262        return false;
263    }
264    // SAFETY: Caller guarantees a valid NUL-terminated C string.
265    unsafe {
266        let mut i = 0usize;
267        while i < bytes.len() {
268            if *s.add(i) as u8 != bytes[i] {
269                return false;
270            }
271            i += 1;
272        }
273        *s.add(i) == 0
274    }
275}
276
277/// Duplicate a C string into heap memory owned by the caller.
278///
279/// Returns NULL when `s` is NULL.
280unsafe fn dup_cstr(s: *const c_char) -> *mut c_char {
281    if s.is_null() {
282        return ptr::null_mut();
283    }
284    // SAFETY: xmlMemStrdup requires a valid C string; caller guarantees it.
285    unsafe { xmlMemStrdupImpl(s) as *mut c_char }
286}
287
288fn is_whitespace(c: char) -> bool {
289    c == ' ' || c == '\t' || c == '\n' || c == '\r'
290}
291
292/// Replace whitespace characters with spaces (upstream `xmlSchemaWhiteSpaceReplace`).
293fn whitespace_replace(s: &str) -> String {
294    s.chars()
295        .map(|c| if is_whitespace(c) { ' ' } else { c })
296        .collect()
297}
298
299/// Collapse whitespace: trim and replace internal runs with single spaces
300/// (upstream `xmlSchemaCollapseString`).
301fn whitespace_collapse(s: &str) -> String {
302    let mut out = String::with_capacity(s.len());
303    let mut in_run = false;
304    for c in s.chars() {
305        if is_whitespace(c) {
306            in_run = true;
307        } else {
308            if in_run && !out.is_empty() {
309                out.push(' ');
310            }
311            in_run = false;
312            out.push(c);
313        }
314    }
315    out
316}
317
318/// Apply one of the `xmlSchemaWhitespaceValueType` transformations.
319fn apply_ws(s: &str, ws: c_int) -> String {
320    match ws {
321        WS_REPLACE => whitespace_replace(s),
322        WS_COLLAPSE => whitespace_collapse(s),
323        _ => s.to_string(),
324    }
325}
326
327/// Map an `xmlSchemaValType` to the internal `XsdDatatypeKind`.
328fn val_type_to_kind(val_type: c_int) -> Option<XsdDatatypeKind> {
329    Some(match val_type {
330        VAL_STRING | VAL_ANYTYPE | VAL_ANYSIMPLETYPE => XsdDatatypeKind::String,
331        VAL_NORMSTRING => XsdDatatypeKind::NormalizedString,
332        VAL_DECIMAL => XsdDatatypeKind::Decimal,
333        VAL_TIME => XsdDatatypeKind::Time,
334        VAL_GDAY => XsdDatatypeKind::GDay,
335        VAL_GMONTH => XsdDatatypeKind::GMonth,
336        VAL_GMONTHDAY => XsdDatatypeKind::GMonthDay,
337        VAL_GYEAR => XsdDatatypeKind::GYear,
338        VAL_GYEARMONTH => XsdDatatypeKind::GYearMonth,
339        VAL_DATE => XsdDatatypeKind::Date,
340        VAL_DATETIME => XsdDatatypeKind::DateTime,
341        VAL_DURATION => XsdDatatypeKind::Duration,
342        VAL_FLOAT => XsdDatatypeKind::Float,
343        VAL_DOUBLE => XsdDatatypeKind::Double,
344        VAL_BOOLEAN => XsdDatatypeKind::Boolean,
345        VAL_TOKEN => XsdDatatypeKind::Token,
346        VAL_LANGUAGE => XsdDatatypeKind::Language,
347        VAL_NMTOKEN => XsdDatatypeKind::Nmtoken,
348        VAL_NMTOKENS => XsdDatatypeKind::Nmtokens,
349        VAL_NAME => XsdDatatypeKind::Name,
350        VAL_QNAME => XsdDatatypeKind::QName,
351        VAL_NCNAME => XsdDatatypeKind::NCName,
352        VAL_ID => XsdDatatypeKind::Id,
353        VAL_IDREF => XsdDatatypeKind::Idref,
354        VAL_IDREFS => XsdDatatypeKind::Idrefs,
355        VAL_ENTITY => XsdDatatypeKind::Entity,
356        VAL_ENTITIES => XsdDatatypeKind::Entities,
357        VAL_NOTATION => XsdDatatypeKind::Notation,
358        VAL_ANYURI => XsdDatatypeKind::AnyURI,
359        VAL_INTEGER => XsdDatatypeKind::Integer,
360        VAL_NPINTEGER => XsdDatatypeKind::NonPositiveInteger,
361        VAL_NINTEGER => XsdDatatypeKind::NegativeInteger,
362        VAL_NNINTEGER => XsdDatatypeKind::NonNegativeInteger,
363        VAL_PINTEGER => XsdDatatypeKind::PositiveInteger,
364        VAL_INT => XsdDatatypeKind::Int,
365        VAL_UINT => XsdDatatypeKind::UnsignedInt,
366        VAL_LONG => XsdDatatypeKind::Long,
367        VAL_ULONG => XsdDatatypeKind::UnsignedLong,
368        VAL_SHORT => XsdDatatypeKind::Short,
369        VAL_USHORT => XsdDatatypeKind::UnsignedShort,
370        VAL_BYTE => XsdDatatypeKind::Byte,
371        VAL_UBYTE => XsdDatatypeKind::UnsignedByte,
372        VAL_HEXBINARY => XsdDatatypeKind::HexBinary,
373        VAL_BASE64BINARY => XsdDatatypeKind::Base64Binary,
374        _ => return None,
375    })
376}
377
378/// Map an `xmlSchemaFacetType` to the internal facet kind.
379fn facet_type_to_kind(facet_type: c_int) -> Option<XsdDatatypeKind> {
380    Some(match facet_type {
381        FACET_MININCLUSIVE => XsdDatatypeKind::FacetMinInclusive,
382        FACET_MINEXCLUSIVE => XsdDatatypeKind::FacetMinExclusive,
383        FACET_MAXINCLUSIVE => XsdDatatypeKind::FacetMaxInclusive,
384        FACET_MAXEXCLUSIVE => XsdDatatypeKind::FacetMaxExclusive,
385        FACET_TOTALDIGITS => XsdDatatypeKind::FacetTotalDigits,
386        FACET_FRACTIONDIGITS => XsdDatatypeKind::FacetFractionDigits,
387        FACET_PATTERN => XsdDatatypeKind::FacetPattern,
388        FACET_ENUMERATION => XsdDatatypeKind::FacetEnumeration,
389        FACET_WHITESPACE => XsdDatatypeKind::FacetWhiteSpace,
390        FACET_LENGTH => XsdDatatypeKind::FacetLength,
391        FACET_MAXLENGTH => XsdDatatypeKind::FacetMaxLength,
392        FACET_MINLENGTH => XsdDatatypeKind::FacetMinLength,
393        _ => return None,
394    })
395}
396
397/// NUL-terminated static name for a built-in `xmlSchemaValType`.
398fn type_name(val_type: c_int) -> Option<&'static [u8]> {
399    Some(match val_type {
400        VAL_STRING => b"string\0",
401        VAL_NORMSTRING => b"normalizedString\0",
402        VAL_DECIMAL => b"decimal\0",
403        VAL_TIME => b"time\0",
404        VAL_GDAY => b"gDay\0",
405        VAL_GMONTH => b"gMonth\0",
406        VAL_GMONTHDAY => b"gMonthDay\0",
407        VAL_GYEAR => b"gYear\0",
408        VAL_GYEARMONTH => b"gYearMonth\0",
409        VAL_DATE => b"date\0",
410        VAL_DATETIME => b"dateTime\0",
411        VAL_DURATION => b"duration\0",
412        VAL_FLOAT => b"float\0",
413        VAL_DOUBLE => b"double\0",
414        VAL_BOOLEAN => b"boolean\0",
415        VAL_TOKEN => b"token\0",
416        VAL_LANGUAGE => b"language\0",
417        VAL_NMTOKEN => b"NMTOKEN\0",
418        VAL_NMTOKENS => b"NMTOKENS\0",
419        VAL_NAME => b"Name\0",
420        VAL_QNAME => b"QName\0",
421        VAL_NCNAME => b"NCName\0",
422        VAL_ID => b"ID\0",
423        VAL_IDREF => b"IDREF\0",
424        VAL_IDREFS => b"IDREFS\0",
425        VAL_ENTITY => b"ENTITY\0",
426        VAL_ENTITIES => b"ENTITIES\0",
427        VAL_NOTATION => b"NOTATION\0",
428        VAL_ANYURI => b"anyURI\0",
429        VAL_INTEGER => b"integer\0",
430        VAL_NPINTEGER => b"nonPositiveInteger\0",
431        VAL_NINTEGER => b"negativeInteger\0",
432        VAL_NNINTEGER => b"nonNegativeInteger\0",
433        VAL_PINTEGER => b"positiveInteger\0",
434        VAL_INT => b"int\0",
435        VAL_UINT => b"unsignedInt\0",
436        VAL_LONG => b"long\0",
437        VAL_ULONG => b"unsignedLong\0",
438        VAL_SHORT => b"short\0",
439        VAL_USHORT => b"unsignedShort\0",
440        VAL_BYTE => b"byte\0",
441        VAL_UBYTE => b"unsignedByte\0",
442        VAL_HEXBINARY => b"hexBinary\0",
443        VAL_BASE64BINARY => b"base64Binary\0",
444        VAL_ANYTYPE => b"anyType\0",
445        VAL_ANYSIMPLETYPE => b"anySimpleType\0",
446        _ => return None,
447    })
448}
449
450/// Whether an `xmlSchemaValType` is numeric (decimal / integer family / float / double).
451fn is_numeric_type(val_type: c_int) -> bool {
452    matches!(
453        val_type,
454        VAL_DECIMAL
455            | VAL_FLOAT
456            | VAL_DOUBLE
457            | VAL_INTEGER
458            | VAL_NPINTEGER
459            | VAL_NINTEGER
460            | VAL_NNINTEGER
461            | VAL_PINTEGER
462            | VAL_INT
463            | VAL_UINT
464            | VAL_LONG
465            | VAL_ULONG
466            | VAL_SHORT
467            | VAL_USHORT
468            | VAL_BYTE
469            | VAL_UBYTE
470    )
471}
472
473/// Whether an `xmlSchemaValType` is a built-in list type.
474fn is_list_type(val_type: c_int) -> bool {
475    matches!(val_type, VAL_NMTOKENS | VAL_IDREFS | VAL_ENTITIES)
476}
477
478/// The item type of a built-in list type (itself otherwise).
479fn list_item_type(val_type: c_int) -> c_int {
480    match val_type {
481        VAL_NMTOKENS => VAL_NMTOKEN,
482        VAL_IDREFS => VAL_IDREF,
483        VAL_ENTITIES => VAL_ENTITY,
484        other => other,
485    }
486}
487
488/// Canonicalize a decimal string (no leading/trailing zeros, "-" only for
489/// negative non-zero values).
490fn canonical_decimal(s: &str) -> String {
491    let mut s = s.trim();
492    let neg = s.starts_with('-');
493    if s.starts_with('+') || s.starts_with('-') {
494        s = &s[1..];
495    }
496    let (int_part, frac_part) = match s.find('.') {
497        Some(i) => (&s[..i], &s[i + 1..]),
498        None => (s, ""),
499    };
500    let int_trimmed = int_part.trim_start_matches('0');
501    let int_trimmed = if int_trimmed.is_empty() { "0" } else { int_trimmed };
502    let frac_trimmed = frac_part.trim_end_matches('0');
503    let mut out = String::new();
504    if neg && !(int_trimmed == "0" && frac_trimmed.is_empty()) {
505        out.push('-');
506    }
507    out.push_str(int_trimmed);
508    if !frac_trimmed.is_empty() {
509        out.push('.');
510        out.push_str(frac_trimmed);
511    }
512    out
513}
514
515// ═══════════════════════════════════════════════════════════════════════════════
516// Value allocation / ownership helpers
517// ═══════════════════════════════════════════════════════════════════════════════
518
519/// Create a value node. `value`/`ns` are duplicated into heap memory.
520unsafe fn new_val(val_type: c_int, value: *const c_char, ns: *const c_char) -> *mut XsdVal {
521    // SAFETY: dup_cstr requires valid C strings or NULL; caller guarantees it.
522    unsafe {
523        Box::into_raw(Box::new(XsdVal {
524            val_type,
525            value: dup_cstr(value) as *mut xmlChar,
526            ns: dup_cstr(ns) as *mut xmlChar,
527            next: ptr::null_mut(),
528        }))
529    }
530}
531
532/// Free a value chain (values + their strings).
533unsafe fn free_val_chain(mut cur: *mut XsdVal) {
534    while !cur.is_null() {
535        let next = unsafe { (*cur).next };
536        // SAFETY: value/ns were xmlMalloc'd by new_val; cur is a Box we own.
537        unsafe {
538            if !(*cur).value.is_null() {
539                xmlFreeImpl((*cur).value as *mut c_void);
540            }
541            if !(*cur).ns.is_null() {
542                xmlFreeImpl((*cur).ns as *mut c_void);
543            }
544            drop(Box::from_raw(cur));
545        }
546        cur = next;
547    }
548}
549
550/// Create a value from a Rust string, duplicating into heap memory.
551fn val_from_str(val_type: c_int, value: &str) -> *mut XsdVal {
552    if let Ok(c) = CString::new(value) {
553        // SAFETY: c is a valid C string for the duration of the call.
554        unsafe { new_val(val_type, c.as_ptr(), ptr::null()) }
555    } else {
556        ptr::null_mut()
557    }
558}
559
560/// Create a (possibly list) value from a C string. List types are split on
561/// whitespace into an item chain, matching upstream's list-value layout.
562unsafe fn new_string_value(val_type: c_int, value: *const c_char) -> *mut XsdVal {
563    if value.is_null() {
564        return ptr::null_mut();
565    }
566    let s = unsafe { cstr_to_str(value).unwrap_or_default() };
567    if is_list_type(val_type) {
568        let item_type = list_item_type(val_type);
569        let tokens: Vec<&str> = s.split_whitespace().collect();
570        if tokens.is_empty() {
571            return ptr::null_mut();
572        }
573        let mut head: *mut XsdVal = ptr::null_mut();
574        let mut tail: *mut XsdVal = ptr::null_mut();
575        for tok in tokens {
576            let item = val_from_str(item_type, tok);
577            if item.is_null() {
578                continue;
579            }
580            if head.is_null() {
581                head = item;
582            } else {
583                // SAFETY: tail is a valid node from this loop.
584                unsafe { (*tail).next = item };
585            }
586            tail = item;
587        }
588        head
589    } else {
590        val_from_str(val_type, &s)
591    }
592}
593
594/// Build the built-in type descriptor for `val_type` (registry-owned, leaked).
595fn builtin_type(val_type: c_int) -> *mut XsdType {
596    if val_type <= VAL_UNKNOWN || val_type > VAL_ANYSIMPLETYPE {
597        return ptr::null_mut();
598    }
599    {
600        let reg = TYPE_REGISTRY.lock();
601        if let Some(addr) = reg.get(&val_type) {
602            return *addr as *mut XsdType;
603        }
604    }
605    // Resolve the item type outside the lock to avoid re-entrancy.
606    let item = if is_list_type(val_type) {
607        builtin_type(list_item_type(val_type))
608    } else {
609        ptr::null_mut()
610    };
611    let name = type_name(val_type).map(|b| b.as_ptr() as *const c_char).unwrap_or(ptr::null());
612    let t = Box::into_raw(Box::new(XsdType {
613        val_type,
614        item_type: item,
615        name,
616    }));
617    let mut reg = TYPE_REGISTRY.lock();
618    if let Some(existing) = reg.get(&val_type) {
619        // Another thread won the race; drop our duplicate.
620        // SAFETY: t is a Box we own and nobody else can see yet.
621        unsafe { drop(Box::from_raw(t)) };
622        return *existing as *mut XsdType;
623    }
624    reg.insert(val_type, t as usize);
625    t
626}
627
628// ═══════════════════════════════════════════════════════════════════════════════
629// Error dispatch helpers
630// ═══════════════════════════════════════════════════════════════════════════════
631
632/// Dispatch accumulated validation errors to the callbacks registered on a
633/// validation context (both the plain and the structured handler).
634///
635/// # SAFETY
636///
637/// - `ctxt_addr` must be the address of an `XsdValidCtxt` that a caller
638///   registered state for; otherwise this is a no-op.
639unsafe fn dispatch_valid_errors(ctxt_addr: usize, errors: &[String]) {
640    let state = {
641        let guard = VALID_STATES.lock();
642        guard.get(&ctxt_addr).copied()
643    };
644    let Some(state) = state else { return };
645    if state.err.is_none() && state.serror.is_none() {
646        return;
647    }
648    for msg in errors {
649        let Ok(cmsg) = CString::new(msg.as_str()) else { continue };
650        if let Some(err) = state.err {
651            // SAFETY: The caller supplied this callback in xmlSchemaSetValidErrors.
652            unsafe { err(state.ctx as *mut c_void, cmsg.as_ptr()) };
653        }
654        if let Some(serror) = state.serror {
655            // SAFETY: The caller supplied this callback in
656            // xmlSchemaSetValidStructuredErrors.
657            let mut e: _xmlError = unsafe { std::mem::zeroed() };
658            e.domain = XML_FROM_SCHEMASV;
659            e.code = 0;
660            e.message = cmsg.as_ptr() as *mut c_char;
661            e.level = xmlErrorLevel::XML_ERR_ERROR as c_int;
662            e.file = state.filename as *mut c_char;
663            e.line = 0;
664            unsafe { serror(state.sctx as *mut c_void, &e) };
665        }
666    }
667}
668
669/// Dispatch a single error message through a parser context's callbacks.
670///
671/// # SAFETY
672///
673/// - `ctxt_addr` must be the address of a parser context that a caller
674///   registered state for; otherwise this is a no-op.
675unsafe fn dispatch_parser_error(ctxt_addr: usize, msg: &str) {
676    let state = {
677        let guard = PARSER_STATES.lock();
678        guard.get(&ctxt_addr).copied()
679    };
680    let Some(state) = state else { return };
681    if state.err.is_none() && state.serror.is_none() {
682        return;
683    }
684    let Ok(cmsg) = CString::new(msg) else { return };
685    if let Some(err) = state.err {
686        // SAFETY: Caller-supplied callback from xmlSchemaSetParserErrors.
687        unsafe { err(state.ctx as *mut c_void, cmsg.as_ptr()) };
688    }
689    if let Some(serror) = state.serror {
690        // SAFETY: Caller-supplied callback from xmlSchemaSetParserStructuredErrors.
691        let mut e: _xmlError = unsafe { std::mem::zeroed() };
692        e.domain = XML_FROM_SCHEMASV;
693        e.code = 0;
694        e.message = cmsg.as_ptr() as *mut c_char;
695        e.level = xmlErrorLevel::XML_ERR_ERROR as c_int;
696        unsafe { serror(state.sctx as *mut c_void, &e) };
697    }
698}
699
700// ═══════════════════════════════════════════════════════════════════════════════
701// Document serialization helpers
702// ═══════════════════════════════════════════════════════════════════════════════
703
704/// Serialize a document to a Rust string.
705///
706/// # SAFETY
707///
708/// - `doc` must be a valid `_xmlDoc` pointer.
709unsafe fn doc_to_string(doc: *mut _xmlDoc) -> Option<String> {
710    if doc.is_null() {
711        return None;
712    }
713    let mut mem: *mut xmlChar = ptr::null_mut();
714    let mut size: c_int = 0;
715    // SAFETY: doc is valid; mem/size are writable locals.
716    unsafe { crate::xml::tree::xmlDocDumpFormatMemory(doc, &mut mem, &mut size, 0) };
717    if mem.is_null() {
718        return None;
719    }
720    // SAFETY: mem/size describe the dumped buffer.
721    let slice = unsafe { std::slice::from_raw_parts(mem as *const u8, size as usize) };
722    let out = String::from_utf8_lossy(slice).to_string();
723    // SAFETY: mem was allocated by the dumper; xmlFree is the matching free.
724    unsafe { xmlFreeImpl(mem as *mut c_void) };
725    Some(out)
726}
727
728/// Serialize a single element (subtree) to a Rust string.
729///
730/// # SAFETY
731///
732/// - `node` must be a valid `_xmlNode` pointer.
733unsafe fn node_to_string(node: *mut _xmlNode) -> Option<String> {
734    if node.is_null() {
735        return None;
736    }
737    // xmlBufferCreate returns a valid buffer or NULL.
738    let buf = crate::abi::exports_xml2::xmlBufferCreate();
739    if buf.is_null() {
740        return None;
741    }
742    // SAFETY: node and buf are valid; node->doc may be NULL, which the dumper tolerates.
743    unsafe { crate::xml::tree::xmlNodeDump(buf, (*node).doc, node, 0, 0) };
744    // buf is valid; content/length are readable fields.
745    let content = crate::abi::exports_xml2::xmlBufferContent(buf);
746    let len = crate::abi::exports_xml2::xmlBufferLength(buf);
747    if content.is_null() || len <= 0 {
748        // buf was created by xmlBufferCreate; xmlBufferFree matches.
749        crate::abi::exports_xml2::xmlBufferFree(buf);
750        return None;
751    }
752    // SAFETY: content/len describe the dumped element bytes.
753    let slice = unsafe { std::slice::from_raw_parts(content as *const u8, len as usize) };
754    let out = String::from_utf8_lossy(slice).to_string();
755    // buf was created by xmlBufferCreate; xmlBufferFree matches.
756    crate::abi::exports_xml2::xmlBufferFree(buf);
757    Some(out)
758}
759
760/// Reset the accumulated error state of a validation context.
761///
762/// # SAFETY
763///
764/// - `ctxt` must be a valid `XsdValidCtxt` pointer.
765unsafe fn reset_valid_ctxt(ctxt: *mut xmlSchemaValidCtxt) {
766    // SAFETY: ctxt is the internal XsdValidCtxt box.
767    let vc = unsafe { &mut * (ctxt as *mut XsdValidCtxt) };
768    vc.errors.clear();
769    vc.nb_errors = 0;
770}
771
772/// Parse an XML string into a doc and validate it with the internal engine,
773/// dispatching any errors through the context's callbacks.
774///
775/// Returns the number of validation errors, or -1 on internal error.
776///
777/// # SAFETY
778///
779/// - `ctxt` must be a valid `XsdValidCtxt` (as created by the internal
780///   `xmlSchemaNewValidCtxt`).
781unsafe fn validate_doc_string(ctxt: *mut xmlSchemaValidCtxt, xml: &str) -> c_int {
782    if ctxt.is_null() {
783        return -1;
784    }
785    // Reset so xmlSchemaIsValid reflects this run even when the internal
786    // engine only records errors on failure.
787    unsafe { reset_valid_ctxt(ctxt) };
788    // SAFETY: xmlReadMemory reads `xml` for `len` bytes; the pointer is valid.
789    let doc = unsafe {
790        crate::abi::exports_xml2::xmlReadMemory(
791            xml.as_ptr() as *const c_char,
792            xml.len() as c_int,
793            b"doc.xml\0".as_ptr() as *const c_char,
794            ptr::null(),
795            0,
796        )
797    };
798    if doc.is_null() {
799        // Mirror upstream xmlSchemaValidateFile: report and bail out with -1.
800        unsafe {
801            dispatch_valid_errors(
802                ctxt as usize,
803                &["Document is not well-formed".to_string()],
804            );
805        }
806        return -1;
807    }
808    // SAFETY: ctxt is a valid XsdValidCtxt; doc is a valid _xmlDoc.
809    let ret = unsafe { crate::xml::schemas::xmlSchemaValidateDoc(ctxt as *mut c_void, doc) };
810    // SAFETY: doc was created by xmlReadMemory; xmlFreeDoc matches.
811    unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
812    if ret != 0 {
813        // SAFETY: ctxt is the internal XsdValidCtxt whose errors were filled
814        // by xmlSchemaValidateDoc on failure.
815        let errors = unsafe { (* (ctxt as *mut XsdValidCtxt)).errors.clone() };
816        unsafe { dispatch_valid_errors(ctxt as usize, &errors) };
817    }
818    ret
819}
820
821// ═══════════════════════════════════════════════════════════════════════════════
822// xmlschemastypes.h — initialization / built-in types
823// ═══════════════════════════════════════════════════════════════════════════════
824
825/// Initialize the XML Schema datatype machinery.
826///
827/// # UPSTREAM-PARITY
828///
829/// ```c
830/// int xmlSchemaInitTypes(void);
831/// ```
832///
833/// Returns 0 on success. The internal engine initializes lazily; this
834/// eagerly creates the built-in type descriptors so subsequent
835/// `xmlSchemaGetBuiltInType` calls never fail.
836#[no_mangle]
837pub extern "C" fn xmlSchemaInitTypes() -> c_int {
838    // Force creation of the full built-in type table.
839    for t in VAL_STRING..=VAL_ANYSIMPLETYPE {
840        builtin_type(t);
841    }
842    0
843}
844
845/// Clean up the XML Schema datatype machinery.
846///
847/// # UPSTREAM-PARITY
848///
849/// ```c
850/// void xmlSchemaCleanupTypes(void);
851/// ```
852///
853/// Upstream frees its static tables at shutdown. The built-in descriptors
854/// here are intentionally leaked statics (their addresses are handed out to
855/// callers and must remain valid), so this is a no-op for ABI compatibility.
856#[no_mangle]
857pub extern "C" fn xmlSchemaCleanupTypes() {
858    // No-op: built-in type descriptors are process-lifetime statics.
859}
860
861/// Look up a predefined (built-in) type by name and namespace.
862///
863/// # UPSTREAM-PARITY
864///
865/// ```c
866/// xmlSchemaType * xmlSchemaGetPredefinedType(const xmlChar *name, const xmlChar *ns);
867/// ```
868///
869/// Matches upstream: both `name` and `ns` must be non-NULL and match the
870/// built-in type's name and the XML Schema namespace
871/// (`http://www.w3.org/2001/XMLSchema`).
872///
873/// # SAFETY
874///
875/// - `name`/`ns` must be valid NUL-terminated C strings or NULL.
876#[no_mangle]
877pub unsafe extern "C" fn xmlSchemaGetPredefinedType(
878    name: *const xmlChar,
879    ns: *const xmlChar,
880) -> *mut xmlSchemaType {
881    if name.is_null() || ns.is_null() {
882        return ptr::null_mut();
883    }
884    // SAFETY: Caller guarantees valid C strings.
885    let ns_str = unsafe { cstr_to_str(ns as *const c_char) }.unwrap_or_default();
886    if ns_str.as_bytes() != XSD_NS {
887        return ptr::null_mut();
888    }
889    for val_type in VAL_STRING..=VAL_ANYSIMPLETYPE {
890        // SAFETY: type_name returns static NUL-terminated bytes.
891        if let Some(bytes) = type_name(val_type) {
892            // SAFETY: caller guarantees a valid C string.
893            if unsafe { cstr_eq(name as *const c_char, &bytes[..bytes.len() - 1]) } {
894                return builtin_type(val_type) as *mut xmlSchemaType;
895            }
896        }
897    }
898    ptr::null_mut()
899}
900
901/// Get the built-in type descriptor for an `xmlSchemaValType`.
902///
903/// # UPSTREAM-PARITY
904///
905/// ```c
906/// xmlSchemaType * xmlSchemaGetBuiltInType(xmlSchemaValType type);
907/// ```
908///
909/// Returns NULL for `XML_SCHEMAS_UNKNOWN` and out-of-range values.
910#[no_mangle]
911pub extern "C" fn xmlSchemaGetBuiltInType(type_: c_int) -> *mut xmlSchemaType {
912    builtin_type(type_) as *mut xmlSchemaType
913}
914
915/// For a built-in list type, return its item type.
916///
917/// # UPSTREAM-PARITY
918///
919/// ```c
920/// xmlSchemaType * xmlSchemaGetBuiltInListSimpleTypeItemType(xmlSchemaType *type);
921/// ```
922///
923/// Returns NULL for non-list types. Implemented via the built-in descriptor
924/// table (NMTOKENS→NMTOKEN, IDREFS→IDREF, ENTITIES→ENTITY).
925///
926/// # SAFETY
927///
928/// - `type_` must be a descriptor returned by `xmlSchemaGetBuiltInType`/
929///   `xmlSchemaGetPredefinedType`, or NULL.
930#[no_mangle]
931pub unsafe extern "C" fn xmlSchemaGetBuiltInListSimpleTypeItemType(
932    type_: *mut xmlSchemaType,
933) -> *mut xmlSchemaType {
934    if type_.is_null() {
935        return ptr::null_mut();
936    }
937    // SAFETY: type_ is one of our descriptor pointers.
938    unsafe { (* (type_ as *mut XsdType)).item_type as *mut xmlSchemaType }
939}
940
941/// Whether `facetType` is a valid facet for the built-in type `type_`.
942///
943/// # UPSTREAM-PARITY
944///
945/// ```c
946/// int xmlSchemaIsBuiltInTypeFacet(xmlSchemaType *type, int facetType);
947/// ```
948///
949/// Returns 1 if valid, 0 otherwise. The check is coarse compared to
950/// upstream's per-type tables: string-family types accept the length/pattern/
951/// enumeration/whitespace facets, numeric types accept the bound/digit facets,
952/// and all types accept pattern/enumeration/whitespace.
953///
954/// # SAFETY
955///
956/// - `type_` must be a descriptor returned by `xmlSchemaGetBuiltInType`/
957///   `xmlSchemaGetPredefinedType`, or NULL.
958#[no_mangle]
959pub unsafe extern "C" fn xmlSchemaIsBuiltInTypeFacet(
960    type_: *mut xmlSchemaType,
961    facetType: c_int,
962) -> c_int {
963    if type_.is_null() {
964        return 0;
965    }
966    if facetType < FACET_MININCLUSIVE || facetType > FACET_MINLENGTH {
967        return 0;
968    }
969    // SAFETY: type_ is one of our descriptor pointers.
970    let val_type = unsafe { (* (type_ as *mut XsdType)).val_type };
971    let length_facets = matches!(
972        facetType,
973        FACET_LENGTH | FACET_MINLENGTH | FACET_MAXLENGTH | FACET_PATTERN | FACET_ENUMERATION
974            | FACET_WHITESPACE
975    );
976    let numeric_facets = matches!(
977        facetType,
978        FACET_MININCLUSIVE | FACET_MINEXCLUSIVE | FACET_MAXINCLUSIVE | FACET_MAXEXCLUSIVE
979            | FACET_TOTALDIGITS | FACET_FRACTIONDIGITS | FACET_PATTERN | FACET_ENUMERATION
980            | FACET_WHITESPACE
981    );
982    let universal = matches!(facetType, FACET_PATTERN | FACET_ENUMERATION | FACET_WHITESPACE);
983    if is_numeric_type(val_type) {
984        if numeric_facets {
985            1
986        } else {
987            0
988        }
989    } else if matches!(val_type, VAL_STRING | VAL_NORMSTRING | VAL_TOKEN) || is_list_type(val_type) {
990        if length_facets {
991            1
992        } else {
993            0
994        }
995    } else if universal {
996        1
997    } else {
998        0
999    }
1000}
1001
1002// ═══════════════════════════════════════════════════════════════════════════════
1003// xmlschemastypes.h — string whitespace helpers
1004// ═══════════════════════════════════════════════════════════════════════════════
1005
1006/// Replace whitespace characters with spaces and return a new string.
1007///
1008/// # UPSTREAM-PARITY
1009///
1010/// ```c
1011/// xmlChar * xmlSchemaWhiteSpaceReplace(const xmlChar *value);
1012/// ```
1013///
1014/// The result is xmlMalloc'd; the caller must free it with `xmlFree`.
1015///
1016/// # SAFETY
1017///
1018/// - `value` must be a valid NUL-terminated C string or NULL.
1019#[no_mangle]
1020pub unsafe extern "C" fn xmlSchemaWhiteSpaceReplace(value: *const xmlChar) -> *mut xmlChar {
1021    if value.is_null() {
1022        return ptr::null_mut();
1023    }
1024    // SAFETY: Caller guarantees a valid C string.
1025    let s = unsafe { cstr_to_str(value as *const c_char) }.unwrap_or_default();
1026    let out = whitespace_replace(&s);
1027    if let Ok(c) = CString::new(out) {
1028        // SAFETY: dup_cstr copies the string into xmlMalloc'd memory.
1029        unsafe { dup_cstr(c.as_ptr()) as *mut xmlChar }
1030    } else {
1031        ptr::null_mut()
1032    }
1033}
1034
1035/// Collapse whitespace and return a new string.
1036///
1037/// # UPSTREAM-PARITY
1038///
1039/// ```c
1040/// xmlChar * xmlSchemaCollapseString(const xmlChar *value);
1041/// ```
1042///
1043/// The result is xmlMalloc'd; the caller must free it with `xmlFree`.
1044///
1045/// # SAFETY
1046///
1047/// - `value` must be a valid NUL-terminated C string or NULL.
1048#[no_mangle]
1049pub unsafe extern "C" fn xmlSchemaCollapseString(value: *const xmlChar) -> *mut xmlChar {
1050    if value.is_null() {
1051        return ptr::null_mut();
1052    }
1053    // SAFETY: Caller guarantees a valid C string.
1054    let s = unsafe { cstr_to_str(value as *const c_char) }.unwrap_or_default();
1055    let out = whitespace_collapse(&s);
1056    if let Ok(c) = CString::new(out) {
1057        // SAFETY: dup_cstr copies the string into xmlMalloc'd memory.
1058        unsafe { dup_cstr(c.as_ptr()) as *mut xmlChar }
1059    } else {
1060        ptr::null_mut()
1061    }
1062}
1063
1064// ═══════════════════════════════════════════════════════════════════════════════
1065// xmlschemastypes.h — value objects
1066// ═══════════════════════════════════════════════════════════════════════════════
1067
1068/// Create a simple typed value.
1069///
1070/// # UPSTREAM-PARITY
1071///
1072/// ```c
1073/// xmlSchemaVal * xmlSchemaNewStringValue(xmlSchemaValType type, const xmlChar *value);
1074/// ```
1075///
1076/// For the built-in list types (NMTOKENS, IDREFS, ENTITIES) the value is
1077/// split on whitespace into an item chain, matching upstream's layout.
1078///
1079/// # SAFETY
1080///
1081/// - `value` must be a valid NUL-terminated C string or NULL.
1082#[no_mangle]
1083pub unsafe extern "C" fn xmlSchemaNewStringValue(
1084    type_: c_int,
1085    value: *const xmlChar,
1086) -> *mut xmlSchemaVal {
1087    // SAFETY: Delegates to new_string_value with the same contract.
1088    unsafe { new_string_value(type_, value as *const c_char) as *mut xmlSchemaVal }
1089}
1090
1091/// Create a NOTATION value.
1092///
1093/// # UPSTREAM-PARITY
1094///
1095/// ```c
1096/// xmlSchemaVal * xmlSchemaNewNOTATIONValue(const xmlChar *name, const xmlChar *ns);
1097/// ```
1098///
1099/// # SAFETY
1100///
1101/// - `name`/`ns` must be valid NUL-terminated C strings or NULL.
1102#[no_mangle]
1103pub unsafe extern "C" fn xmlSchemaNewNOTATIONValue(
1104    name: *const xmlChar,
1105    ns: *const xmlChar,
1106) -> *mut xmlSchemaVal {
1107    // SAFETY: Delegates to new_val with the same contract.
1108    unsafe { new_val(VAL_NOTATION, name as *const c_char, ns as *const c_char) as *mut xmlSchemaVal }
1109}
1110
1111/// Create a QName value.
1112///
1113/// # UPSTREAM-PARITY
1114///
1115/// ```c
1116/// xmlSchemaVal * xmlSchemaNewQNameValue(const xmlChar *namespaceName, const xmlChar *localName);
1117/// ```
1118///
1119/// # SAFETY
1120///
1121/// - `namespaceName`/`localName` must be valid NUL-terminated C strings or NULL.
1122#[no_mangle]
1123pub unsafe extern "C" fn xmlSchemaNewQNameValue(
1124    namespaceName: *const xmlChar,
1125    localName: *const xmlChar,
1126) -> *mut xmlSchemaVal {
1127    // SAFETY: Delegates to new_val with the same contract.
1128    unsafe { new_val(VAL_QNAME, localName as *const c_char, namespaceName as *const c_char) as *mut xmlSchemaVal }
1129}
1130
1131/// Deep-copy a value (including list chains).
1132///
1133/// # UPSTREAM-PARITY
1134///
1135/// ```c
1136/// xmlSchemaVal * xmlSchemaCopyValue(xmlSchemaVal *val);
1137/// ```
1138///
1139/// # SAFETY
1140///
1141/// - `val` must be a value created by this module or NULL.
1142#[no_mangle]
1143pub unsafe extern "C" fn xmlSchemaCopyValue(val: *mut xmlSchemaVal) -> *mut xmlSchemaVal {
1144    if val.is_null() {
1145        return ptr::null_mut();
1146    }
1147    // SAFETY: val is one of our XsdVal nodes.
1148    let mut src = val as *mut XsdVal;
1149    let mut head: *mut XsdVal = ptr::null_mut();
1150    let mut tail: *mut XsdVal = ptr::null_mut();
1151    while !src.is_null() {
1152        // SAFETY: each node is a valid XsdVal owned by this module.
1153        let (vtype, vvalue, vns) = unsafe { ((*src).val_type, (*src).value, (*src).ns) };
1154        let node = unsafe { new_val(vtype, vvalue as *const c_char, vns as *const c_char) };
1155        if node.is_null() {
1156            // Out of memory: free the partial chain.
1157            unsafe { free_val_chain(head) };
1158            return ptr::null_mut();
1159        }
1160        if head.is_null() {
1161            head = node;
1162        } else {
1163            // SAFETY: tail is a valid node from this loop.
1164            unsafe { (*tail).next = node };
1165        }
1166        tail = node;
1167        // SAFETY: src is a valid node from the chain.
1168        src = unsafe { (*src).next };
1169    }
1170    head as *mut xmlSchemaVal
1171}
1172
1173/// Free a value (and any list items).
1174///
1175/// # UPSTREAM-PARITY
1176///
1177/// ```c
1178/// void xmlSchemaFreeValue(xmlSchemaVal *val);
1179/// ```
1180///
1181/// # SAFETY
1182///
1183/// - `val` must be a value created by this module (or NULL).
1184#[no_mangle]
1185pub unsafe extern "C" fn xmlSchemaFreeValue(val: *mut xmlSchemaVal) {
1186    if val.is_null() {
1187        return;
1188    }
1189    // SAFETY: val is one of our XsdVal chains.
1190    unsafe { free_val_chain(val as *mut XsdVal) };
1191}
1192
1193/// Get the `xmlSchemaValType` of a value.
1194///
1195/// # UPSTREAM-PARITY
1196///
1197/// ```c
1198/// xmlSchemaValType xmlSchemaGetValType(xmlSchemaVal *val);
1199/// ```
1200///
1201/// # SAFETY
1202///
1203/// - `val` must be a value created by this module or NULL (returns 0).
1204#[no_mangle]
1205pub unsafe extern "C" fn xmlSchemaGetValType(val: *mut xmlSchemaVal) -> c_int {
1206    if val.is_null() {
1207        return VAL_UNKNOWN;
1208    }
1209    // SAFETY: val is one of our XsdVal nodes.
1210    unsafe { (* (val as *mut XsdVal)).val_type }
1211}
1212
1213/// Return the next item of a list value.
1214///
1215/// # UPSTREAM-PARITY
1216///
1217/// ```c
1218/// xmlSchemaVal * xmlSchemaValueGetNext(xmlSchemaVal *cur);
1219/// ```
1220///
1221/// # SAFETY
1222///
1223/// - `cur` must be a value created by this module or NULL.
1224#[no_mangle]
1225pub unsafe extern "C" fn xmlSchemaValueGetNext(cur: *mut xmlSchemaVal) -> *mut xmlSchemaVal {
1226    if cur.is_null() {
1227        return ptr::null_mut();
1228    }
1229    // SAFETY: cur is one of our XsdVal nodes.
1230    unsafe { (* (cur as *mut XsdVal)).next as *mut xmlSchemaVal }
1231}
1232
1233/// Return the string representation of a value.
1234///
1235/// # UPSTREAM-PARITY
1236///
1237/// ```c
1238/// const xmlChar * xmlSchemaValueGetAsString(xmlSchemaVal *val);
1239/// ```
1240///
1241/// The returned pointer is owned by the value and stays valid until the
1242/// value is freed. Returns NULL for a NULL argument.
1243///
1244/// # SAFETY
1245///
1246/// - `val` must be a value created by this module or NULL.
1247#[no_mangle]
1248pub unsafe extern "C" fn xmlSchemaValueGetAsString(val: *mut xmlSchemaVal) -> *const xmlChar {
1249    if val.is_null() {
1250        return ptr::null_mut();
1251    }
1252    // SAFETY: val is one of our XsdVal nodes; value is a valid C string or NULL.
1253    unsafe { (* (val as *mut XsdVal)).value as *const xmlChar }
1254}
1255
1256/// Return a boolean value as 1/0; -1 if the value is not boolean.
1257///
1258/// # UPSTREAM-PARITY
1259///
1260/// ```c
1261/// int xmlSchemaValueGetAsBoolean(xmlSchemaVal *val);
1262/// ```
1263///
1264/// # SAFETY
1265///
1266/// - `val` must be a value created by this module or NULL.
1267#[no_mangle]
1268pub unsafe extern "C" fn xmlSchemaValueGetAsBoolean(val: *mut xmlSchemaVal) -> c_int {
1269    if val.is_null() {
1270        return -1;
1271    }
1272    // SAFETY: val is one of our XsdVal nodes.
1273    let v = unsafe { &* (val as *mut XsdVal) };
1274    if v.val_type != VAL_BOOLEAN {
1275        return -1;
1276    }
1277    // SAFETY: v.value is a valid C string or NULL.
1278    let s = unsafe { cstr_to_str(v.value as *const c_char) }.unwrap_or_default();
1279    match s.as_str() {
1280        "true" | "1" => 1,
1281        "false" | "0" => 0,
1282        _ => -1,
1283    }
1284}
1285
1286/// Append `cur` to the list value `prev` (which may itself be a chain).
1287///
1288/// # UPSTREAM-PARITY
1289///
1290/// ```c
1291/// int xmlSchemaValueAppend(xmlSchemaVal *prev, xmlSchemaVal *cur);
1292/// ```
1293///
1294/// Returns 0 on success, -1 on error (NULL argument).
1295///
1296/// # SAFETY
1297///
1298/// - `prev`/`cur` must be values created by this module.
1299#[no_mangle]
1300pub unsafe extern "C" fn xmlSchemaValueAppend(
1301    prev: *mut xmlSchemaVal,
1302    cur: *mut xmlSchemaVal,
1303) -> c_int {
1304    if prev.is_null() || cur.is_null() {
1305        return -1;
1306    }
1307    // SAFETY: prev is one of our XsdVal chains.
1308    let mut tail = prev as *mut XsdVal;
1309    // SAFETY: walking the chain; all nodes are ours.
1310    while !unsafe { (*tail).next }.is_null() {
1311        tail = unsafe { (*tail).next };
1312    }
1313    // SAFETY: tail is the last node of our chain; cur is ours.
1314    unsafe { (*tail).next = cur as *mut XsdVal };
1315    0
1316}
1317
1318// ═══════════════════════════════════════════════════════════════════════════════
1319// xmlschemastypes.h — canonical values
1320// ═══════════════════════════════════════════════════════════════════════════════
1321
1322/// Compute the canonical representation of a value.
1323///
1324/// # UPSTREAM-PARITY
1325///
1326/// ```c
1327/// int xmlSchemaGetCanonValue(xmlSchemaVal *val, const xmlChar **retValue);
1328/// ```
1329///
1330/// Returns 0 on success and stores an xmlMalloc'd string in `*retValue`
1331/// (caller frees with `xmlFree`); -1 on error.
1332///
1333/// # SAFETY
1334///
1335/// - `val` must be a value created by this module; `retValue` must be a
1336///   writable non-NULL pointer.
1337#[no_mangle]
1338pub unsafe extern "C" fn xmlSchemaGetCanonValue(
1339    val: *mut xmlSchemaVal,
1340    retValue: *mut *const xmlChar,
1341) -> c_int {
1342    // SAFETY: Delegates with the same contract.
1343    unsafe { xmlSchemaGetCanonValueWhtsp(val, retValue, WS_COLLAPSE) }
1344}
1345
1346/// Compute the canonical representation of a value, applying a whitespace
1347/// transformation first.
1348///
1349/// # UPSTREAM-PARITY
1350///
1351/// ```c
1352/// int xmlSchemaGetCanonValueWhtsp(xmlSchemaVal *val, const xmlChar **retValue,
1353///                                 xmlSchemaWhitespaceValueType ws);
1354/// ```
1355///
1356/// Returns 0 on success and stores an xmlMalloc'd string in `*retValue`
1357/// (caller frees with `xmlFree`); -1 on error.
1358///
1359/// # SAFETY
1360///
1361/// - `val` must be a value created by this module; `retValue` must be a
1362///   writable non-NULL pointer.
1363#[no_mangle]
1364pub unsafe extern "C" fn xmlSchemaGetCanonValueWhtsp(
1365    val: *mut xmlSchemaVal,
1366    retValue: *mut *const xmlChar,
1367    ws: c_int,
1368) -> c_int {
1369    if val.is_null() || retValue.is_null() {
1370        return -1;
1371    }
1372    // SAFETY: val is one of our XsdVal nodes.
1373    let v = unsafe { &* (val as *mut XsdVal) };
1374    // List values canonicalize to their items joined by single spaces.
1375    if v.next.is_null() && !is_list_type(v.val_type) {
1376        let s = unsafe { cstr_to_str(v.value as *const c_char) }.unwrap_or_default();
1377        let norm = apply_ws(&s, ws);
1378        let canon = match v.val_type {
1379            t if is_numeric_type(t) => {
1380                if matches!(t, VAL_FLOAT | VAL_DOUBLE) {
1381                    match norm.as_str() {
1382                        "NaN" => "NaN".to_string(),
1383                        "INF" => "INF".to_string(),
1384                        "-INF" => "-INF".to_string(),
1385                        _ => match norm.parse::<f64>() {
1386                            Ok(f) if f.is_nan() => "NaN".to_string(),
1387                            Ok(f) if f.is_infinite() && f.is_sign_negative() => "-INF".to_string(),
1388                            Ok(f) if f.is_infinite() => "INF".to_string(),
1389                            Ok(f) => f.to_string(),
1390                            Err(_) => canonical_decimal(&norm),
1391                        },
1392                    }
1393                } else if matches!(t, VAL_DECIMAL) {
1394                    canonical_decimal(&norm)
1395                } else {
1396                    // Integer family: parse as i128 when possible.
1397                    match norm.parse::<i128>() {
1398                        Ok(i) => i.to_string(),
1399                        Err(_) => canonical_decimal(&norm),
1400                    }
1401                }
1402            }
1403            VAL_BOOLEAN => match norm.as_str() {
1404                "true" | "1" => "true".to_string(),
1405                "false" | "0" => "false".to_string(),
1406                _ => return -1,
1407            },
1408            _ => norm,
1409        };
1410        let Ok(c) = CString::new(canon) else { return -1 };
1411        // SAFETY: dup_cstr copies into xmlMalloc'd memory; caller frees.
1412        let out = unsafe { dup_cstr(c.as_ptr()) } as *const xmlChar;
1413        if out.is_null() {
1414            return -1;
1415        }
1416        // SAFETY: retValue is caller-guaranteed writable.
1417        unsafe { *retValue = out };
1418        0
1419    } else {
1420        // List value: canonicalize each item and join with single spaces.
1421        let mut parts: Vec<String> = Vec::new();
1422        let mut cur = val as *mut XsdVal;
1423        while !cur.is_null() {
1424            // SAFETY: cur is a node of our chain.
1425            let s = unsafe { cstr_to_str((*cur).value as *const c_char) }.unwrap_or_default();
1426            parts.push(apply_ws(&s, ws));
1427            cur = unsafe { (*cur).next };
1428        }
1429        let Ok(c) = CString::new(parts.join(" ")) else { return -1 };
1430        // SAFETY: dup_cstr copies into xmlMalloc'd memory; caller frees.
1431        let out = unsafe { dup_cstr(c.as_ptr()) } as *const xmlChar;
1432        if out.is_null() {
1433            return -1;
1434        }
1435        // SAFETY: retValue is caller-guaranteed writable.
1436        unsafe { *retValue = out };
1437        0
1438    }
1439}
1440
1441// ═══════════════════════════════════════════════════════════════════════════════
1442// xmlschemastypes.h — facets
1443// ═══════════════════════════════════════════════════════════════════════════════
1444
1445/// Create an empty facet.
1446///
1447/// # UPSTREAM-PARITY
1448///
1449/// ```c
1450/// xmlSchemaFacet * xmlSchemaNewFacet(void);
1451/// ```
1452///
1453/// The facet starts with type 0 and a NULL value; the value can be set
1454/// through `xmlSchemaGetFacetValueAsULong` consumers via the internal
1455/// representation. Returns NULL on allocation failure.
1456#[no_mangle]
1457pub extern "C" fn xmlSchemaNewFacet() -> *mut xmlSchemaFacet {
1458    // SAFETY: Box allocation is infallible modulo OOM abort.
1459    Box::into_raw(Box::new(XsdFacet {
1460        facet_type: 0,
1461        value: ptr::null_mut(),
1462        next: ptr::null_mut(),
1463    })) as *mut xmlSchemaFacet
1464}
1465
1466/// Free a facet.
1467///
1468/// # UPSTREAM-PARITY
1469///
1470/// ```c
1471/// void xmlSchemaFreeFacet(xmlSchemaFacet *facet);
1472/// ```
1473///
1474/// # SAFETY
1475///
1476/// - `facet` must be a facet created by this module or NULL.
1477#[no_mangle]
1478pub unsafe extern "C" fn xmlSchemaFreeFacet(facet: *mut xmlSchemaFacet) {
1479    if facet.is_null() {
1480        return;
1481    }
1482    // SAFETY: facet is one of our XsdFacet boxes.
1483    let f = unsafe { Box::from_raw(facet as *mut XsdFacet) };
1484    if !f.value.is_null() {
1485        // SAFETY: value was xmlMalloc'd by this module.
1486        unsafe { xmlFreeImpl(f.value as *mut c_void) };
1487    }
1488    drop(f);
1489}
1490
1491/// Parse a facet's value as an unsigned long.
1492///
1493/// # UPSTREAM-PARITY
1494///
1495/// ```c
1496/// unsigned long xmlSchemaGetFacetValueAsULong(xmlSchemaFacet *facet);
1497/// ```
1498///
1499/// Returns 0 when the facet is NULL or the value is not numeric.
1500///
1501/// # SAFETY
1502///
1503/// - `facet` must be a facet created by this module or NULL.
1504#[no_mangle]
1505pub unsafe extern "C" fn xmlSchemaGetFacetValueAsULong(facet: *mut xmlSchemaFacet) -> c_ulong {
1506    if facet.is_null() {
1507        return 0;
1508    }
1509    // SAFETY: facet is one of our XsdFacet boxes; value is a C string or NULL.
1510    let s = unsafe { cstr_to_str((* (facet as *mut XsdFacet)).value as *const c_char) }
1511        .unwrap_or_default();
1512    s.trim().parse::<u64>().unwrap_or(0) as c_ulong
1513}
1514
1515/// Check a facet for validity against a type.
1516///
1517/// # UPSTREAM-PARITY
1518///
1519/// ```c
1520/// int xmlSchemaCheckFacet(xmlSchemaFacet *facet, xmlSchemaType *typeDecl,
1521///                         xmlSchemaParserCtxt *ctxt, const xmlChar *name);
1522/// ```
1523///
1524/// Returns 0 if the facet is acceptable, -1 otherwise (reporting through the
1525/// parser context's error callbacks). Simplified: verifies the facet type is
1526/// known and valid for `typeDecl`; deeper value-level checks (upstream's
1527/// full facet construction) are handled by `xmlSchemaValidateFacet` at
1528/// validation time.
1529///
1530/// # SAFETY
1531///
1532/// - `facet`/`typeDecl`/`ctxt` must be objects created by this crate or NULL;
1533///   `name` must be a valid C string or NULL.
1534#[no_mangle]
1535pub unsafe extern "C" fn xmlSchemaCheckFacet(
1536    facet: *mut xmlSchemaFacet,
1537    typeDecl: *mut xmlSchemaType,
1538    ctxt: *mut xmlSchemaParserCtxt,
1539    name: *const xmlChar,
1540) -> c_int {
1541    if facet.is_null() {
1542        return -1;
1543    }
1544    // SAFETY: facet is one of our XsdFacet boxes.
1545    let facet_type = unsafe { (* (facet as *mut XsdFacet)).facet_type };
1546    if facet_type < FACET_MININCLUSIVE || facet_type > FACET_MINLENGTH {
1547        unsafe {
1548            dispatch_parser_error(ctxt as usize, "Invalid facet type");
1549        }
1550        return -1;
1551    }
1552    if !typeDecl.is_null() {
1553        let valid = xmlSchemaIsBuiltInTypeFacet(typeDecl, facet_type);
1554        if valid == 0 {
1555            let label = unsafe { cstr_to_str(name as *const c_char) }
1556                .unwrap_or_else(|| "facet".to_string());
1557            unsafe {
1558                dispatch_parser_error(
1559                    ctxt as usize,
1560                    &format!("Facet '{}' is not valid for this type", label),
1561                );
1562            }
1563            return -1;
1564        }
1565    }
1566    0
1567}
1568
1569/// Validate a value against a facet of a base type.
1570///
1571/// # UPSTREAM-PARITY
1572///
1573/// ```c
1574/// int xmlSchemaValidateFacet(xmlSchemaType *base, xmlSchemaFacet *facet,
1575///                            const xmlChar *value, xmlSchemaVal *val);
1576/// ```
1577///
1578/// Returns 0 if the value satisfies the facet, -1 otherwise. Implemented via
1579/// the internal engine's `xsd_validate_facet`.
1580///
1581/// # SAFETY
1582///
1583/// - `base`/`facet`/`val` must be objects created by this crate or NULL;
1584///   `value` must be a valid C string or NULL.
1585#[no_mangle]
1586pub unsafe extern "C" fn xmlSchemaValidateFacet(
1587    base: *mut xmlSchemaType,
1588    facet: *mut xmlSchemaFacet,
1589    value: *const xmlChar,
1590    val: *mut xmlSchemaVal,
1591) -> c_int {
1592    if facet.is_null() {
1593        return -1;
1594    }
1595    // SAFETY: facet is one of our boxes.
1596    let facet_type = unsafe { (* (facet as *mut XsdFacet)).facet_type };
1597    let Some(facet_kind) = facet_type_to_kind(facet_type) else {
1598        return -1;
1599    };
1600    // SAFETY: facet value is a C string or NULL.
1601    let facet_value = unsafe { cstr_to_str((* (facet as *mut XsdFacet)).value as *const c_char) }
1602        .unwrap_or_default();
1603    // Determine the base kind: from `base`, falling back to `val`, then string.
1604    let base_type = if !base.is_null() {
1605        // SAFETY: base is one of our descriptors.
1606        unsafe { (* (base as *mut XsdType)).val_type }
1607    } else if !val.is_null() {
1608        // SAFETY: val is one of our nodes.
1609        unsafe { (* (val as *mut XsdVal)).val_type }
1610    } else {
1611        VAL_STRING
1612    };
1613    let Some(kind) = val_type_to_kind(base_type) else {
1614        return -1;
1615    };
1616    // SAFETY: value is a caller-guaranteed C string or NULL.
1617    let raw = unsafe { cstr_to_str(value as *const c_char) }.unwrap_or_default();
1618    let norm = whitespace_collapse(&raw);
1619    if xsd_validate_facet(&kind, &norm, &facet_kind, &facet_value) {
1620        0
1621    } else {
1622        -1
1623    }
1624}
1625
1626/// Validate a value against a facet with explicit whitespace handling.
1627///
1628/// # UPSTREAM-PARITY
1629///
1630/// ```c
1631/// int xmlSchemaValidateFacetWhtsp(xmlSchemaFacet *facet,
1632///                                 xmlSchemaWhitespaceValueType fws,
1633///                                 xmlSchemaValType valType,
1634///                                 const xmlChar *value, xmlSchemaVal *val,
1635///                                 xmlSchemaWhitespaceValueType ws);
1636/// ```
1637///
1638/// Returns 0 if the value satisfies the facet, -1 otherwise.
1639///
1640/// # SAFETY
1641///
1642/// - `facet`/`val` must be objects created by this crate or NULL; `value`
1643///   must be a valid C string or NULL.
1644#[no_mangle]
1645pub unsafe extern "C" fn xmlSchemaValidateFacetWhtsp(
1646    facet: *mut xmlSchemaFacet,
1647    _fws: c_int,
1648    valType: c_int,
1649    value: *const xmlChar,
1650    val: *mut xmlSchemaVal,
1651    ws: c_int,
1652) -> c_int {
1653    if facet.is_null() {
1654        return -1;
1655    }
1656    // SAFETY: facet is one of our boxes.
1657    let facet_type = unsafe { (* (facet as *mut XsdFacet)).facet_type };
1658    let Some(facet_kind) = facet_type_to_kind(facet_type) else {
1659        return -1;
1660    };
1661    // SAFETY: facet value is a C string or NULL.
1662    let facet_value = unsafe { cstr_to_str((* (facet as *mut XsdFacet)).value as *const c_char) }
1663        .unwrap_or_default();
1664    let kind = if val.is_null() {
1665        val_type_to_kind(valType)
1666    } else {
1667        // SAFETY: val is one of our nodes.
1668        val_type_to_kind(unsafe { (* (val as *mut XsdVal)).val_type })
1669    };
1670    let Some(kind) = kind else {
1671        return -1;
1672    };
1673    // SAFETY: value is a caller-guaranteed C string or NULL.
1674    let raw = unsafe { cstr_to_str(value as *const c_char) }.unwrap_or_default();
1675    let norm = apply_ws(&raw, ws);
1676    if xsd_validate_facet(&kind, &norm, &facet_kind, &facet_value) {
1677        0
1678    } else {
1679        -1
1680    }
1681}
1682
1683/// Validate a length facet against a value, reporting the value's length.
1684///
1685/// # UPSTREAM-PARITY
1686///
1687/// ```c
1688/// int xmlSchemaValidateLengthFacet(xmlSchemaType *type, xmlSchemaFacet *facet,
1689///                                  const xmlChar *value, xmlSchemaVal *val,
1690///                                  unsigned long *length);
1691/// ```
1692///
1693/// Returns 0 if the facet holds, -1 otherwise. `*length` receives the
1694/// value's length (characters for string types, items for list types).
1695///
1696/// # SAFETY
1697///
1698/// - `type`/`facet`/`val` must be objects created by this crate or NULL;
1699///   `value` must be a valid C string or NULL; `length` must be writable.
1700#[no_mangle]
1701pub unsafe extern "C" fn xmlSchemaValidateLengthFacet(
1702    type_: *mut xmlSchemaType,
1703    facet: *mut xmlSchemaFacet,
1704    value: *const xmlChar,
1705    val: *mut xmlSchemaVal,
1706    length: *mut c_ulong,
1707) -> c_int {
1708    let val_type = if !type_.is_null() {
1709        // SAFETY: type_ is one of our descriptors.
1710        unsafe { (* (type_ as *mut XsdType)).val_type }
1711    } else if !val.is_null() {
1712        // SAFETY: val is one of our nodes.
1713        unsafe { (* (val as *mut XsdVal)).val_type }
1714    } else {
1715        VAL_STRING
1716    };
1717    // SAFETY: Delegates with the same contract.
1718    unsafe { xmlSchemaValidateLengthFacetWhtsp(facet, val_type, value, val, length, WS_PRESERVE) }
1719}
1720
1721/// Validate a length facet with explicit whitespace handling.
1722///
1723/// # UPSTREAM-PARITY
1724///
1725/// ```c
1726/// int xmlSchemaValidateLengthFacetWhtsp(xmlSchemaFacet *facet,
1727///                                       xmlSchemaValType valType,
1728///                                       const xmlChar *value, xmlSchemaVal *val,
1729///                                       unsigned long *length,
1730///                                       xmlSchemaWhitespaceValueType ws);
1731/// ```
1732///
1733/// Returns 0 if the facet holds, -1 otherwise. `*length` receives the
1734/// value's length (characters for string types, items for list types).
1735///
1736/// # SAFETY
1737///
1738/// - `facet`/`val` must be objects created by this crate or NULL; `value`
1739///   must be a valid C string or NULL; `length` must be writable.
1740#[no_mangle]
1741pub unsafe extern "C" fn xmlSchemaValidateLengthFacetWhtsp(
1742    facet: *mut xmlSchemaFacet,
1743    valType: c_int,
1744    value: *const xmlChar,
1745    val: *mut xmlSchemaVal,
1746    length: *mut c_ulong,
1747    ws: c_int,
1748) -> c_int {
1749    if facet.is_null() {
1750        return -1;
1751    }
1752    // SAFETY: facet is one of our boxes.
1753    let facet_type = unsafe { (* (facet as *mut XsdFacet)).facet_type };
1754    let facet_ulong = xmlSchemaGetFacetValueAsULong(facet);
1755    let len: u64 = if is_list_type(valType) {
1756        if val.is_null() {
1757            0
1758        } else {
1759            // Count items in the chain.
1760            let mut count: u64 = 0;
1761            let mut cur = val as *mut XsdVal;
1762            while !cur.is_null() {
1763                count += 1;
1764                // SAFETY: cur is a node of our chain.
1765                cur = unsafe { (*cur).next };
1766            }
1767            count
1768        }
1769    } else {
1770        // SAFETY: value is a caller-guaranteed C string or NULL.
1771        let raw = unsafe { cstr_to_str(value as *const c_char) }.unwrap_or_default();
1772        let norm = apply_ws(&raw, ws);
1773        norm.chars().count() as u64
1774    };
1775    if !length.is_null() {
1776        // SAFETY: caller-guaranteed writable.
1777        unsafe { *length = len as c_ulong };
1778    }
1779    let ok = match facet_type {
1780        FACET_LENGTH => len == facet_ulong,
1781        FACET_MINLENGTH => len >= facet_ulong,
1782        FACET_MAXLENGTH => len <= facet_ulong,
1783        _ => {
1784            // Not a length facet: upstream reports "not a length facet".
1785            return -1;
1786        }
1787    };
1788    if ok {
1789        0
1790    } else {
1791        -1
1792    }
1793}
1794
1795/// Validate a list-type facet given the actual number of items.
1796///
1797/// # UPSTREAM-PARITY
1798///
1799/// ```c
1800/// int xmlSchemaValidateListSimpleTypeFacet(xmlSchemaFacet *facet,
1801///                                          const xmlChar *value,
1802///                                          unsigned long actualLen,
1803///                                          unsigned long *expectedLen);
1804/// ```
1805///
1806/// For length facets, checks `actualLen` against the facet and stores the
1807/// facet's value in `*expectedLen`. Returns 0 if valid, -1 otherwise.
1808///
1809/// # SAFETY
1810///
1811/// - `facet` must be a facet created by this module or NULL; `value` must be
1812///   a valid C string or NULL; `expectedLen` must be writable.
1813#[no_mangle]
1814pub unsafe extern "C" fn xmlSchemaValidateListSimpleTypeFacet(
1815    facet: *mut xmlSchemaFacet,
1816    _value: *const xmlChar,
1817    actualLen: c_ulong,
1818    expectedLen: *mut c_ulong,
1819) -> c_int {
1820    if facet.is_null() {
1821        return -1;
1822    }
1823    // SAFETY: facet is one of our boxes.
1824    let facet_type = unsafe { (* (facet as *mut XsdFacet)).facet_type };
1825    let facet_ulong = xmlSchemaGetFacetValueAsULong(facet);
1826    if !expectedLen.is_null() {
1827        // SAFETY: caller-guaranteed writable.
1828        unsafe { *expectedLen = facet_ulong };
1829    }
1830    let ok = match facet_type {
1831        FACET_LENGTH => actualLen == facet_ulong,
1832        FACET_MINLENGTH => actualLen >= facet_ulong,
1833        FACET_MAXLENGTH => actualLen <= facet_ulong,
1834        _ => return -1,
1835    };
1836    if ok {
1837        0
1838    } else {
1839        -1
1840    }
1841}
1842
1843// ═══════════════════════════════════════════════════════════════════════════════
1844// xmlschemastypes.h — predefined-type validation
1845// ═══════════════════════════════════════════════════════════════════════════════
1846
1847/// Validate a value against a predefined (built-in) type.
1848///
1849/// # UPSTREAM-PARITY
1850///
1851/// ```c
1852/// int xmlSchemaValidatePredefinedType(xmlSchemaType *type, const xmlChar *value,
1853///                                     xmlSchemaVal **val);
1854/// ```
1855///
1856/// Returns 0 if the value is valid, -1 otherwise. If `val` is non-NULL it
1857/// receives a newly created value on success (caller frees it).
1858///
1859/// # SAFETY
1860///
1861/// - `type` must be a built-in descriptor from this crate; `value` must be a
1862///   valid C string; `val` must be writable or NULL.
1863#[no_mangle]
1864pub unsafe extern "C" fn xmlSchemaValidatePredefinedType(
1865    type_: *mut xmlSchemaType,
1866    value: *const xmlChar,
1867    val: *mut *mut xmlSchemaVal,
1868) -> c_int {
1869    // SAFETY: Delegates with the same contract (node is unused here).
1870    unsafe { xmlSchemaValPredefTypeNode(type_, value, val, ptr::null_mut()) }
1871}
1872
1873/// Validate a value against a predefined type, with whitespace normalization.
1874///
1875/// # UPSTREAM-PARITY
1876///
1877/// ```c
1878/// int xmlSchemaValPredefTypeNode(xmlSchemaType *type, const xmlChar *value,
1879///                                xmlSchemaVal **val, xmlNode *node);
1880/// ```
1881///
1882/// Returns 0 if the value is valid, -1 otherwise. If `val` is non-NULL it
1883/// receives a newly created value on success (caller frees it). `node` is
1884/// accepted for signature parity and used for error reporting (ignored here;
1885/// the internal engine validates strings).
1886///
1887/// # SAFETY
1888///
1889/// - `type` must be a built-in descriptor from this crate; `value` must be a
1890///   valid C string; `val` must be writable or NULL; `node` may be NULL.
1891#[no_mangle]
1892pub unsafe extern "C" fn xmlSchemaValPredefTypeNode(
1893    type_: *mut xmlSchemaType,
1894    value: *const xmlChar,
1895    val: *mut *mut xmlSchemaVal,
1896    _node: *mut _xmlNode,
1897) -> c_int {
1898    if type_.is_null() || value.is_null() {
1899        return -1;
1900    }
1901    // SAFETY: type_ is one of our descriptors.
1902    let val_type = unsafe { (* (type_ as *mut XsdType)).val_type };
1903    let Some(kind) = val_type_to_kind(val_type) else {
1904        return -1;
1905    };
1906    // SAFETY: value is a caller-guaranteed C string.
1907    let raw = unsafe { cstr_to_str(value as *const c_char) }.unwrap_or_default();
1908    // Whitespace normalization, as upstream does before validating.
1909    let norm = whitespace_collapse(&raw);
1910    if !xsd_validate_datatype(&kind, &norm, &[]) {
1911        return -1;
1912    }
1913    if !val.is_null() {
1914        // SAFETY: val is caller-guaranteed writable; new_string_value creates
1915        // a value with the same contract as xmlSchemaNewStringValue.
1916        *val = unsafe { new_string_value(val_type, value as *const c_char) } as *mut xmlSchemaVal;
1917    }
1918    0
1919}
1920
1921/// Validate a value against a predefined type, without normalization.
1922///
1923/// # UPSTREAM-PARITY
1924///
1925/// ```c
1926/// int xmlSchemaValPredefTypeNodeNoNorm(xmlSchemaType *type, const xmlChar *value,
1927///                                      xmlSchemaVal **val, xmlNode *node);
1928/// ```
1929///
1930/// Same as `xmlSchemaValPredefTypeNode` but the value is validated as-is
1931/// (no whitespace collapse). Returns 0 if valid, -1 otherwise.
1932///
1933/// # SAFETY
1934///
1935/// - `type` must be a built-in descriptor from this crate; `value` must be a
1936///   valid C string; `val` must be writable or NULL; `node` may be NULL.
1937#[no_mangle]
1938pub unsafe extern "C" fn xmlSchemaValPredefTypeNodeNoNorm(
1939    type_: *mut xmlSchemaType,
1940    value: *const xmlChar,
1941    val: *mut *mut xmlSchemaVal,
1942    _node: *mut _xmlNode,
1943) -> c_int {
1944    if type_.is_null() || value.is_null() {
1945        return -1;
1946    }
1947    // SAFETY: type_ is one of our descriptors.
1948    let val_type = unsafe { (* (type_ as *mut XsdType)).val_type };
1949    let Some(kind) = val_type_to_kind(val_type) else {
1950        return -1;
1951    };
1952    // SAFETY: value is a caller-guaranteed C string.
1953    let raw = unsafe { cstr_to_str(value as *const c_char) }.unwrap_or_default();
1954    if !xsd_validate_datatype(&kind, &raw, &[]) {
1955        return -1;
1956    }
1957    if !val.is_null() {
1958        // SAFETY: val is caller-guaranteed writable.
1959        *val = unsafe { new_string_value(val_type, value as *const c_char) } as *mut xmlSchemaVal;
1960    }
1961    0
1962}
1963
1964// ═══════════════════════════════════════════════════════════════════════════════
1965// xmlschemastypes.h — value comparison
1966// ═══════════════════════════════════════════════════════════════════════════════
1967
1968/// Compare two schema values.
1969///
1970/// # UPSTREAM-PARITY
1971///
1972/// ```c
1973/// int xmlSchemaCompareValues(xmlSchemaVal *x, xmlSchemaVal *y);
1974/// ```
1975///
1976/// Returns -2 on error, -1 if x < y, 0 if equal, 1 if x > y. Numeric values
1977/// are compared numerically, booleans as false < true, everything else as
1978/// (collapsed) strings.
1979///
1980/// # SAFETY
1981///
1982/// - `x`/`y` must be values created by this module or NULL.
1983#[no_mangle]
1984pub unsafe extern "C" fn xmlSchemaCompareValues(
1985    x: *mut xmlSchemaVal,
1986    y: *mut xmlSchemaVal,
1987) -> c_int {
1988    // SAFETY: Delegates with the same contract.
1989    unsafe { xmlSchemaCompareValuesWhtsp(x, WS_COLLAPSE, y, WS_COLLAPSE) }
1990}
1991
1992/// Compare two schema values with explicit whitespace handling.
1993///
1994/// # UPSTREAM-PARITY
1995///
1996/// ```c
1997/// int xmlSchemaCompareValuesWhtsp(xmlSchemaVal *x,
1998///                                 xmlSchemaWhitespaceValueType xws,
1999///                                 xmlSchemaVal *y,
2000///                                 xmlSchemaWhitespaceValueType yws);
2001/// ```
2002///
2003/// Returns -2 on error, -1 if x < y, 0 if equal, 1 if x > y.
2004///
2005/// # SAFETY
2006///
2007/// - `x`/`y` must be values created by this module or NULL.
2008#[no_mangle]
2009pub unsafe extern "C" fn xmlSchemaCompareValuesWhtsp(
2010    x: *mut xmlSchemaVal,
2011    xws: c_int,
2012    y: *mut xmlSchemaVal,
2013    yws: c_int,
2014) -> c_int {
2015    if x.is_null() || y.is_null() {
2016        return -2;
2017    }
2018    // SAFETY: x/y are our nodes.
2019    let (xt, xv, yt, yv) = unsafe {
2020        (
2021            (* (x as *mut XsdVal)).val_type,
2022            cstr_to_str((* (x as *mut XsdVal)).value as *const c_char).unwrap_or_default(),
2023            (* (y as *mut XsdVal)).val_type,
2024            cstr_to_str((* (y as *mut XsdVal)).value as *const c_char).unwrap_or_default(),
2025        )
2026    };
2027    let xs = apply_ws(&xv, xws);
2028    let ys = apply_ws(&yv, yws);
2029    if is_numeric_type(xt) && is_numeric_type(yt) {
2030        // Numeric comparison with INF/NaN handling.
2031        let to_f64 = |s: &str| -> Option<f64> {
2032            match s {
2033                "INF" => Some(f64::INFINITY),
2034                "-INF" => Some(f64::NEG_INFINITY),
2035                "NaN" => Some(f64::NAN),
2036                _ => s.parse::<f64>().ok(),
2037            }
2038        };
2039        match (to_f64(&xs), to_f64(&ys)) {
2040            (Some(a), Some(b)) => {
2041                if a.is_nan() || b.is_nan() {
2042                    return -2;
2043                }
2044                if a < b {
2045                    -1
2046                } else if a > b {
2047                    1
2048                } else {
2049                    0
2050                }
2051            }
2052            _ => -2,
2053        }
2054    } else if xt == VAL_BOOLEAN && yt == VAL_BOOLEAN {
2055        let b = |s: &str| -> Option<i32> {
2056            match s {
2057                "true" | "1" => Some(1),
2058                "false" | "0" => Some(0),
2059                _ => None,
2060            }
2061        };
2062        match (b(&xs), b(&ys)) {
2063            (Some(a), Some(c)) => a.cmp(&c) as c_int,
2064            _ => -2,
2065        }
2066    } else {
2067        match xs.cmp(&ys) {
2068            std::cmp::Ordering::Less => -1,
2069            std::cmp::Ordering::Equal => 0,
2070            std::cmp::Ordering::Greater => 1,
2071        }
2072    }
2073}
2074
2075// ═══════════════════════════════════════════════════════════════════════════════
2076// xmlschemastypes.h — free functions for types/wildcards
2077// ═══════════════════════════════════════════════════════════════════════════════
2078
2079/// Free a schema type.
2080///
2081/// # UPSTREAM-PARITY
2082///
2083/// ```c
2084/// void xmlSchemaFreeType(xmlSchemaType *type);
2085/// ```
2086///
2087/// Built-in type descriptors are process-lifetime statics owned by the
2088/// registry, so freeing one is a no-op. Foreign (non-registry) pointers are
2089/// dropped as this crate's descriptor boxes.
2090///
2091/// # SAFETY
2092///
2093/// - `type_` must be a pointer returned by this crate (or NULL).
2094#[no_mangle]
2095pub unsafe extern "C" fn xmlSchemaFreeType(type_: *mut xmlSchemaType) {
2096    if type_.is_null() {
2097        return;
2098    }
2099    let addr = type_ as usize;
2100    let registered = {
2101        let reg = TYPE_REGISTRY.lock();
2102        reg.values().any(|&v| v == addr)
2103    };
2104    if registered {
2105        // Static descriptor: nothing to free.
2106        return;
2107    }
2108    // SAFETY: The pointer was not registry-owned, so it must be a Box we
2109    // created (defensive; callers should only pass our descriptors).
2110    unsafe { drop(Box::from_raw(type_ as *mut XsdType)) };
2111}
2112
2113/// Free a schema wildcard.
2114///
2115/// # UPSTREAM-PARITY
2116///
2117/// ```c
2118/// void xmlSchemaFreeWildcard(xmlSchemaWildcard *wildcard);
2119/// ```
2120///
2121/// This crate never allocates wildcard objects (the internal engine has no
2122/// wildcard representation), so there is nothing owned to free; kept as a
2123/// no-op for ABI compatibility.
2124///
2125/// # SAFETY
2126///
2127/// - `wildcard` must be NULL or a pointer from this crate.
2128#[no_mangle]
2129pub unsafe extern "C" fn xmlSchemaFreeWildcard(_wildcard: *mut xmlSchemaWildcard) {
2130    // No-op: the internal engine does not allocate wildcard objects.
2131}
2132
2133// ═══════════════════════════════════════════════════════════════════════════════
2134// xmlschemas.h — parser context
2135// ═══════════════════════════════════════════════════════════════════════════════
2136
2137/// Create a schema parser context from an already-parsed document.
2138///
2139/// # UPSTREAM-PARITY
2140///
2141/// ```c
2142/// xmlSchemaParserCtxt * xmlSchemaNewDocParserCtxt(xmlDoc *doc);
2143/// ```
2144///
2145/// The document is serialized and compiled through the internal engine
2146/// (`xsd_parse`). Following the internal engine's convention,
2147/// `xmlSchemaParse` returns its context as the schema pointer, so the
2148/// returned context IS the compiled schema box. Returns NULL if `doc` is
2149/// NULL or the schema fails to compile.
2150///
2151/// # SAFETY
2152///
2153/// - `doc` must be a valid `_xmlDoc` pointer.
2154#[no_mangle]
2155pub unsafe extern "C" fn xmlSchemaNewDocParserCtxt(doc: *mut _xmlDoc) -> *mut xmlSchemaParserCtxt {
2156    // SAFETY: doc_to_string requires a valid doc.
2157    let Some(xml) = (unsafe { doc_to_string(doc) }) else {
2158        return ptr::null_mut();
2159    };
2160    match xsd_parse(&xml) {
2161        Ok(schema) => {
2162            Box::into_raw(Box::new(schema)) as *mut xmlSchemaParserCtxt
2163        }
2164        Err(_) => ptr::null_mut(),
2165    }
2166}
2167
2168/// Set the error/warning callbacks of a parser context.
2169///
2170/// # UPSTREAM-PARITY
2171///
2172/// ```c
2173/// void xmlSchemaSetParserErrors(xmlSchemaParserCtxt *ctxt,
2174///                               xmlSchemaValidityErrorFunc err,
2175///                               xmlSchemaValidityWarningFunc warn, void *ctx);
2176/// ```
2177///
2178/// The callbacks are stored in a side registry keyed by the context address.
2179///
2180/// # SAFETY
2181///
2182/// - `ctxt` must be a parser context created by this crate; `err`/`warn`
2183///   must be valid callbacks or NULL; `ctx` may be NULL.
2184#[no_mangle]
2185pub unsafe extern "C" fn xmlSchemaSetParserErrors(
2186    ctxt: *mut xmlSchemaParserCtxt,
2187    err: Option<xmlValidityErrorFunc>,
2188    warn: Option<xmlValidityWarningFunc>,
2189    ctx: *mut c_void,
2190) {
2191    if ctxt.is_null() {
2192        return;
2193    }
2194    let mut guard = PARSER_STATES.lock();
2195    let e = guard.entry(ctxt as usize).or_default();
2196    e.err = err;
2197    e.warn = warn;
2198    e.ctx = ctx as usize;
2199}
2200
2201/// Set the structured error callback of a parser context.
2202///
2203/// # UPSTREAM-PARITY
2204///
2205/// ```c
2206/// void xmlSchemaSetParserStructuredErrors(xmlSchemaParserCtxt *ctxt,
2207///                                         xmlStructuredErrorFunc serror, void *ctx);
2208/// ```
2209///
2210/// # SAFETY
2211///
2212/// - `ctxt` must be a parser context created by this crate; `serror` must be
2213///   a valid callback or NULL; `ctx` may be NULL.
2214#[no_mangle]
2215pub unsafe extern "C" fn xmlSchemaSetParserStructuredErrors(
2216    ctxt: *mut xmlSchemaParserCtxt,
2217    serror: Option<xmlStructuredErrorFunc>,
2218    ctx: *mut c_void,
2219) {
2220    if ctxt.is_null() {
2221        return;
2222    }
2223    let mut guard = PARSER_STATES.lock();
2224    let e = guard.entry(ctxt as usize).or_default();
2225    e.serror = serror;
2226    e.sctx = ctx as usize;
2227}
2228
2229/// Retrieve the error/warning callbacks of a parser context.
2230///
2231/// # UPSTREAM-PARITY
2232///
2233/// ```c
2234/// int xmlSchemaGetParserErrors(xmlSchemaParserCtxt *ctxt,
2235///                              xmlSchemaValidityErrorFunc *err,
2236///                              xmlSchemaValidityWarningFunc *warn, void **ctx);
2237/// ```
2238///
2239/// Returns 0 on success, -1 if `ctxt` is NULL. Output parameters may be NULL.
2240///
2241/// # SAFETY
2242///
2243/// - `ctxt` must be a parser context created by this crate; `err`/`warn`/
2244///   `ctx` must be writable or NULL.
2245#[no_mangle]
2246pub unsafe extern "C" fn xmlSchemaGetParserErrors(
2247    ctxt: *mut xmlSchemaParserCtxt,
2248    err: *mut Option<xmlValidityErrorFunc>,
2249    warn: *mut Option<xmlValidityWarningFunc>,
2250    ctx: *mut *mut c_void,
2251) -> c_int {
2252    if ctxt.is_null() {
2253        return -1;
2254    }
2255    let state = {
2256        let guard = PARSER_STATES.lock();
2257        guard.get(&(ctxt as usize)).copied().unwrap_or_default()
2258    };
2259    // SAFETY: output pointers are caller-guaranteed writable when non-NULL.
2260    unsafe {
2261        if !err.is_null() {
2262            *err = state.err;
2263        }
2264        if !warn.is_null() {
2265            *warn = state.warn;
2266        }
2267        if !ctx.is_null() {
2268            *ctx = state.ctx as *mut c_void;
2269        }
2270    }
2271    0
2272}
2273
2274// ═══════════════════════════════════════════════════════════════════════════════
2275// xmlschemas.h — validation context
2276// ═══════════════════════════════════════════════════════════════════════════════
2277
2278/// Set the error/warning callbacks of a validation context.
2279///
2280/// # UPSTREAM-PARITY
2281///
2282/// ```c
2283/// void xmlSchemaSetValidErrors(xmlSchemaValidCtxt *ctxt,
2284///                              xmlSchemaValidityErrorFunc err,
2285///                              xmlSchemaValidityWarningFunc warn, void *ctx);
2286/// ```
2287///
2288/// # SAFETY
2289///
2290/// - `ctxt` must be a validation context created by this crate; `err`/`warn`
2291///   must be valid callbacks or NULL; `ctx` may be NULL.
2292#[no_mangle]
2293pub unsafe extern "C" fn xmlSchemaSetValidErrors(
2294    ctxt: *mut xmlSchemaValidCtxt,
2295    err: Option<xmlValidityErrorFunc>,
2296    warn: Option<xmlValidityWarningFunc>,
2297    ctx: *mut c_void,
2298) {
2299    if ctxt.is_null() {
2300        return;
2301    }
2302    let mut guard = VALID_STATES.lock();
2303    let e = guard.entry(ctxt as usize).or_default();
2304    e.err = err;
2305    e.warn = warn;
2306    e.ctx = ctx as usize;
2307}
2308
2309/// Retrieve the error/warning callbacks of a validation context.
2310///
2311/// # UPSTREAM-PARITY
2312///
2313/// ```c
2314/// int xmlSchemaGetValidErrors(xmlSchemaValidCtxt *ctxt,
2315///                             xmlSchemaValidityErrorFunc *err,
2316///                             xmlSchemaValidityWarningFunc *warn, void **ctx);
2317/// ```
2318///
2319/// Returns 0 on success, -1 if `ctxt` is NULL. Output parameters may be NULL.
2320///
2321/// # SAFETY
2322///
2323/// - `ctxt` must be a validation context created by this crate; `err`/`warn`/
2324///   `ctx` must be writable or NULL.
2325#[no_mangle]
2326pub unsafe extern "C" fn xmlSchemaGetValidErrors(
2327    ctxt: *mut xmlSchemaValidCtxt,
2328    err: *mut Option<xmlValidityErrorFunc>,
2329    warn: *mut Option<xmlValidityWarningFunc>,
2330    ctx: *mut *mut c_void,
2331) -> c_int {
2332    if ctxt.is_null() {
2333        return -1;
2334    }
2335    let state = {
2336        let guard = VALID_STATES.lock();
2337        guard.get(&(ctxt as usize)).copied().unwrap_or_default()
2338    };
2339    // SAFETY: output pointers are caller-guaranteed writable when non-NULL.
2340    unsafe {
2341        if !err.is_null() {
2342            *err = state.err;
2343        }
2344        if !warn.is_null() {
2345            *warn = state.warn;
2346        }
2347        if !ctx.is_null() {
2348            *ctx = state.ctx as *mut c_void;
2349        }
2350    }
2351    0
2352}
2353
2354/// Set the structured error callback of a validation context.
2355///
2356/// # UPSTREAM-PARITY
2357///
2358/// ```c
2359/// void xmlSchemaSetValidStructuredErrors(xmlSchemaValidCtxt *ctxt,
2360///                                        xmlStructuredErrorFunc serror, void *ctx);
2361/// ```
2362///
2363/// # SAFETY
2364///
2365/// - `ctxt` must be a validation context created by this crate; `serror`
2366///   must be a valid callback or NULL; `ctx` may be NULL.
2367#[no_mangle]
2368pub unsafe extern "C" fn xmlSchemaSetValidStructuredErrors(
2369    ctxt: *mut xmlSchemaValidCtxt,
2370    serror: Option<xmlStructuredErrorFunc>,
2371    ctx: *mut c_void,
2372) {
2373    if ctxt.is_null() {
2374        return;
2375    }
2376    let mut guard = VALID_STATES.lock();
2377    let e = guard.entry(ctxt as usize).or_default();
2378    e.serror = serror;
2379    e.sctx = ctx as usize;
2380}
2381
2382/// Set validation options.
2383///
2384/// # UPSTREAM-PARITY
2385///
2386/// ```c
2387/// int xmlSchemaSetValidOptions(xmlSchemaValidCtxt *ctxt, int options);
2388/// ```
2389///
2390/// Accepts `XML_SCHEMA_VAL_VC_I_CREATE` and `XML_SCHEMA_VAL_XSI_ASSEMBLE`
2391/// (stored for the context; the internal engine does not branch on them).
2392/// Returns 0 on success, -1 if `ctxt` is NULL or the options are invalid.
2393///
2394/// # SAFETY
2395///
2396/// - `ctxt` must be a validation context created by this crate.
2397#[no_mangle]
2398pub unsafe extern "C" fn xmlSchemaSetValidOptions(
2399    ctxt: *mut xmlSchemaValidCtxt,
2400    options: c_int,
2401) -> c_int {
2402    if ctxt.is_null() {
2403        return -1;
2404    }
2405    if options & !VAL_OPTIONS_MASK != 0 {
2406        return -1;
2407    }
2408    VALID_STATES.lock().entry(ctxt as usize).or_default().options = options;
2409    0
2410}
2411
2412/// Get the validation options of a context.
2413///
2414/// # UPSTREAM-PARITY
2415///
2416/// ```c
2417/// int xmlSchemaValidCtxtGetOptions(xmlSchemaValidCtxt *ctxt);
2418/// ```
2419///
2420/// # SAFETY
2421///
2422/// - `ctxt` must be a validation context created by this crate or NULL.
2423#[no_mangle]
2424pub unsafe extern "C" fn xmlSchemaValidCtxtGetOptions(ctxt: *mut xmlSchemaValidCtxt) -> c_int {
2425    if ctxt.is_null() {
2426        return 0;
2427    }
2428    let guard = VALID_STATES.lock();
2429    guard.get(&(ctxt as usize)).map(|s| s.options).unwrap_or(0)
2430}
2431
2432/// Get the parser context associated with a validation context.
2433///
2434/// # UPSTREAM-PARITY
2435///
2436/// ```c
2437/// xmlParserCtxt * xmlSchemaValidCtxtGetParserCtxt(xmlSchemaValidCtxt *ctxt);
2438/// ```
2439///
2440/// Upstream returns the parser context the validator was created from during
2441/// streaming (SAX) validation. The internal engine performs DOM-based
2442/// validation and never creates an associated parser context, so NULL is
2443/// returned (upstream also returns NULL when none is associated).
2444///
2445/// # SAFETY
2446///
2447/// - `ctxt` may be NULL or a validation context created by this crate.
2448#[no_mangle]
2449pub unsafe extern "C" fn xmlSchemaValidCtxtGetParserCtxt(
2450    _ctxt: *mut xmlSchemaValidCtxt,
2451) -> *mut c_void {
2452    ptr::null_mut()
2453}
2454
2455/// Set the filename reported with validation errors.
2456///
2457/// # UPSTREAM-PARITY
2458///
2459/// ```c
2460/// void xmlSchemaValidateSetFilename(xmlSchemaValidCtxt *vctxt, const char *filename);
2461/// ```
2462///
2463/// The pointer is stored as-is (upstream does not copy it) and is used as
2464/// the `file` field of structured errors.
2465///
2466/// # SAFETY
2467///
2468/// - `vctxt` must be a validation context created by this crate; `filename`
2469///   must be a valid C string or NULL and stay alive while `vctxt` is used.
2470#[no_mangle]
2471pub unsafe extern "C" fn xmlSchemaValidateSetFilename(
2472    vctxt: *mut xmlSchemaValidCtxt,
2473    filename: *const c_char,
2474) {
2475    if vctxt.is_null() {
2476        return;
2477    }
2478    VALID_STATES.lock().entry(vctxt as usize).or_default().filename = filename as usize;
2479}
2480
2481/// Set a validity locator callback.
2482///
2483/// # UPSTREAM-PARITY
2484///
2485/// ```c
2486/// void xmlSchemaValidateSetLocator(xmlSchemaValidCtxt *vctxt,
2487///                                  xmlSchemaValidityLocatorFunc f, void *ctxt);
2488/// ```
2489///
2490/// The locator is stored; the internal engine reports errors without line
2491/// information, so the locator is not consulted.
2492///
2493/// # SAFETY
2494///
2495/// - `vctxt` must be a validation context created by this crate; `f` must be
2496///   a valid callback or NULL; `ctxt` may be NULL.
2497#[no_mangle]
2498pub unsafe extern "C" fn xmlSchemaValidateSetLocator(
2499    vctxt: *mut xmlSchemaValidCtxt,
2500    f: Option<xmlSchemaValidityLocatorFunc>,
2501    ctxt: *mut c_void,
2502) {
2503    if vctxt.is_null() {
2504        return;
2505    }
2506    let mut guard = VALID_STATES.lock();
2507    let e = guard.entry(vctxt as usize).or_default();
2508    e.locator = f;
2509    e.locator_ctx = ctxt as usize;
2510}
2511
2512/// Report whether the last validation had no errors.
2513///
2514/// # UPSTREAM-PARITY
2515///
2516/// ```c
2517/// int xmlSchemaIsValid(xmlSchemaValidCtxt *ctxt);
2518/// ```
2519///
2520/// Returns 1 if the last validation passed, 0 otherwise (and for NULL).
2521///
2522/// # SAFETY
2523///
2524/// - `ctxt` must be a validation context created by this crate or NULL.
2525#[no_mangle]
2526pub unsafe extern "C" fn xmlSchemaIsValid(ctxt: *mut xmlSchemaValidCtxt) -> c_int {
2527    if ctxt.is_null() {
2528        return 0;
2529    }
2530    // SAFETY: ctxt is the internal XsdValidCtxt whose nb_errors the internal
2531    // xmlSchemaValidateDoc updates.
2532    let nb = unsafe { (* (ctxt as *mut XsdValidCtxt)).nb_errors };
2533    if nb == 0 {
2534        1
2535    } else {
2536        0
2537    }
2538}
2539
2540// ═══════════════════════════════════════════════════════════════════════════════
2541// xmlschemas.h — validation entry points
2542// ═══════════════════════════════════════════════════════════════════════════════
2543
2544/// Validate an XML file against the context's schema.
2545///
2546/// # UPSTREAM-PARITY
2547///
2548/// ```c
2549/// int xmlSchemaValidateFile(xmlSchemaValidCtxt *ctxt, const char *filename, int options);
2550/// ```
2551///
2552/// Returns the number of validation errors (0 = valid), or -1 on internal
2553/// error (unreadable/unparseable file). Errors are reported through the
2554/// context's callbacks. Wraps the internal engine's `xmlSchemaValidateDoc`.
2555///
2556/// # SAFETY
2557///
2558/// - `ctxt` must be a validation context created by this crate; `filename`
2559///   must be a valid C string.
2560#[no_mangle]
2561pub unsafe extern "C" fn xmlSchemaValidateFile(
2562    ctxt: *mut xmlSchemaValidCtxt,
2563    filename: *const c_char,
2564    options: c_int,
2565) -> c_int {
2566    if ctxt.is_null() || filename.is_null() {
2567        return -1;
2568    }
2569    // Reset so xmlSchemaIsValid reflects this run even when the internal
2570    // engine only records errors on failure.
2571    unsafe { reset_valid_ctxt(ctxt) };
2572    // SAFETY: xmlReadFile requires a valid C string; options is forwarded.
2573    let doc = unsafe {
2574        crate::abi::exports_xml2::xmlReadFile(filename, ptr::null(), options)
2575    };
2576    if doc.is_null() {
2577        unsafe {
2578            dispatch_valid_errors(
2579                ctxt as usize,
2580                &["Failed to parse document".to_string()],
2581            );
2582        }
2583        return -1;
2584    }
2585    // SAFETY: ctxt is a valid XsdValidCtxt; doc is a valid _xmlDoc.
2586    let ret = unsafe { crate::xml::schemas::xmlSchemaValidateDoc(ctxt as *mut c_void, doc) };
2587    // SAFETY: doc was created by xmlReadFile; xmlFreeDoc matches.
2588    unsafe { crate::abi::exports_xml2::xmlFreeDoc(doc) };
2589    if ret != 0 {
2590        // SAFETY: ctxt is the internal XsdValidCtxt whose errors were filled
2591        // by xmlSchemaValidateDoc on failure.
2592        let errors = unsafe { (* (ctxt as *mut XsdValidCtxt)).errors.clone() };
2593        unsafe { dispatch_valid_errors(ctxt as usize, &errors) };
2594    }
2595    ret
2596}
2597
2598/// Validate a single element against the context's schema.
2599///
2600/// # UPSTREAM-PARITY
2601///
2602/// ```c
2603/// int xmlSchemaValidateOneElement(xmlSchemaValidCtxt *ctxt, xmlNode *elem);
2604/// ```
2605///
2606/// The element subtree is serialized and validated through the internal
2607/// engine (`xsd_validate`), which matches it against the global element
2608/// declarations of the schema. Returns the number of validation errors
2609/// (0 = valid), or -1 on internal error.
2610///
2611/// # SAFETY
2612///
2613/// - `ctxt` must be a validation context created by this crate; `elem` must
2614///   be a valid `_xmlNode` pointer.
2615#[no_mangle]
2616pub unsafe extern "C" fn xmlSchemaValidateOneElement(
2617    ctxt: *mut xmlSchemaValidCtxt,
2618    elem: *mut _xmlNode,
2619) -> c_int {
2620    if ctxt.is_null() || elem.is_null() {
2621        return -1;
2622    }
2623    // Reset so xmlSchemaIsValid reflects this run even when the internal
2624    // engine only records errors on failure.
2625    unsafe { reset_valid_ctxt(ctxt) };
2626    // SAFETY: ctxt is the internal XsdValidCtxt.
2627    let valid_ctxt = unsafe { &mut * (ctxt as *mut XsdValidCtxt) };
2628    let Some(schema) = valid_ctxt.schema.clone() else {
2629        return -1;
2630    };
2631    // SAFETY: node_to_string requires a valid node.
2632    let Some(xml) = (unsafe { node_to_string(elem) }) else {
2633        return -1;
2634    };
2635    match xsd_validate(&schema, &xml) {
2636        Ok(()) => 0,
2637        Err(errors) => {
2638            // Record the errors on the context so xmlSchemaGetValidErrors /
2639            // xmlSchemaIsValid see this run's state.
2640            valid_ctxt.errors = errors.clone();
2641            valid_ctxt.nb_errors = errors.len() as i32;
2642            unsafe { dispatch_valid_errors(ctxt as usize, &errors) };
2643            errors.len() as c_int
2644        }
2645    }
2646}
2647
2648/// Validate a stream of SAX events / input buffer content.
2649///
2650/// # UPSTREAM-PARITY
2651///
2652/// ```c
2653/// int xmlSchemaValidateStream(xmlSchemaValidCtxt *ctxt, xmlParserInputBuffer *input,
2654///                             xmlCharEncoding enc, const xmlSAXHandler *sax,
2655///                             void *user_data);
2656/// ```
2657///
2658/// The internal engine is DOM-based, so content is read from `input` via its
2659/// read callback (the only path in the internal engine that carries data)
2660/// and validated as a document. Returns the number of validation errors
2661/// (0 = valid), or -1 when no content can be obtained (NULL input, NULL
2662/// read callback — upstream SAX-push validation is not supported).
2663///
2664/// # SAFETY
2665///
2666/// - `ctxt` must be a validation context created by this crate; `input`,
2667///   `sax`, `user_data` must be valid or NULL as documented above.
2668#[no_mangle]
2669pub unsafe extern "C" fn xmlSchemaValidateStream(
2670    ctxt: *mut xmlSchemaValidCtxt,
2671    input: *mut _xmlParserInputBuffer,
2672    _enc: xmlCharEncoding,
2673    _sax: *const _xmlSAXHandler,
2674    _user_data: *mut c_void,
2675) -> c_int {
2676    if ctxt.is_null() || input.is_null() {
2677        return -1;
2678    }
2679    // SAFETY: input is a valid _xmlParserInputBuffer.
2680    let ib = unsafe { &*input };
2681    let Some(read) = ib.readcallback else {
2682        // The internal engine's memory/file input buffers carry no content,
2683        // so there is nothing to validate.
2684        return -1;
2685    };
2686    let mut content: Vec<u8> = Vec::new();
2687    let mut chunk = [0u8; 4096];
2688    // SAFETY: read is the caller-supplied callback; context/buffer are valid
2689    // for the duration of each call.
2690    loop {
2691        let n = unsafe { read(ib.context, chunk.as_mut_ptr() as *mut c_char, 4096) };
2692        if n <= 0 {
2693            break;
2694        }
2695        content.extend_from_slice(&chunk[..n as usize]);
2696    }
2697    let xml_str = String::from_utf8_lossy(&content).to_string();
2698    // SAFETY: Delegates to validate_doc_string with the same contract.
2699    unsafe { validate_doc_string(ctxt, &xml_str) }
2700}
2701
2702// ═══════════════════════════════════════════════════════════════════════════════
2703// xmlschemas.h — SAX plug
2704// ═══════════════════════════════════════════════════════════════════════════════
2705
2706/// Plug a schema validator into a SAX handler sequence.
2707///
2708/// # UPSTREAM-PARITY
2709///
2710/// ```c
2711/// xmlSchemaSAXPlugStruct * xmlSchemaSAXPlug(xmlSchemaValidCtxt *ctxt,
2712///                                           xmlSAXHandler **sax, void **user_data);
2713/// ```
2714///
2715/// Upstream replaces the caller's SAX handler with the validator's own and
2716/// returns a plug to restore it later. The internal engine performs DOM
2717/// validation and cannot intercept SAX events, so the plug is a pass-through:
2718/// `*sax` and `*user_data` are left untouched and a plug is returned so the
2719/// call sequence (plug → validate → unplug) still works. Returns NULL when
2720/// `ctxt` or `sax` is NULL.
2721///
2722/// # SAFETY
2723///
2724/// - `ctxt` must be a validation context created by this crate; `sax`/
2725///   `user_data` must be writable or NULL.
2726#[no_mangle]
2727pub unsafe extern "C" fn xmlSchemaSAXPlug(
2728    ctxt: *mut xmlSchemaValidCtxt,
2729    sax: *mut *mut _xmlSAXHandler,
2730    user_data: *mut *mut c_void,
2731) -> *mut xmlSchemaSAXPlugStruct {
2732    if ctxt.is_null() || sax.is_null() {
2733        return ptr::null_mut();
2734    }
2735    // SAFETY: sax/user_data are caller-guaranteed writable when non-NULL.
2736    let original_sax = unsafe { if sax.is_null() { ptr::null_mut() } else { *sax } };
2737    let original_ud = unsafe {
2738        if user_data.is_null() {
2739            ptr::null_mut()
2740        } else {
2741            *user_data
2742        }
2743    };
2744    let plug = Box::new(XsdSaxPlug {
2745        sax: original_sax as *const _xmlSAXHandler,
2746        user_data: original_ud,
2747    });
2748    Box::into_raw(plug) as *mut xmlSchemaSAXPlugStruct
2749}
2750
2751/// Unplug a schema validator from a SAX handler sequence.
2752///
2753/// # UPSTREAM-PARITY
2754///
2755/// ```c
2756/// int xmlSchemaSAXUnplug(xmlSchemaSAXPlugStruct *plug);
2757/// ```
2758///
2759/// Frees the plug. Returns 0 on success, -1 if `plug` is NULL.
2760///
2761/// # SAFETY
2762///
2763/// - `plug` must be a plug returned by `xmlSchemaSAXPlug` or NULL.
2764#[no_mangle]
2765pub unsafe extern "C" fn xmlSchemaSAXUnplug(plug: *mut xmlSchemaSAXPlugStruct) -> c_int {
2766    if plug.is_null() {
2767        return -1;
2768    }
2769    // SAFETY: plug is a Box created by xmlSchemaSAXPlug.
2770    unsafe { drop(Box::from_raw(plug as *mut XsdSaxPlug)) };
2771    0
2772}
2773
2774// ═══════════════════════════════════════════════════════════════════════════════
2775// xmlschemas.h — schema dump
2776// ═══════════════════════════════════════════════════════════════════════════════
2777
2778/// Dump a schema to a FILE*.
2779///
2780/// # UPSTREAM-PARITY
2781///
2782/// ```c
2783/// void xmlSchemaDump(FILE *output, xmlSchema *schema);
2784/// ```
2785///
2786/// Writes a human-readable listing of the compiled schema components
2787/// (target namespace, form defaults, component tree with types and
2788/// occurrence bounds) using the internal engine's `XsdSchema` data.
2789///
2790/// # SAFETY
2791///
2792/// - `output` must be a valid open `FILE*`; `schema` must be a schema
2793///   pointer produced by this crate's parser (or NULL).
2794#[no_mangle]
2795pub unsafe extern "C" fn xmlSchemaDump(output: *mut c_void, schema: *mut xmlSchema) {
2796    if output.is_null() || schema.is_null() {
2797        return;
2798    }
2799    // SAFETY: schema is the internal boxed XsdSchema.
2800    let s = unsafe { &* (schema as *const XsdSchema) };
2801    let mut out = String::new();
2802    out.push_str("Schema dump\n");
2803    if let Some(ref tns) = s.target_namespace {
2804        out.push_str(&format!("  target namespace: {}\n", tns));
2805    } else {
2806        out.push_str("  target namespace: (none)\n");
2807    }
2808    out.push_str(&format!(
2809        "  element form default: {}\n",
2810        s.element_form_default.as_deref().unwrap_or("unqualified")
2811    ));
2812    out.push_str(&format!(
2813        "  attribute form default: {}\n",
2814        s.attribute_form_default.as_deref().unwrap_or("unqualified")
2815    ));
2816    out.push_str("  components:\n");
2817    for c in &s.components {
2818        dump_component(&mut out, c, 2);
2819    }
2820    // SAFETY: output is a valid FILE*.
2821    unsafe { libc::fputs(out.as_ptr() as *const c_char, output as *mut libc::FILE) };
2822}
2823
2824/// Append a component (and its children) to the dump text.
2825fn dump_component(out: &mut String, c: &crate::xml::schemas::XsdComponent, depth: usize) {
2826    let indent = "  ".repeat(depth);
2827    let ctype = format!("{:?}", c.component_type).to_lowercase();
2828    let mut line = format!("{}{}", indent, ctype);
2829    if let Some(ref name) = c.name {
2830        line.push_str(&format!(" name='{}'", name));
2831    }
2832    if let Some(ref dtype) = c.datatype {
2833        line.push_str(&format!(" type={:?}", dtype));
2834    }
2835    if c.min_occurs != 1 || c.max_occurs != 1 {
2836        line.push_str(&format!(
2837            " minOccurs={} maxOccurs={}",
2838            c.min_occurs,
2839            if c.max_occurs == -1 {
2840                "unbounded".to_string()
2841            } else {
2842                c.max_occurs.to_string()
2843            }
2844        ));
2845    }
2846    if !c.facets.is_empty() {
2847        let facets: Vec<String> = c
2848            .facets
2849            .iter()
2850            .map(|(k, v)| format!("{:?}={}", k, v))
2851            .collect();
2852        line.push_str(&format!(" facets=[{}]", facets.join(", ")));
2853    }
2854    out.push_str(&line);
2855    out.push('\n');
2856    for child in &c.children {
2857        dump_component(out, child, depth + 1);
2858    }
2859    for attr in &c.attributes {
2860        dump_component(out, attr, depth + 1);
2861    }
2862}
2863
2864// ═══════════════════════════════════════════════════════════════════════════════
2865// schematron.h
2866// ═══════════════════════════════════════════════════════════════════════════════
2867
2868/// Create a Schematron parser context from an already-parsed document.
2869///
2870/// # UPSTREAM-PARITY
2871///
2872/// ```c
2873/// xmlSchematronParserCtxt * xmlSchematronNewDocParserCtxt(xmlDoc *doc);
2874/// ```
2875///
2876/// The document is serialized and compiled through the internal Schematron
2877/// engine (`schematron_parse`). Following that engine's convention,
2878/// `xmlSchematronParse` returns its context as the schema pointer. Returns
2879/// NULL if `doc` is NULL or the schema fails to compile.
2880///
2881/// # SAFETY
2882///
2883/// - `doc` must be a valid `_xmlDoc` pointer.
2884#[no_mangle]
2885pub unsafe extern "C" fn xmlSchematronNewDocParserCtxt(
2886    doc: *mut _xmlDoc,
2887) -> *mut xmlSchematronParserCtxt {
2888    // SAFETY: doc_to_string requires a valid doc.
2889    let Some(xml) = (unsafe { doc_to_string(doc) }) else {
2890        return ptr::null_mut();
2891    };
2892    match schematron_parse(&xml) {
2893        Ok(schema) => {
2894            Box::into_raw(Box::new(schema)) as *mut xmlSchematronParserCtxt
2895        }
2896        Err(_) => ptr::null_mut(),
2897    }
2898}
2899
2900/// Set the structured error callback of a Schematron validation context.
2901///
2902/// # UPSTREAM-PARITY
2903///
2904/// ```c
2905/// void xmlSchematronSetValidStructuredErrors(xmlSchematronValidCtxt *ctxt,
2906///                                            xmlStructuredErrorFunc serror, void *ctx);
2907/// ```
2908///
2909/// The callback is stored in a side registry keyed by the context address.
2910/// (The internal Schematron engine reports errors through its own channel;
2911/// the stored callback is not invoked by it.)
2912///
2913/// # SAFETY
2914///
2915/// - `ctxt` must be a Schematron validation context created by this crate;
2916///   `serror` must be a valid callback or NULL; `ctx` may be NULL.
2917#[no_mangle]
2918pub unsafe extern "C" fn xmlSchematronSetValidStructuredErrors(
2919    ctxt: *mut xmlSchematronValidCtxt,
2920    serror: Option<xmlStructuredErrorFunc>,
2921    ctx: *mut c_void,
2922) {
2923    if ctxt.is_null() {
2924        return;
2925    }
2926    let mut guard = SCHEMATRON_VALID_STATES.lock();
2927    let e = guard.entry(ctxt as usize).or_default();
2928    e.serror = serror;
2929    e.sctx = ctx as usize;
2930}