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