Skip to main content

libxml_rs/xml/validation/
mod.rs

1//! DTD validation (§27, §85 Phase 6).
2//!
3//! DTD validation against element/attribute declarations:
4//!
5//! 1. **Element content model validation** — does the element's child sequence
6//!    match its declared content model?
7//! 2. **Attribute value validation** — does each attribute's value conform to
8//!    its declared type (CDATA, ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN,
9//!    NMTOKENS, ENUMERATION, NOTATION)?
10//! 3. **ID/IDREF consistency** — are all ID values unique? Does each IDREF
11//!    reference a valid ID?
12//! 4. **Required attributes** — are all REQUIRED attributes present?
13//! 5. **NOTATION validation** — are NOTATION attributes referencing declared
14//!    notations?
15//! 6. **Well-formedness constraints** — additional validity constraints
16//!
17//! # UPSTREAM-PARITY
18//!
19//! This module follows upstream libxml2 `valid.c` implementation. The validation
20//! context (`xmlValidCtxt`) accumulates ID/IDREF tables across the document
21//! and checks consistency in `xmlValidateDocumentFinal`.
22//!
23//! # Phase 6 status
24//!
25//! Complete — all core DTD validation functions are implemented.
26//! Edge-case behavior for degenerate DTDs matches upstream.
27//!
28//! # Upstream contract
29//!
30//! Mirrors upstream valid.c (SRC-LIBXML2-2.15.0-VALID-C, oracle tree
31//! `oracle/historical/src/libxml2-2.15.0/valid.c`): xmlValidateDocument,
32//! xmlValidateDtd, xmlValidCtxt ID/IDREF tables, xmlValidateElement,
33//! xmlValidateAttributeDecl and the content-model validation walks.
34//!
35//! # Conceptual behavior
36//!
37//! DTD validation against element/attribute declarations: content-model
38//! matching, attribute value types (CDATA, ID, IDREF(S), ENTITY(IES),
39//! NMTOKEN(S), ENUMERATION, NOTATION), ID/IDREF consistency, REQUIRED
40//! attributes, NOTATION references and well-formedness constraints. The
41//! xmlValidCtxt accumulates ID/IDREF tables across the document and checks
42//! consistency in xmlValidateDocumentFinal.
43//!
44//! # Ownership & safety invariants
45//!
46//! Ownership: the validation context owns its ID/IDREF table storage and
47//! error output; the document and declarations are borrowed. SAFETY: the
48//! recursive validation walks are depth-bounded (VALID_CTXT_DEPTH_MAX = 256)
49//! to avoid stack exhaustion on degenerate DTDs.
50//!
51//! # Historical quirks & epochs
52//!
53//! E-005: --valid on an invalid document exits 3 from 2.13.0 (was 4);
54//! E-006: --valid with no DTD exits 0 from 2.15.0 (was 3) — the no-DTD-found
55//! failure stopped being exit-worthy. Parse-time ID/IDREF registration
56//! (xmlIsID/xmlAddID) was aligned with upstream in 11.1-N (R-000164); the
57//! `_xmlElement` mirror is 104 bytes (R-000139).
58//!
59//! # Deliberate oddities
60//!
61//! Deliberate oddities: element-decl type_ carries XML_ELEMENT_DECL while
62//! etype holds the element type (upstream field split); ATTLISTs for
63//! undeclared elements create UNDEFINED placeholders not linked into the DTD
64//! children (xmlGetDtdElementDesc semantics).
65//!
66//! # Proving courts
67//!
68//! DTD, RELAXNG and XSD court families; TREE-001 (ID/IDREF registration,
69//! atype = XML_ATTRIBUTE_ID), CLI-XMLLINT valid cases (exit codes 3/0 per
70//! E-005/E-006) and `cargo test --lib`.
71//!
72//! # Tempting simplifications that would break parity
73//!
74//! A tempting simplification is skipping parse-time ID registration and
75//! resolving IDs lazily at validation time — it would break doc->ids
76//! fingerprints and xmlIsID semantics (R-000164). Do not unbounded the
77//! recursion: VALID_CTXT_DEPTH_MAX mirrors the hardened oracle (SD-002).
78
79use core::ffi::c_void;
80use core::ptr;
81use std::os::raw::{c_char, c_int, c_uint};
82
83use crate::abi::allocator;
84use crate::abi::callbacks::xmlGenericErrorFunc;
85use crate::abi::structs::*;
86use crate::abi::types::xmlAttributeDefault::*;
87use crate::abi::types::xmlAttributeType::*;
88use crate::abi::types::xmlElementContentOccur::*;
89use crate::abi::types::xmlElementContentType::*;
90use crate::abi::types::xmlElementType::*;
91use crate::abi::types::xmlElementTypeVal::*;
92use crate::abi::types::xmlEntityType::*;
93use crate::abi::types::*;
94use crate::xml::dtd;
95use crate::xml::hash;
96use crate::xml::string;
97use crate::xml::tree;
98
99// ═══════════════════════════════════════════════════════════════════════════════
100// Constants
101// ═══════════════════════════════════════════════════════════════════════════════
102
103/// Maximum allowed depth for recursive validation walks.
104#[allow(dead_code)]
105const VALID_CTXT_DEPTH_MAX: c_int = 256;
106
107// ═══════════════════════════════════════════════════════════════════════════════
108// Validation Context
109// ═══════════════════════════════════════════════════════════════════════════════
110
111/// Create a new validation context.
112///
113/// # UPSTREAM-PARITY
114///
115/// ```c
116/// xmlValidCtxtPtr xmlNewValidCtxt(void);
117/// ```
118///
119/// Returns a new zero-initialized validation context, or NULL on OOM.
120///
121/// # SAFETY
122///
123/// The function touches crate-global state only; it is safe
124/// as long as the caller respects the library's global
125/// initialization/cleanup ordering (xmlInitParser before use,
126/// xmlCleanupParser only after all users are done).
127///
128/// Violating the global lifecycle ordering, or calling this after
129/// teardown or from a signal handler, is undefined behavior.
130pub unsafe fn new_valid_ctxt() -> *mut _xmlValidCtxt {
131    // SAFETY: Allocate zero-initialized memory for the validation context.
132    let ctxt = allocator::xmlMallocZero(size_of::<_xmlValidCtxt>() as usize) as *mut _xmlValidCtxt;
133    if ctxt.is_null() {
134        return ptr::null_mut();
135    }
136
137    unsafe {
138        (*ctxt).valid = 1;
139        (*ctxt).node = ptr::null_mut();
140        (*ctxt).doc = ptr::null_mut();
141        (*ctxt).nodeNr = 0;
142        (*ctxt).nodeMax = 0;
143        (*ctxt).nodeTab = ptr::null_mut();
144        (*ctxt).flags = 0;
145        (*ctxt).vstate = ptr::null_mut();
146        (*ctxt).vstateNr = 0;
147        (*ctxt).vstateMax = 0;
148        (*ctxt).vstateTab = ptr::null_mut();
149        (*ctxt).am = ptr::null_mut();
150        (*ctxt).state = ptr::null_mut();
151        (*ctxt).error = None;
152        (*ctxt).warning = None;
153        (*ctxt).userData = ptr::null_mut();
154    }
155
156    ctxt
157}
158
159/// Free a validation context.
160///
161/// # UPSTREAM-PARITY
162///
163/// ```c
164/// void xmlFreeValidCtxt(xmlValidCtxtPtr ctxt);
165/// ```
166///
167/// # SAFETY
168///
169/// - `ctxt` must be a valid pointer to an _xmlValidCtxt, or NULL.
170pub unsafe fn free_valid_ctxt(ctxt: *mut _xmlValidCtxt) {
171    if ctxt.is_null() {
172        return;
173    }
174
175    unsafe {
176        let c = &mut *ctxt;
177
178        // Free node stack
179        if !c.nodeTab.is_null() {
180            allocator::xmlFreeImpl(c.nodeTab as *mut c_void);
181        }
182
183        // Free automata
184        if !c.am.is_null() {
185            // Automata free — currently a no-op since am is opaque.
186            // UPSTREAM-PARITY: xmlFreeAutomata(c.am) in upstream.
187        }
188
189        // Free state
190        if !c.state.is_null() {
191            // State free — currently a no-op.
192        }
193
194        allocator::xmlFreeImpl(ctxt as *mut c_void);
195    }
196}
197
198/// Set error and warning callbacks on a validation context.
199///
200/// # UPSTREAM-PARITY
201///
202/// ```c
203/// void xmlSetValidErrors(xmlValidCtxtPtr ctxt,
204///                        xmlGenericErrorFunc err,
205///                        xmlGenericErrorFunc warn,
206///                        void *data);
207/// ```
208///
209/// # SAFETY
210///
211/// - `ctxt` may be NULL (no-op).
212/// - `err`, `warn`, `data` may be NULL.
213pub unsafe fn set_valid_errors(
214    ctxt: *mut _xmlValidCtxt,
215    err: Option<xmlGenericErrorFunc>,
216    warn: Option<xmlGenericErrorFunc>,
217    data: *mut c_void,
218) {
219    if ctxt.is_null() {
220        return;
221    }
222
223    unsafe {
224        // UPSTREAM-PARITY: libxml2 stores these as xmlValidityErrorFunc
225        // but accepts xmlGenericErrorFunc in the setter.
226        (*ctxt).error = err;
227        (*ctxt).warning = warn;
228        (*ctxt).userData = data;
229    }
230}
231
232// ═══════════════════════════════════════════════════════════════════════════════
233// Internal helpers
234// ═══════════════════════════════════════════════════════════════════════════════
235
236/// Report a validation error through the context.
237///
238/// # SAFETY
239///
240/// - `ctxt` may be NULL.
241/// - `msg` must be a valid null-terminated C string.
242unsafe fn vctxt_error(ctxt: *mut _xmlValidCtxt, msg: *const c_char) {
243    if ctxt.is_null() {
244        return;
245    }
246    unsafe {
247        let c = &mut *ctxt;
248        c.valid = 0;
249        if let Some(err) = c.error {
250            err(c.userData, msg);
251        }
252        // UPSTREAM-PARITY (valid.c xmlErrValid -> __xmlRaiseError): raise the
253        // error through the global STRUCTURED handler as well, so consumers
254        // like nokogiri that install a structured error func (xmlSetStructured
255        // ErrorFunc in DTD#validate) collect it into their error list. Only
256        // when a structured handler is registered — the generic fallback would
257        // double-report what the validation context's own `error` callback
258        // (used by xmllint --valid) already printed.
259        let has_structured = crate::xml::globals::with_structured_error(|h, _| h.is_some());
260        if has_structured {
261            crate::xml::errors::raise_error(
262                ctxt as *mut c_void,
263                ptr::null_mut(),
264                ptr::null_mut(),
265                ptr::null_mut(),
266                ptr::null_mut(),
267                crate::abi::types::XML_FROM_VALID,
268                0,
269                crate::abi::types::xmlErrorLevel::XML_ERR_ERROR as c_int,
270                ptr::null(),
271                0,
272                ptr::null(),
273                ptr::null(),
274                ptr::null(),
275                0,
276                0,
277                msg,
278            );
279        }
280    }
281}
282
283/// Push a node onto the validation context's node stack.
284///
285/// Returns 0 on success, -1 on failure.
286///
287/// # SAFETY
288///
289/// - `ctxt` must be a valid pointer.
290unsafe fn vctxt_push_node(ctxt: *mut _xmlValidCtxt, node: *mut _xmlNode) -> c_int {
291    unsafe {
292        let c = &mut *ctxt;
293
294        if c.nodeNr >= c.nodeMax {
295            let new_max = if c.nodeMax == 0 { 4 } else { c.nodeMax * 2 };
296            let new_tab = allocator::xmlReallocImpl(
297                c.nodeTab as *mut c_void,
298                (new_max as usize) * size_of::<*mut _xmlNode>(),
299            ) as *mut *mut _xmlNode;
300            if new_tab.is_null() {
301                return -1;
302            }
303            c.nodeTab = new_tab;
304            c.nodeMax = new_max;
305        }
306
307        *c.nodeTab.add(c.nodeNr as usize) = node;
308        c.nodeNr += 1;
309        c.node = node;
310    }
311    0
312}
313
314/// Pop a node from the validation context's node stack.
315///
316/// # SAFETY
317///
318/// - `ctxt` must be a valid pointer.
319unsafe fn vctxt_pop_node(ctxt: *mut _xmlValidCtxt) {
320    unsafe {
321        let c = &mut *ctxt;
322        if c.nodeNr > 0 {
323            c.nodeNr -= 1;
324        }
325        if c.nodeNr > 0 {
326            c.node = *c.nodeTab.add((c.nodeNr - 1) as usize);
327        } else {
328            c.node = ptr::null_mut();
329        }
330    }
331}
332
333/// Get the DTD to validate against for a given document.
334///
335/// Returns the internal subset first, then the external subset.
336///
337/// # SAFETY
338///
339/// - `doc` must be a valid pointer or NULL.
340unsafe fn get_valid_dtd(doc: *mut _xmlDoc) -> *mut _xmlDtd {
341    if doc.is_null() {
342        return ptr::null_mut();
343    }
344    unsafe {
345        let d = &*doc;
346        if !d.intSubset.is_null() {
347            d.intSubset
348        } else {
349            d.extSubset
350        }
351    }
352}
353
354// ═══════════════════════════════════════════════════════════════════════════════
355// XML Name / NMTOKEN Character Classification
356// ═══════════════════════════════════════════════════════════════════════════════
357
358/// Check if a character is a valid XML Name start character.
359///
360/// # UPSTREAM-PARITY
361///
362/// Matches the XML 1.0 Fifth Edition NameStartChar production:
363/// `[a-zA-Z_:] | [\xC0-\xD6] | [\xD8-\xF6] | [\xF8-\u{2FF}] |
364///  [\u{370}-\u{37D}] | [\u{37F}-\u{1FFF}] | [\u{200C}-\u{200D}] |
365///  [\u{2070}-\u{218F}] | [\u{2C00}-\u{2FEF}] | [\u{3001}-\u{D7FF}] |
366///  [\u{F900}-\u{FDCF}] | [\u{FDF0}-\u{FFFD}]`
367pub(crate) const fn is_xml_name_start(c: char) -> bool {
368    matches!(c,
369        'a'..='z' | 'A'..='Z' | '_' | ':' |
370        '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{2FF}' |
371        '\u{370}'..='\u{37D}' | '\u{37F}'..='\u{1FFF}' |
372        '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}' |
373        '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' |
374        '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' |
375        '\u{10000}'..='\u{EFFFF}'
376    )
377}
378
379/// Check if a character is a valid XML Name character.
380///
381/// # UPSTREAM-PARITY
382///
383/// Matches NameChar production: NameStartChar | '-' | '.' | [0-9] |
384/// \u{B7} | [\u{0300}-\u{036F}] | [\u{203F}-\u{2040}]
385pub(crate) const fn is_xml_name_char(c: char) -> bool {
386    is_xml_name_start(c)
387        || matches!(c,
388            '-' | '.' | '0'..='9' | '\u{B7}' |
389            '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
390        )
391}
392
393// ═══════════════════════════════════════════════════════════════════════════════
394// xmlValidateName / xmlValidateNames
395// ═══════════════════════════════════════════════════════════════════════════════
396
397/// Validate whether `value` is a valid XML Name.
398///
399/// # UPSTREAM-PARITY
400///
401/// ```c
402/// int xmlValidateName(const xmlChar *value);
403/// ```
404///
405/// Returns 1 if valid, 0 if not.
406///
407/// # SAFETY
408///
409/// - `value` must be a valid null-terminated string or NULL.
410pub unsafe fn validate_name(value: *const xmlChar) -> c_int {
411    if value.is_null() {
412        return 0;
413    }
414
415    let s = unsafe { string::xmlstr_to_bytes(value) };
416    let s = core::str::from_utf8(s).unwrap_or("");
417
418    if s.is_empty() {
419        return 0;
420    }
421
422    let mut chars = s.chars();
423
424    // First character must be a NameStartChar
425    match chars.next() {
426        Some(c) if is_xml_name_start(c) => {}
427        _ => return 0,
428    }
429
430    // Remaining characters must be NameChars
431    for c in chars {
432        if !is_xml_name_char(c) {
433            return 0;
434        }
435    }
436
437    1
438}
439
440/// Validate whether `value` is a whitespace-separated list of XML Names.
441///
442/// # UPSTREAM-PARITY
443///
444/// ```c
445/// int xmlValidateNames(const xmlChar *value);
446/// ```
447///
448/// Returns 1 if valid, 0 if not.
449///
450/// # SAFETY
451///
452/// - `value` must be a valid null-terminated string or NULL.
453pub unsafe fn validate_names(value: *const xmlChar) -> c_int {
454    if value.is_null() {
455        return 0;
456    }
457
458    let s = unsafe { string::xmlstr_to_bytes(value) };
459    let s = core::str::from_utf8(s).unwrap_or("");
460
461    if s.is_empty() {
462        return 0;
463    }
464
465    for token in s.split_whitespace() {
466        if token.is_empty() {
467            return 0;
468        }
469        let mut chars = token.chars();
470        match chars.next() {
471            Some(c) if is_xml_name_start(c) => {}
472            _ => return 0,
473        }
474        for c in chars {
475            if !is_xml_name_char(c) {
476                return 0;
477            }
478        }
479    }
480
481    1
482}
483
484// ═══════════════════════════════════════════════════════════════════════════════
485// xmlValidateNmtoken / xmlValidateNmtokens
486// ═══════════════════════════════════════════════════════════════════════════════
487
488/// Validate whether `value` is a valid XML NMTOKEN.
489///
490/// # UPSTREAM-PARITY
491///
492/// ```c
493/// int xmlValidateNmtoken(const xmlChar *value);
494/// ```
495///
496/// An NMTOKEN is like a Name but the first character can also be a NameChar
497/// (not just a NameStartChar). Returns 1 if valid, 0 if not.
498///
499/// # SAFETY
500///
501/// - `value` must be a valid null-terminated string or NULL.
502pub unsafe fn validate_nmtoken(value: *const xmlChar) -> c_int {
503    if value.is_null() {
504        return 0;
505    }
506
507    let s = unsafe { string::xmlstr_to_bytes(value) };
508    let s = core::str::from_utf8(s).unwrap_or("");
509
510    if s.is_empty() {
511        return 0;
512    }
513
514    for c in s.chars() {
515        if !is_xml_name_char(c) {
516            return 0;
517        }
518    }
519
520    1
521}
522
523/// Validate whether `value` is a whitespace-separated list of XML NMTOKENs.
524///
525/// # UPSTREAM-PARITY
526///
527/// ```c
528/// int xmlValidateNmtokens(const xmlChar *value);
529/// ```
530///
531/// Returns 1 if valid, 0 if not.
532///
533/// # SAFETY
534///
535/// - `value` must be a valid null-terminated string or NULL.
536pub unsafe fn validate_nmtokens(value: *const xmlChar) -> c_int {
537    if value.is_null() {
538        return 0;
539    }
540
541    let s = unsafe { string::xmlstr_to_bytes(value) };
542    let s = core::str::from_utf8(s).unwrap_or("");
543
544    if s.is_empty() {
545        return 0;
546    }
547
548    for token in s.split_whitespace() {
549        if token.is_empty() {
550            return 0;
551        }
552        for c in token.chars() {
553            if !is_xml_name_char(c) {
554                return 0;
555            }
556        }
557    }
558
559    1
560}
561
562// ═══════════════════════════════════════════════════════════════════════════════
563// xmlValidateAttributeValue
564// ═══════════════════════════════════════════════════════════════════════════════
565
566/// Validate an attribute value against its declared type.
567///
568/// # UPSTREAM-PARITY
569///
570/// ```c
571/// int xmlValidateAttributeValue(int type, const xmlChar *value);
572/// ```
573///
574/// Returns 1 if the value is valid for the given attribute type, 0 otherwise.
575///
576/// # SAFETY
577///
578/// - `value` must be a valid null-terminated string or NULL.
579pub unsafe fn validate_attribute_value(atype: c_int, value: *const xmlChar) -> c_int {
580    // UPSTREAM-PARITY: upstream xmlValidateAttributeValue dispatches to
581    // xmlValidateAttributeValueInternal(NULL, type, value) whose switch
582    // matches this exactly; CDATA (and unknown types) fall through to 1.
583    match atype as u32 {
584        t if t == XML_ATTRIBUTE_ENTITIES as u32 || t == XML_ATTRIBUTE_IDREFS as u32 => {
585            validate_values_internal(value, 0)
586        }
587        t if t == XML_ATTRIBUTE_ENTITY as u32
588            || t == XML_ATTRIBUTE_IDREF as u32
589            || t == XML_ATTRIBUTE_ID as u32
590            || t == XML_ATTRIBUTE_NOTATION as u32 =>
591        {
592            validate_value_internal(value, 0)
593        }
594        t if t == XML_ATTRIBUTE_NMTOKENS as u32 || t == XML_ATTRIBUTE_ENUMERATION as u32 => {
595            validate_values_internal(value, XML_SCAN_NMTOKEN)
596        }
597        t if t == XML_ATTRIBUTE_NMTOKEN as u32 => validate_value_internal(value, XML_SCAN_NMTOKEN),
598        _ => 1, // CDATA / unknown
599    }
600}
601
602// ═══════════════════════════════════════════════════════════════════════════════
603// xmlValidateEnumeration
604// ═══════════════════════════════════════════════════════════════════════════════
605
606/// Validate that `value` is one of the values in the enumeration.
607///
608/// # UPSTREAM-PARITY
609///
610/// ```c
611/// int xmlValidateEnumeration(xmlValidCtxtPtr ctxt,
612///                            const xmlChar *value,
613///                            xmlEnumerationPtr tree);
614/// ```
615///
616/// Returns 1 if the value is in the enumeration, 0 otherwise.
617///
618/// # SAFETY
619///
620/// - `ctxt` may be NULL.
621/// - `value` must be a valid null-terminated string or NULL.
622/// - `tree` may be NULL (returns 0).
623pub unsafe fn validate_enumeration(
624    ctxt: *mut _xmlValidCtxt,
625    value: *const xmlChar,
626    tree: *mut _xmlEnumeration,
627) -> c_int {
628    if value.is_null() || tree.is_null() {
629        return 0;
630    }
631
632    let mut cur = tree;
633    while !cur.is_null() {
634        unsafe {
635            if string::xml_strcmp(value, (*cur).name) == 0 {
636                return 1;
637            }
638            cur = (*cur).next;
639        }
640    }
641
642    // Value not found in enumeration
643    unsafe {
644        let msg = string::xmlstr_to_string(value);
645        let err_msg = format!("Value '{}' is not a valid enumeration value\0", msg);
646        vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
647    }
648    0
649}
650
651// ═══════════════════════════════════════════════════════════════════════════════
652// xmlValidateNotationUse
653// ═══════════════════════════════════════════════════════════════════════════════
654
655/// Validate that `notationName` is a declared notation in the document's DTD.
656///
657/// # UPSTREAM-PARITY
658///
659/// ```c
660/// int xmlValidateNotationUse(xmlValidCtxtPtr ctxt,
661///                            xmlDocPtr doc,
662///                            const xmlChar *notationName);
663/// ```
664///
665/// Returns 1 if the notation is declared, 0 otherwise.
666///
667/// # SAFETY
668///
669/// - `ctxt`, `doc`, `notationName` may be NULL.
670pub unsafe fn validate_notation_use(
671    ctxt: *mut _xmlValidCtxt,
672    doc: *mut _xmlDoc,
673    notation_name: *const xmlChar,
674) -> c_int {
675    if notation_name.is_null() {
676        return 0;
677    }
678
679    let dtd = unsafe { get_valid_dtd(doc) };
680    if dtd.is_null() {
681        unsafe {
682            vctxt_error(
683                ctxt,
684                b"No DTD available for notation validation\0" as *const u8 as *const c_char,
685            );
686        }
687        return 0;
688    }
689
690    // Look up the notation in the DTD's notation hash table
691    unsafe {
692        let notations = (*dtd).notations;
693        if notations.is_null() {
694            vctxt_error(
695                ctxt,
696                b"No notations declared in DTD\0" as *const u8 as *const c_char,
697            );
698            return 0;
699        }
700
701        let notation = hash::hash_lookup(notations as *mut hash::HashTable, notation_name);
702        if notation.is_null() {
703            let msg = string::xmlstr_to_string(notation_name);
704            let err_msg = format!("Notation '{}' is not declared\0", msg);
705            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
706            return 0;
707        }
708    }
709
710    1
711}
712
713// ═══════════════════════════════════════════════════════════════════════════════
714// xmlValidateID / xmlValidateIDRef / xmlValidateIDRefs
715// ═══════════════════════════════════════════════════════════════════════════════
716
717/// Validate an ID value: check that the value is a valid XML Name and
718/// that no duplicate ID values exist in the document.
719///
720/// # UPSTREAM-PARITY
721///
722/// ```c
723/// int xmlValidateID(xmlValidCtxtPtr ctxt,
724///                   xmlDocPtr doc,
725///                   xmlNodePtr node,
726///                   const xmlChar *value);
727/// ```
728///
729/// Returns 1 if the ID is valid, 0 otherwise.
730///
731/// # SAFETY
732///
733/// - `ctxt`, `doc`, `node`, `value` may be NULL.
734pub unsafe fn validate_id(
735    ctxt: *mut _xmlValidCtxt,
736    doc: *mut _xmlDoc,
737    node: *mut _xmlNode,
738    value: *const xmlChar,
739) -> c_int {
740    if value.is_null() || doc.is_null() {
741        return 0;
742    }
743
744    // First, check that the value is a valid XML Name
745    // UPSTREAM-PARITY: xmlValidateID uses xmlValidateNameValue semantics.
746    if unsafe { validate_name_value(value) } == 0 {
747        unsafe {
748            let msg = string::xmlstr_to_string(value);
749            let err_msg = format!("ID value '{}' is not a valid XML Name\0", msg);
750            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
751        }
752        return 0;
753    }
754
755    // Check for duplicate ID in the document's ID hash table
756    unsafe {
757        let doc_ref = &*doc;
758        if !doc_ref.ids.is_null() {
759            let existing = hash::hash_lookup(doc_ref.ids as *mut hash::HashTable, value);
760            if !existing.is_null() {
761                let msg = string::xmlstr_to_string(value);
762                let err_msg = format!("Duplicate ID value '{}'\0", msg);
763                vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
764                return 0;
765            }
766        }
767    }
768
769    // Register the ID in the document's ID hash table
770    unsafe {
771        if (*doc).ids.is_null() {
772            (*doc).ids = hash::hash_create(16) as *mut c_void;
773        }
774        hash::hash_add_entry(
775            (*doc).ids as *mut hash::HashTable,
776            value,
777            node as *mut c_void,
778        );
779    }
780
781    1
782}
783
784/// Validate an IDREF value: check that the referenced ID exists in the document.
785///
786/// # UPSTREAM-PARITY
787///
788/// ```c
789/// int xmlValidateIDRef(xmlValidCtxtPtr ctxt,
790///                      xmlDocPtr doc,
791///                      xmlNodePtr node,
792///                      const xmlChar *value);
793/// ```
794///
795/// Returns 1 if the IDREF is valid (references a known ID), 0 otherwise.
796///
797/// # SAFETY
798///
799/// - `ctxt`, `doc`, `node`, `value` may be NULL.
800pub unsafe fn validate_id_ref(
801    ctxt: *mut _xmlValidCtxt,
802    doc: *mut _xmlDoc,
803    _node: *mut _xmlNode,
804    value: *const xmlChar,
805) -> c_int {
806    if value.is_null() || doc.is_null() {
807        return 0;
808    }
809
810    // Check that the value is a valid XML Name
811    // UPSTREAM-PARITY: xmlValidateIDRef uses xmlValidateNameValue semantics.
812    if unsafe { validate_name_value(value) } == 0 {
813        unsafe {
814            let msg = string::xmlstr_to_string(value);
815            let err_msg = format!("IDREF value '{}' is not a valid XML Name\0", msg);
816            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
817        }
818        return 0;
819    }
820
821    // Check if the referenced ID exists
822    unsafe {
823        let doc_ref = &*doc;
824        if doc_ref.ids.is_null()
825            || hash::hash_lookup(doc_ref.ids as *mut hash::HashTable, value).is_null()
826        {
827            // UPSTREAM-PARITY: Forward references are allowed during
828            // validation but are reported as warnings. The final check
829            // happens in xmlValidateDocumentFinal.
830            let msg = string::xmlstr_to_string(value);
831            let err_msg = format!("IDREF '{}' references an unknown ID\0", msg);
832            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
833            return 0;
834        }
835    }
836
837    1
838}
839
840/// Validate IDREFS (whitespace-separated list of IDREF values).
841///
842/// # UPSTREAM-PARITY
843///
844/// ```c
845/// int xmlValidateIDRefs(xmlValidCtxtPtr ctxt,
846///                       xmlDocPtr doc,
847///                       xmlNodePtr node,
848///                       const xmlChar *value);
849/// ```
850///
851/// Returns 1 if all IDREFs are valid, 0 otherwise.
852///
853/// # SAFETY
854///
855/// - `ctxt`, `doc`, `node`, `value` may be NULL.
856pub unsafe fn validate_id_refs(
857    ctxt: *mut _xmlValidCtxt,
858    doc: *mut _xmlDoc,
859    node: *mut _xmlNode,
860    value: *const xmlChar,
861) -> c_int {
862    if value.is_null() || doc.is_null() {
863        return 0;
864    }
865
866    let s = unsafe { string::xmlstr_to_bytes(value) };
867    let s = core::str::from_utf8(s).unwrap_or("");
868
869    if s.is_empty() {
870        return 0;
871    }
872
873    let mut valid = 1;
874    for token in s.split_whitespace() {
875        if token.is_empty() {
876            continue;
877        }
878        // Create a null-terminated xmlChar string for each token
879        let token_ptr = unsafe { string::bytes_to_xmlstr(token.as_bytes()) };
880        if token_ptr.is_null() {
881            valid = 0;
882            break;
883        }
884        let result = unsafe { validate_id_ref(ctxt, doc, node, token_ptr) };
885        unsafe {
886            allocator::xmlFreeImpl(token_ptr as *mut c_void);
887        }
888        if result == 0 {
889            valid = 0;
890        }
891    }
892
893    valid
894}
895
896// ═══════════════════════════════════════════════════════════════════════════════
897// xmlValidateAttributeDecl
898// ═══════════════════════════════════════════════════════════════════════════════
899
900/// Validate an attribute's value against its declaration.
901///
902/// # UPSTREAM-PARITY
903///
904/// ```c
905/// int xmlValidateAttributeDecl(xmlValidCtxtPtr ctxt,
906///                              xmlDocPtr doc,
907///                              xmlNodePtr elem,
908///                              xmlAttributePtr attr);
909/// ```
910///
911/// Checks:
912/// - Attribute value type (CDATA, ID, IDREF, etc.)
913/// - Enumeration membership
914/// - NOTATION declaration
915/// - Default value validity
916///
917/// Returns 1 if valid, 0 otherwise.
918///
919/// # SAFETY
920///
921/// - `ctxt`, `doc`, `attr` may be NULL.
922pub unsafe fn validate_attribute_decl(
923    ctxt: *mut _xmlValidCtxt,
924    doc: *mut _xmlDoc,
925    attr: *mut _xmlAttribute,
926) -> c_int {
927    if attr.is_null() {
928        return 0;
929    }
930
931    unsafe {
932        let a = &*attr;
933        let atype = a.atype as c_int;
934
935        // Validate the default value if present
936        if !a.defaultValue.is_null() && validate_attribute_value(atype, a.defaultValue) == 0 {
937            let name_str = string::xmlstr_to_string(a.name);
938            let val_str = string::xmlstr_to_string(a.defaultValue);
939            let err_msg = format!(
940                "Default value '{}' for attribute '{}' is not valid for its type\0",
941                val_str, name_str
942            );
943            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
944            return 0;
945        }
946
947        // Validate enumeration values
948        if atype == XML_ATTRIBUTE_ENUMERATION as c_int && !a.tree.is_null() {
949            // Validate each enumeration value is a valid NMTOKEN
950            let mut cur = a.tree;
951            while !cur.is_null() {
952                if !(*cur).name.is_null() && validate_nmtoken_value((*cur).name) == 0 {
953                    let val_str = string::xmlstr_to_string((*cur).name);
954                    let err_msg =
955                        format!("Enumeration value '{}' is not a valid NMTOKEN\0", val_str);
956                    vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
957                    return 0;
958                }
959                cur = (*cur).next;
960            }
961        }
962
963        // Validate NOTATION values reference declared notations
964        if atype == XML_ATTRIBUTE_NOTATION as c_int && !a.tree.is_null() {
965            let mut cur = a.tree;
966            while !cur.is_null() {
967                if !(*cur).name.is_null() && validate_notation_use(ctxt, doc, (*cur).name) == 0 {
968                    let val_str = string::xmlstr_to_string((*cur).name);
969                    let err_msg = format!(
970                        "NOTATION value '{}' references undeclared notation\0",
971                        val_str
972                    );
973                    vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
974                    return 0;
975                }
976                cur = (*cur).next;
977            }
978        }
979
980        1
981    }
982}
983
984// ═══════════════════════════════════════════════════════════════════════════════
985// xmlValidateElement — Core element validation
986// ═══════════════════════════════════════════════════════════════════════════════
987
988/// Validate a single element node against its DTD element and attribute
989/// declarations.
990///
991/// # UPSTREAM-PARITY
992///
993/// ```c
994/// int xmlValidateElement(xmlValidCtxtPtr ctxt,
995///                        xmlDocPtr doc,
996///                        xmlNodePtr elem);
997/// ```
998///
999/// Validates:
1000/// 1. Element declaration exists for the element name
1001/// 2. Content model matches child elements
1002/// 3. Required attributes are present
1003/// 4. Attribute values match their declared types
1004/// 5. ID uniqueness
1005/// 6. IDREF references resolve
1006///
1007/// Returns 1 if valid, 0 otherwise.
1008///
1009/// # SAFETY
1010///
1011/// - `ctxt`, `doc`, `elem` may be NULL.
1012pub unsafe fn validate_element(
1013    ctxt: *mut _xmlValidCtxt,
1014    doc: *mut _xmlDoc,
1015    elem: *mut _xmlNode,
1016) -> c_int {
1017    if elem.is_null() || doc.is_null() || ctxt.is_null() {
1018        return 0;
1019    }
1020
1021    unsafe {
1022        let e = &*elem;
1023
1024        // Skip non-element nodes
1025        if e.type_ != XML_ELEMENT_NODE as c_int {
1026            return 1;
1027        }
1028
1029        // Push node onto stack
1030        if vctxt_push_node(ctxt, elem) != 0 {
1031            return 0;
1032        }
1033
1034        let mut valid = 1;
1035
1036        // Get the DTD
1037        let dtd = get_valid_dtd(doc);
1038        if dtd.is_null() {
1039            // No DTD — no validation to perform
1040            // UPSTREAM-PARITY: libxml2 returns 1 if there's no DTD.
1041            vctxt_pop_node(ctxt);
1042            return 1;
1043        }
1044
1045        let dtd_ref = &*dtd;
1046
1047        // Look up element declaration
1048        let elem_name = e.name;
1049        let elem_decl = if !dtd_ref.elements.is_null() {
1050            hash::hash_lookup(dtd_ref.elements as *mut hash::HashTable, elem_name)
1051        } else {
1052            ptr::null_mut()
1053        };
1054
1055        if elem_decl.is_null() {
1056            let name_str = string::xmlstr_to_string(elem_name);
1057            let err_msg = format!("No declaration for element {}\0", name_str);
1058            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1059            // UPSTREAM-PARITY (valid.c xmlValidateElement): even when an
1060            // element has no declaration, upstream still reports undeclared
1061            // attributes and recurses into children so every descendant
1062            // violation is counted (nokogiri's DTD#validate expects 45 for
1063            // staff.xml: 36 element + 9 attribute errors).
1064            if !dtd_ref.attributes.is_null() {
1065                let mut attr_prop = e.properties;
1066                while !attr_prop.is_null() {
1067                    let a_ref = &*attr_prop;
1068                    if !a_ref.name.is_null() {
1069                        let ad = hash::hash_lookup3(
1070                            dtd_ref.attributes as *mut hash::HashTable,
1071                            a_ref.name,
1072                            ptr::null(),
1073                            elem_name,
1074                        );
1075                        if ad.is_null() {
1076                            let aname_str = string::xmlstr_to_string(a_ref.name);
1077                            let ename_str = string::xmlstr_to_string(elem_name);
1078                            let aerr = format!(
1079                                "No declaration for attribute {} of element {}\0",
1080                                aname_str, ename_str
1081                            );
1082                            vctxt_error(ctxt, aerr.as_ptr() as *const c_char);
1083                        }
1084                    }
1085                    attr_prop = (*attr_prop).next;
1086                }
1087            }
1088            let mut child = e.children;
1089            while !child.is_null() {
1090                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1091                    validate_element(ctxt, doc, child);
1092                }
1093                child = (*child).next;
1094            }
1095            vctxt_pop_node(ctxt);
1096            return 0;
1097        }
1098
1099        let elem_decl_ref = &*(elem_decl as *mut _xmlElement);
1100
1101        // ── Content model validation ──────────────────────────────────────
1102        let elem_type = elem_decl_ref.etype as u32;
1103
1104        if elem_type == XML_ELEMENT_TYPE_EMPTY as u32 {
1105            // Element must have no children (except text nodes)
1106            let mut child = e.children;
1107            while !child.is_null() {
1108                let child_type = (*child).type_ as u32;
1109                if child_type != XML_TEXT_NODE as u32 && child_type != XML_CDATA_SECTION_NODE as u32
1110                {
1111                    valid = 0;
1112                    let name_str = string::xmlstr_to_string(elem_name);
1113                    let err_msg = format!(
1114                        "Element '{}' is declared EMPTY but has child elements\0",
1115                        name_str
1116                    );
1117                    vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1118                    break;
1119                }
1120                child = (*child).next;
1121            }
1122        } else if elem_type == XML_ELEMENT_TYPE_ANY as u32 {
1123            // ANY: any content is allowed
1124        } else if elem_type == XML_ELEMENT_TYPE_MIXED as u32 {
1125            // MIXED: PCDATA plus optionally declared child elements
1126            let mut child = e.children;
1127            while !child.is_null() {
1128                let child_type = (*child).type_ as u32;
1129                if child_type == XML_ELEMENT_NODE as u32 {
1130                    // Validate that child element name is in the mixed content model
1131                    let child_name = (*child).name;
1132                    let result = dtd::valid_content_model(elem_decl_ref.content, &[child_name]);
1133                    if result != dtd::ContentModelResult::Valid {
1134                        let cname_str = string::xmlstr_to_string(child_name);
1135                        let ename_str = string::xmlstr_to_string(elem_name);
1136                        let err_msg = format!(
1137                            "Element '{}' is not allowed in mixed content of '{}'\0",
1138                            cname_str, ename_str
1139                        );
1140                        vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1141                        valid = 0;
1142                    }
1143                }
1144                child = (*child).next;
1145            }
1146        } else if elem_type == XML_ELEMENT_TYPE_ELEMENT as u32 {
1147            // Element-only content: collect child element names and validate
1148            let mut child_names: Vec<*const xmlChar> = Vec::new();
1149            let mut child = e.children;
1150            while !child.is_null() {
1151                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1152                    child_names.push((*child).name);
1153                }
1154                child = (*child).next;
1155            }
1156
1157            let result = dtd::valid_content_model(elem_decl_ref.content, &child_names);
1158            if result != dtd::ContentModelResult::Valid {
1159                let ename_str = string::xmlstr_to_string(elem_name);
1160                let err_msg = format!(
1161                    "Content model validation failed for element '{}'\0",
1162                    ename_str
1163                );
1164                vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1165                valid = 0;
1166            }
1167        }
1168
1169        // ── Attribute validation ──────────────────────────────────────────
1170        if !dtd_ref.attributes.is_null() {
1171            // Walk all attributes on the element node
1172            let mut attr_prop = e.properties;
1173            while !attr_prop.is_null() {
1174                let attr_ref = &*attr_prop;
1175                let attr_name = attr_ref.name;
1176
1177                // Look up the attribute declaration (keyed by name, prefix,
1178                // elem — upstream xmlHashLookup3).
1179                let attr_decl = hash::hash_lookup3(
1180                    dtd_ref.attributes as *mut hash::HashTable,
1181                    attr_name,
1182                    ptr::null(),
1183                    elem_name,
1184                );
1185
1186                if attr_decl.is_null() {
1187                    // UPSTREAM-PARITY (valid.c xmlValidateElement): an
1188                    // attribute with no declaration is reported as an error
1189                    // ("No declaration for attribute %s of element %s") —
1190                    // not skipped.
1191                    let aname_str = string::xmlstr_to_string(attr_name);
1192                    let ename_str = string::xmlstr_to_string(elem_name);
1193                    let err_msg = format!(
1194                        "No declaration for attribute {} of element {}\0",
1195                        aname_str, ename_str
1196                    );
1197                    vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1198                    valid = 0;
1199                    attr_prop = attr_ref.next;
1200                    continue;
1201                }
1202
1203                let attr_decl_ref = &*(attr_decl as *mut _xmlAttribute);
1204                let atype = attr_decl_ref.atype as c_int;
1205
1206                // Get attribute value from content
1207                let attr_value = if !attr_ref.children.is_null() {
1208                    // Get text content of the attribute node
1209                    let text_node = attr_ref.children;
1210                    if (*text_node).type_ == XML_TEXT_NODE as c_int
1211                        || (*text_node).type_ == XML_CDATA_SECTION_NODE as c_int
1212                    {
1213                        (*text_node).content
1214                    } else {
1215                        ptr::null()
1216                    }
1217                } else {
1218                    ptr::null()
1219                };
1220
1221                // Validate the attribute value against its type
1222                if !attr_value.is_null() {
1223                    if atype == XML_ATTRIBUTE_ENUMERATION as c_int && !attr_decl_ref.tree.is_null()
1224                    {
1225                        if validate_enumeration(ctxt, attr_value, attr_decl_ref.tree) == 0 {
1226                            let aname_str = string::xmlstr_to_string(attr_name);
1227                            let aval_str = string::xmlstr_to_string(attr_value);
1228                            let err_msg = format!(
1229                                "Attribute '{}' has value '{}' not in enumeration\0",
1230                                aname_str, aval_str
1231                            );
1232                            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1233                            valid = 0;
1234                        }
1235                    } else if atype == XML_ATTRIBUTE_NOTATION as c_int {
1236                        if validate_notation_use(ctxt, doc, attr_value) == 0 {
1237                            let aname_str = string::xmlstr_to_string(attr_name);
1238                            let aval_str = string::xmlstr_to_string(attr_value);
1239                            let err_msg = format!(
1240                                "Attribute '{}' references undeclared notation '{}'\0",
1241                                aname_str, aval_str
1242                            );
1243                            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1244                            valid = 0;
1245                        }
1246                    } else if validate_attribute_value(atype, attr_value) == 0 {
1247                        let aname_str = string::xmlstr_to_string(attr_name);
1248                        let aval_str = string::xmlstr_to_string(attr_value);
1249                        let err_msg = format!(
1250                            "Attribute '{}' has invalid value '{}' for its type\0",
1251                            aname_str, aval_str
1252                        );
1253                        vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1254                        valid = 0;
1255                    }
1256
1257                    // ID/IDREF specific validation
1258                    if atype == XML_ATTRIBUTE_ID as c_int {
1259                        if validate_id(ctxt, doc, elem, attr_value) == 0 {
1260                            valid = 0;
1261                        }
1262                    } else if atype == XML_ATTRIBUTE_IDREF as c_int {
1263                        if validate_id_ref(ctxt, doc, elem, attr_value) == 0 {
1264                            valid = 0;
1265                        }
1266                    } else if atype == XML_ATTRIBUTE_IDREFS as c_int
1267                        && validate_id_refs(ctxt, doc, elem, attr_value) == 0
1268                    {
1269                        valid = 0;
1270                    }
1271                }
1272
1273                attr_prop = attr_ref.next;
1274            }
1275
1276            // ── Check for required attributes ─────────────────────────────
1277            struct RequiredAttrCheck {
1278                ctxt: *mut _xmlValidCtxt,
1279                elem_name: *const xmlChar,
1280                elem_props: *mut _xmlAttr,
1281                valid: *mut c_int,
1282            }
1283
1284            extern "C" fn check_required_attr(
1285                payload: *mut c_void,
1286                data: *mut c_void,
1287                name: *const xmlChar,
1288                name2: *const xmlChar,
1289                _name3: *const xmlChar,
1290            ) {
1291                if payload.is_null() || data.is_null() || name2.is_null() {
1292                    return;
1293                }
1294
1295                // SAFETY: Called from hash_scan_full.
1296                let check = unsafe { &*(data as *mut RequiredAttrCheck) };
1297                unsafe {
1298                    // Only check attributes belonging to this element
1299                    if string::xml_strcmp(name, check.elem_name) != 0 {
1300                        return;
1301                    }
1302
1303                    let attr_decl = &*(payload as *mut _xmlAttribute);
1304
1305                    // If the attribute is REQUIRED, check if it's present
1306                    if attr_decl.def == XML_ATTRIBUTE_REQUIRED as c_int {
1307                        // Check if this attribute name is in the element's properties
1308                        let mut found = 0;
1309                        let mut prop = check.elem_props;
1310                        while !prop.is_null() {
1311                            if string::xml_strcmp((*prop).name, name2) == 0 {
1312                                found = 1;
1313                                break;
1314                            }
1315                            prop = (*prop).next;
1316                        }
1317
1318                        if found == 0 {
1319                            let aname_str = string::xmlstr_to_string(name2);
1320                            let ename_str = string::xmlstr_to_string(check.elem_name);
1321                            let err_msg = format!(
1322                                "Required attribute '{}' missing on element '{}'\0",
1323                                aname_str, ename_str
1324                            );
1325                            vctxt_error(check.ctxt, err_msg.as_ptr() as *const c_char);
1326                            *(check.valid) = 0;
1327                        }
1328                    }
1329                }
1330            }
1331
1332            let mut required_valid = valid;
1333            let check = RequiredAttrCheck {
1334                ctxt,
1335                elem_name,
1336                elem_props: e.properties,
1337                valid: &mut required_valid,
1338            };
1339
1340            hash::hash_scan_full(
1341                dtd_ref.attributes as *mut hash::HashTable,
1342                Some(check_required_attr),
1343                &check as *const RequiredAttrCheck as *mut c_void,
1344            );
1345
1346            valid = required_valid;
1347        }
1348
1349        // ── Recurse into children ─────────────────────────────────────────
1350        let mut child = e.children;
1351        while !child.is_null() {
1352            if (*child).type_ == XML_ELEMENT_NODE as c_int
1353                && validate_element(ctxt, doc, child) == 0
1354            {
1355                valid = 0;
1356            }
1357            child = (*child).next;
1358        }
1359
1360        vctxt_pop_node(ctxt);
1361        valid
1362    }
1363}
1364
1365// ═══════════════════════════════════════════════════════════════════════════════
1366// xmlValidateDocument
1367// ═══════════════════════════════════════════════════════════════════════════════
1368
1369/// Validate an entire document against its DTD.
1370///
1371/// # UPSTREAM-PARITY
1372///
1373/// ```c
1374/// int xmlValidateDocument(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
1375/// ```
1376///
1377/// Validates the root element and all its descendants, plus the DTD itself.
1378///
1379/// Returns 1 if valid, 0 otherwise.
1380///
1381/// # SAFETY
1382///
1383/// - `ctxt`, `doc` may be NULL.
1384pub unsafe fn validate_document(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
1385    if ctxt.is_null() || doc.is_null() {
1386        return 0;
1387    }
1388
1389    unsafe {
1390        let c = &mut *ctxt;
1391        c.doc = doc;
1392        c.valid = 1;
1393
1394        let d = &*doc;
1395
1396        // UPSTREAM-PARITY: xmlValidateDocumentInternal rejects documents with
1397        // no internal or external subset (valid.c:6266-6271):
1398        //
1399        // ```c
1400        // if ((doc->intSubset == NULL) && (doc->extSubset == NULL)) {
1401        //     xmlErrValid(vctxt, XML_DTD_NO_DTD, "no DTD found!\n", NULL);
1402        //     return(0);
1403        // }
1404        // ```
1405        if d.intSubset.is_null() && d.extSubset.is_null() {
1406            vctxt_error(ctxt, b"no DTD found!\0" as *const u8 as *const c_char);
1407            return 0;
1408        }
1409
1410        // Find the root element (first child that's an element node)
1411        let mut root = d.children;
1412        while !root.is_null() {
1413            if (*root).type_ == XML_ELEMENT_NODE as c_int {
1414                break;
1415            }
1416            root = (*root).next;
1417        }
1418
1419        if root.is_null() {
1420            vctxt_error(
1421                ctxt,
1422                b"No root element found in document\0" as *const u8 as *const c_char,
1423            );
1424            return 0;
1425        }
1426
1427        // Validate the root element
1428        if validate_element(ctxt, doc, root) == 0 {
1429            return 0;
1430        }
1431
1432        c.valid
1433    }
1434}
1435
1436// ═══════════════════════════════════════════════════════════════════════════════
1437// xmlValidateDocumentFinal
1438// ═══════════════════════════════════════════════════════════════════════════════
1439
1440/// Final validation: check that all IDREFs resolve to existing IDs.
1441///
1442/// # UPSTREAM-PARITY
1443///
1444/// ```c
1445/// int xmlValidateDocumentFinal(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
1446/// ```
1447///
1448/// This is called after the document is fully parsed, to verify ID/IDREF
1449/// consistency. During parsing, forward IDREFs may not be resolvable, so
1450/// this final pass checks them.
1451///
1452/// Returns 1 if all IDREFs resolve, 0 otherwise.
1453///
1454/// # SAFETY
1455///
1456/// - `ctxt`, `doc` may be NULL.
1457pub unsafe fn validate_document_final(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
1458    if ctxt.is_null() || doc.is_null() {
1459        return 0;
1460    }
1461
1462    unsafe {
1463        let c = &mut *ctxt;
1464        c.doc = doc;
1465
1466        let d = &*doc;
1467
1468        // If there's no refs table, no IDREFs were found
1469        if d.refs.is_null() {
1470            return c.valid;
1471        }
1472
1473        // Check each IDREF against the IDs table
1474        struct IdRefCheckContext {
1475            ctxt: *mut _xmlValidCtxt,
1476            doc: *mut _xmlDoc,
1477        }
1478
1479        extern "C" fn check_idref(
1480            _payload: *mut c_void,
1481            data: *mut c_void,
1482            _name: *const xmlChar,
1483            name2: *const xmlChar,
1484            _name3: *const xmlChar,
1485        ) {
1486            if data.is_null() || name2.is_null() {
1487                return;
1488            }
1489
1490            // SAFETY: Called from hash_scan_full.
1491            let cx = unsafe { &*(data as *mut IdRefCheckContext) };
1492            unsafe {
1493                let doc_ref = &*cx.doc;
1494
1495                // Look up the IDREF value in the IDs table
1496                if doc_ref.ids.is_null()
1497                    || hash::hash_lookup(doc_ref.ids as *mut hash::HashTable, name2).is_null()
1498                {
1499                    let ref_str = string::xmlstr_to_string(name2);
1500                    let err_msg = format!("IDREF '{}' does not reference a declared ID\0", ref_str);
1501                    vctxt_error(cx.ctxt, err_msg.as_ptr() as *const c_char);
1502                }
1503            }
1504        }
1505
1506        let ctx = IdRefCheckContext { ctxt, doc };
1507        hash::hash_scan_full(
1508            d.refs as *mut hash::HashTable,
1509            Some(check_idref),
1510            &ctx as *const IdRefCheckContext as *mut c_void,
1511        );
1512
1513        c.valid
1514    }
1515}
1516
1517// ═══════════════════════════════════════════════════════════════════════════════
1518// xmlValidateRoot
1519// ═══════════════════════════════════════════════════════════════════════════════
1520
1521/// Validate the root element of a document.
1522///
1523/// # UPSTREAM-PARITY
1524///
1525/// ```c
1526/// int xmlValidateRoot(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
1527/// ```
1528///
1529/// Returns 1 if the root element is valid, 0 otherwise.
1530///
1531/// # SAFETY
1532///
1533/// - `ctxt`, `doc` may be NULL.
1534pub unsafe fn validate_root(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
1535    if ctxt.is_null() || doc.is_null() {
1536        return 0;
1537    }
1538
1539    unsafe {
1540        let c = &mut *ctxt;
1541        c.doc = doc;
1542        c.valid = 1;
1543
1544        let d = &*doc;
1545
1546        // Find root element
1547        let mut root = d.children;
1548        while !root.is_null() {
1549            if (*root).type_ == XML_ELEMENT_NODE as c_int {
1550                break;
1551            }
1552            root = (*root).next;
1553        }
1554
1555        if root.is_null() {
1556            vctxt_error(
1557                ctxt,
1558                b"No root element found\0" as *const u8 as *const c_char,
1559            );
1560            return 0;
1561        }
1562
1563        // Get the DTD
1564        let dtd = get_valid_dtd(doc);
1565        if dtd.is_null() {
1566            // No DTD — nothing to validate against
1567            return 1;
1568        }
1569
1570        // UPSTREAM-PARITY: libxml2 checks that the root element name matches
1571        // the DTD's name (the DOCTYPE name).
1572        let dtd_ref = &*dtd;
1573        if !dtd_ref.name.is_null() && string::xml_strcmp((*root).name, dtd_ref.name) != 0 {
1574            let root_str = string::xmlstr_to_string((*root).name);
1575            let dtd_str = string::xmlstr_to_string(dtd_ref.name);
1576            let err_msg = format!(
1577                "Root element '{}' does not match DTD root '{}'\0",
1578                root_str, dtd_str
1579            );
1580            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1581            return 0;
1582        }
1583
1584        c.valid
1585    }
1586}
1587
1588// ═══════════════════════════════════════════════════════════════════════════════
1589// xmlValidateContent
1590// ═══════════════════════════════════════════════════════════════════════════════
1591
1592/// Validate the content of an element node against its content model.
1593///
1594/// # UPSTREAM-PARITY
1595///
1596/// ```c
1597/// int xmlValidateContent(xmlValidCtxtPtr ctxt,
1598///                        xmlNodePtr node,
1599///                        xmlDocPtr doc);
1600/// ```
1601///
1602/// Returns 1 if content is valid, 0 otherwise.
1603///
1604/// # SAFETY
1605///
1606/// - `ctxt`, `node`, `doc` may be NULL.
1607pub unsafe fn validate_content(
1608    ctxt: *mut _xmlValidCtxt,
1609    node: *mut _xmlNode,
1610    doc: *mut _xmlDoc,
1611) -> c_int {
1612    if node.is_null() || doc.is_null() || ctxt.is_null() {
1613        return 0;
1614    }
1615
1616    unsafe {
1617        let n = &*node;
1618        if n.type_ != XML_ELEMENT_NODE as c_int {
1619            return 1;
1620        }
1621
1622        let dtd = get_valid_dtd(doc);
1623        if dtd.is_null() {
1624            return 1;
1625        }
1626
1627        let dtd_ref = &*dtd;
1628        if dtd_ref.elements.is_null() {
1629            return 1;
1630        }
1631
1632        let elem_decl = hash::hash_lookup(dtd_ref.elements as *mut hash::HashTable, n.name);
1633        if elem_decl.is_null() {
1634            return 1;
1635        }
1636
1637        let elem_decl_ref = &*(elem_decl as *mut _xmlElement);
1638        if elem_decl_ref.content.is_null() {
1639            return 1;
1640        }
1641
1642        let elem_type = elem_decl_ref.etype as u32;
1643        if elem_type == XML_ELEMENT_TYPE_EMPTY as u32 {
1644            // Check no element children
1645            let mut child = n.children;
1646            while !child.is_null() {
1647                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1648                    let name_str = string::xmlstr_to_string(n.name);
1649                    let err_msg =
1650                        format!("Element '{}' is EMPTY but has child elements\0", name_str);
1651                    vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1652                    return 0;
1653                }
1654                child = (*child).next;
1655            }
1656            return 1;
1657        }
1658
1659        if elem_type == XML_ELEMENT_TYPE_ANY as u32 {
1660            return 1;
1661        }
1662
1663        // Collect child element names
1664        let mut child_names: Vec<*const xmlChar> = Vec::new();
1665        let mut child = n.children;
1666        while !child.is_null() {
1667            if (*child).type_ == XML_ELEMENT_NODE as c_int {
1668                child_names.push((*child).name);
1669            }
1670            child = (*child).next;
1671        }
1672
1673        let result = dtd::valid_content_model(elem_decl_ref.content, &child_names);
1674        if result != dtd::ContentModelResult::Valid {
1675            let name_str = string::xmlstr_to_string(n.name);
1676            let err_msg = format!(
1677                "Content model validation failed for element '{}'\0",
1678                name_str
1679            );
1680            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1681            0
1682        } else {
1683            1
1684        }
1685    }
1686}
1687
1688// ═══════════════════════════════════════════════════════════════════════════════
1689// xmlIsMixedElement / xmlIsEmptyElement
1690// ═══════════════════════════════════════════════════════════════════════════════
1691
1692/// Check if an element has a mixed content model.
1693///
1694/// # UPSTREAM-PARITY
1695///
1696/// ```c
1697/// int xmlIsMixedElement(xmlDocPtr doc, const xmlChar *name);
1698/// ```
1699///
1700/// Returns 1 if the element is declared as mixed content, 0 otherwise.
1701///
1702/// # SAFETY
1703///
1704/// - `doc`, `name` may be NULL.
1705pub unsafe fn is_mixed_element(doc: *mut _xmlDoc, name: *const xmlChar) -> c_int {
1706    if doc.is_null() || name.is_null() {
1707        return 0;
1708    }
1709
1710    let dtd = unsafe { get_valid_dtd(doc) };
1711    if dtd.is_null() {
1712        return 0;
1713    }
1714
1715    unsafe {
1716        let dtd_ref = &*dtd;
1717        if dtd_ref.elements.is_null() {
1718            return 0;
1719        }
1720
1721        let elem_decl = hash::hash_lookup(dtd_ref.elements as *mut hash::HashTable, name);
1722        if elem_decl.is_null() {
1723            return 0;
1724        }
1725
1726        let elem_decl_ref = &*(elem_decl as *mut _xmlElement);
1727        ((elem_decl_ref.etype as u32) == XML_ELEMENT_TYPE_MIXED as u32) as c_int
1728    }
1729}
1730
1731/// Check if an element is declared as EMPTY.
1732///
1733/// # UPSTREAM-PARITY
1734///
1735/// ```c
1736/// int xmlIsEmptyElement(xmlDocPtr doc, const xmlChar *name);
1737/// ```
1738///
1739/// Returns 1 if the element is declared EMPTY, 0 otherwise.
1740///
1741/// # SAFETY
1742///
1743/// - `doc`, `name` may be NULL.
1744pub unsafe fn is_empty_element(doc: *mut _xmlDoc, name: *const xmlChar) -> c_int {
1745    if doc.is_null() || name.is_null() {
1746        return 0;
1747    }
1748
1749    let dtd = unsafe { get_valid_dtd(doc) };
1750    if dtd.is_null() {
1751        return 0;
1752    }
1753
1754    unsafe {
1755        let dtd_ref = &*dtd;
1756        if dtd_ref.elements.is_null() {
1757            return 0;
1758        }
1759
1760        let elem_decl = hash::hash_lookup(dtd_ref.elements as *mut hash::HashTable, name);
1761        if elem_decl.is_null() {
1762            return 0;
1763        }
1764
1765        let elem_decl_ref = &*(elem_decl as *mut _xmlElement);
1766        ((elem_decl_ref.etype as u32) == XML_ELEMENT_TYPE_EMPTY as u32) as c_int
1767    }
1768}
1769
1770// ═══════════════════════════════════════════════════════════════════════════════
1771// xmlValidateDtd
1772// ═══════════════════════════════════════════════════════════════════════════════
1773
1774/// Validate a DTD's declarations (element/attribute declarations).
1775///
1776/// # UPSTREAM-PARITY
1777///
1778/// ```c
1779/// int xmlValidateDtd(xmlValidCtxtPtr ctxt,
1780///                    xmlDocPtr doc,
1781///                    xmlDtdPtr dtd);
1782/// ```
1783///
1784/// Validates:
1785/// - Attribute declarations (default values, enumeration values, notation refs)
1786/// - Element content models reference only declared elements
1787///
1788/// Returns 1 if the DTD is valid, 0 otherwise.
1789///
1790/// # SAFETY
1791///
1792/// - `ctxt`, `doc`, `dtd` may be NULL.
1793pub unsafe fn validate_dtd(
1794    ctxt: *mut _xmlValidCtxt,
1795    doc: *mut _xmlDoc,
1796    dtd: *mut _xmlDtd,
1797) -> c_int {
1798    if ctxt.is_null() || dtd.is_null() {
1799        return 0;
1800    }
1801
1802    let c = unsafe { &mut *ctxt };
1803    c.doc = doc;
1804    c.valid = 1;
1805
1806    struct ValidateDtdCtx {
1807        ctxt: *mut _xmlValidCtxt,
1808        doc: *mut _xmlDoc,
1809    }
1810
1811    extern "C" fn validate_attr_decl_cb(
1812        payload: *mut c_void,
1813        data: *mut c_void,
1814        _name: *const xmlChar,
1815        _name2: *const xmlChar,
1816        _name3: *const xmlChar,
1817    ) {
1818        if payload.is_null() || data.is_null() {
1819            return;
1820        }
1821
1822        // SAFETY: Called from hash_scan_full with a ValidateDtdCtx as data.
1823        let ctx = unsafe { &*(data as *mut ValidateDtdCtx) };
1824        unsafe {
1825            let attr = payload as *mut _xmlAttribute;
1826            validate_attribute_decl(ctx.ctxt, ctx.doc, attr);
1827        }
1828    }
1829
1830    extern "C" fn validate_elem_content_cb(
1831        payload: *mut c_void,
1832        data: *mut c_void,
1833        _name: *const xmlChar,
1834        _name2: *const xmlChar,
1835        _name3: *const xmlChar,
1836    ) {
1837        if payload.is_null() || data.is_null() {
1838            return;
1839        }
1840
1841        // SAFETY: Called from hash_scan_full with a ValidateDtdCtx as data.
1842        let ctx = unsafe { &*(data as *mut ValidateDtdCtx) };
1843        unsafe {
1844            let elem = &*(payload as *mut _xmlElement);
1845            if !elem.content.is_null() {
1846                validate_content_model_refs(ctx.ctxt, ctx.doc, elem.content);
1847            }
1848        }
1849    }
1850
1851    unsafe {
1852        let dtd_ref = &*dtd;
1853
1854        // Validate all attribute declarations
1855        if !dtd_ref.attributes.is_null() {
1856            let ctx = ValidateDtdCtx { ctxt, doc };
1857            hash::hash_scan_full(
1858                dtd_ref.attributes as *mut hash::HashTable,
1859                Some(validate_attr_decl_cb),
1860                &ctx as *const ValidateDtdCtx as *mut c_void,
1861            );
1862        }
1863
1864        // Validate that element content models reference declared elements
1865        if !dtd_ref.elements.is_null() {
1866            let ctx = ValidateDtdCtx { ctxt, doc };
1867            hash::hash_scan_full(
1868                dtd_ref.elements as *mut hash::HashTable,
1869                Some(validate_elem_content_cb),
1870                &ctx as *const ValidateDtdCtx as *mut c_void,
1871            );
1872        }
1873
1874        // UPSTREAM-PARITY (valid.c xmlValidateDtd): beyond DTD-internal
1875        // consistency, xmlValidateDtd walks every element of the document and
1876        // validates it against the DTD (content model, attributes, required
1877        // attributes, ID/IDREF). The doc is passed explicitly so an external
1878        // subset can validate a document it wasn't loaded from.
1879        if !doc.is_null() {
1880            let d = &*doc;
1881            let mut root = d.children;
1882            while !root.is_null() {
1883                if (*root).type_ == XML_ELEMENT_NODE as c_int {
1884                    break;
1885                }
1886                root = (*root).next;
1887            }
1888            if !root.is_null() {
1889                // validate_element recurses; a 0 return marks the doc invalid
1890                // but all errors are still reported via vctxt_error handlers.
1891                let el_valid = validate_element(ctxt, doc, root);
1892                if el_valid == 0 {
1893                    c.valid = 0;
1894                }
1895            }
1896        }
1897
1898        c.valid
1899    }
1900}
1901
1902/// Recursively check that all element references in a content model
1903/// reference declared elements.
1904///
1905/// # SAFETY
1906///
1907/// - `ctxt`, `doc`, `content` may be NULL.
1908unsafe fn validate_content_model_refs(
1909    ctxt: *mut _xmlValidCtxt,
1910    doc: *mut _xmlDoc,
1911    content: *mut _xmlElementContent,
1912) {
1913    if content.is_null() {
1914        return;
1915    }
1916
1917    unsafe {
1918        let c = &*content;
1919
1920        match c.type_ as u32 {
1921            t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
1922                // Check that the element name is declared
1923                if !c.name.is_null() {
1924                    let dtd = get_valid_dtd(doc);
1925                    if !dtd.is_null() {
1926                        let dtd_ref = &*dtd;
1927                        if !dtd_ref.elements.is_null() {
1928                            let decl =
1929                                hash::hash_lookup(dtd_ref.elements as *mut hash::HashTable, c.name);
1930                            if decl.is_null() {
1931                                let name_str = string::xmlstr_to_string(c.name);
1932                                let err_msg = format!(
1933                                    "Element '{}' referenced in content model is not declared\0",
1934                                    name_str
1935                                );
1936                                vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1937                            }
1938                        }
1939                    }
1940                }
1941            }
1942            t if t == XML_ELEMENT_CONTENT_SEQ as u32 || t == XML_ELEMENT_CONTENT_OR as u32 => {
1943                validate_content_model_refs(ctxt, doc, c.c1);
1944                validate_content_model_refs(ctxt, doc, c.c2);
1945            }
1946            _ => {}
1947        }
1948    }
1949}
1950
1951// ═══════════════════════════════════════════════════════════════════════════════
1952// xmlValidateDtdFinal
1953// ═══════════════════════════════════════════════════════════════════════════════
1954
1955/// Final DTD validation — checks ID/IDREF consistency.
1956///
1957/// # UPSTREAM-PARITY
1958///
1959/// ```c
1960/// int xmlValidateDtdFinal(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
1961/// ```
1962///
1963/// This is equivalent to `xmlValidateDocumentFinal` and checks that all
1964/// IDREF values resolve to declared IDs.
1965///
1966/// Returns 1 if valid, 0 otherwise.
1967///
1968/// # SAFETY
1969///
1970/// - `ctxt`, `doc` may be NULL.
1971pub unsafe fn validate_dtd_final(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
1972    unsafe { validate_document_final(ctxt, doc) }
1973}
1974
1975// ═══════════════════════════════════════════════════════════════════════════════
1976// 11.1-I validation surface closure
1977// ═══════════════════════════════════════════════════════════════════════════════
1978//
1979// Closes the missing xmlValidate* exports against the oracle (system libxml2
1980// 2.15.3): the modern 2-arg name validators (xmlValidateNCName/QName/Name/
1981// NMToken), the 1-arg *Value family (xmlValidateNameValue/NamesValue/
1982// NmtokenValue/NmtokensValue), the declaration validators (ElementDecl /
1983// NotationDecl / OneAttribute / OneElement / OneNamespace), the streaming
1984// push family (xmlValidatePushElement/PushCData/PopElement +
1985// xmlValidBuildContentModel), and the ID/REF table machinery they depend on
1986// (xmlAddID/xmlAddRef/xmlRemoveID/xmlRemoveRef).
1987//
1988// UPSTREAM-PARITY notes:
1989// - The modern 2-arg validators return -1 on NULL, 0 if valid, 1 if invalid.
1990// - The 1-arg *Value validators return 1 if valid, 0 otherwise (NULL too).
1991// - Names/Nmtokens separators are exactly 0x20 (upstream erratum E20: no
1992//   other whitespace is accepted).
1993// - The char classes are the XML 1.0 Fifth-Edition productions including the
1994//   supplementary plane 0x10000..0xEFFFF (upstream xmlIsNameStartCharNew /
1995//   xmlIsNameCharNew in parser.c).
1996
1997// XML_SCAN_* flags (upstream parser.c xmlScanName)
1998const XML_SCAN_NC: u32 = 1; // stop at ':'
1999const XML_SCAN_NMTOKEN: u32 = 2; // first char may be any NameChar
2000
2001/// Upstream IS_BLANK_CH: space, tab, LF, CR.
2002const fn is_blank_byte(b: u8) -> bool {
2003    b == b' ' || b == b'\t' || b == b'\n' || b == b'\r'
2004}
2005
2006/// UTF-8 sequence length from the lead byte (0 when invalid).
2007const fn utf8_char_len(lead: u8) -> usize {
2008    if lead < 0x80 {
2009        1
2010    } else if lead >= 0xC0 && lead <= 0xDF {
2011        2
2012    } else if lead >= 0xE0 && lead <= 0xEF {
2013        3
2014    } else if lead >= 0xF0 && lead <= 0xF7 {
2015        4
2016    } else {
2017        0
2018    }
2019}
2020
2021/// Byte-level scan mirroring upstream `xmlScanName(ptr, SIZE_MAX, flags)`
2022/// (parser.c): consumes a Name (or NCName / Nmtoken) starting at `start`.
2023///
2024/// Semantics preserved:
2025/// - NC mode stops (without consuming) at ':'.
2026/// - The first character must be a NameStartChar unless XML_SCAN_NMTOKEN;
2027///   every later character must be a NameChar.
2028/// - Invalid UTF-8 stops the scan (upstream xmlGetUTF8Char < 0).
2029/// - With SIZE_MAX the length bound never triggers.
2030///
2031/// Returns the offset of the first byte past the name; equals `start` when
2032/// nothing was consumed.
2033unsafe fn scan_name_offsets(bytes: &[u8], start: usize, flags: u32) -> usize {
2034    let stop = if flags & XML_SCAN_NC != 0 {
2035        Some(b':')
2036    } else {
2037        None
2038    };
2039    let mut i = start;
2040    let mut is_nmtoken = flags & XML_SCAN_NMTOKEN != 0;
2041    while i < bytes.len() {
2042        let b = bytes[i];
2043        if b < 0x80 {
2044            if stop == Some(b) {
2045                break;
2046            }
2047            let c = b as char;
2048            let ok = if is_nmtoken {
2049                is_xml_name_char(c)
2050            } else {
2051                is_xml_name_start(c)
2052            };
2053            if !ok {
2054                break;
2055            }
2056            i += 1;
2057        } else {
2058            let len = utf8_char_len(b);
2059            if len == 0 || i + len > bytes.len() {
2060                break;
2061            }
2062            let ch = match core::str::from_utf8(&bytes[i..i + len])
2063                .ok()
2064                .and_then(|s| s.chars().next())
2065            {
2066                Some(c) => c,
2067                None => break,
2068            };
2069            let ok = if is_nmtoken {
2070                is_xml_name_char(ch)
2071            } else {
2072                is_xml_name_start(ch)
2073            };
2074            if !ok {
2075                break;
2076            }
2077            i += len;
2078        }
2079        // subsequent characters use the NameChar production
2080        is_nmtoken = true;
2081    }
2082    i
2083}
2084
2085/// Modern 2-arg form, upstream tree.c `xmlValidateNCName(value, space)`.
2086///
2087/// # SAFETY
2088///
2089/// - `value` must be a valid null-terminated string or NULL.
2090pub unsafe fn validate_ncname(value: *const xmlChar, space: c_int) -> c_int {
2091    if value.is_null() {
2092        return -1;
2093    }
2094    let bytes = string::xmlstr_to_bytes(value);
2095    let mut start = 0usize;
2096    if space != 0 {
2097        while start < bytes.len() && is_blank_byte(bytes[start]) {
2098            start += 1;
2099        }
2100    }
2101    let end = scan_name_offsets(bytes, start, XML_SCAN_NC);
2102    if end == start {
2103        return 1;
2104    }
2105    let mut end2 = end;
2106    if space != 0 {
2107        while end2 < bytes.len() && is_blank_byte(bytes[end2]) {
2108            end2 += 1;
2109        }
2110    }
2111    if end2 == bytes.len() {
2112        0
2113    } else {
2114        1
2115    }
2116}
2117
2118/// Modern 2-arg form, upstream tree.c `xmlValidateQName(value, space)`.
2119///
2120/// # SAFETY
2121///
2122/// - `value` must be a valid null-terminated string or NULL.
2123pub unsafe fn validate_qname(value: *const xmlChar, space: c_int) -> c_int {
2124    if value.is_null() {
2125        return -1;
2126    }
2127    let bytes = string::xmlstr_to_bytes(value);
2128    let mut start = 0usize;
2129    if space != 0 {
2130        while start < bytes.len() && is_blank_byte(bytes[start]) {
2131            start += 1;
2132        }
2133    }
2134    let mut end = scan_name_offsets(bytes, start, XML_SCAN_NC);
2135    if end == start {
2136        return 1;
2137    }
2138    if end < bytes.len() && bytes[end] == b':' {
2139        end += 1;
2140        let end2 = scan_name_offsets(bytes, end, XML_SCAN_NC);
2141        if end2 == end {
2142            return 1;
2143        }
2144        end = end2;
2145    }
2146    if space != 0 {
2147        while end < bytes.len() && is_blank_byte(bytes[end]) {
2148            end += 1;
2149        }
2150    }
2151    if end == bytes.len() {
2152        0
2153    } else {
2154        1
2155    }
2156}
2157
2158/// Modern 2-arg form, upstream tree.c `xmlValidateName(value, space)`.
2159///
2160/// NOTE: this is the CURRENT oracle ABI — since libxml2 2.12 the symbol
2161/// carries a second `int space` parameter (tree.c) and inverted return
2162/// semantics (0 valid / 1 invalid / -1 NULL). The pre-2.12 1-arg form is
2163/// gone from the DSO; the 1-arg semantics live on as xmlValidateNameValue.
2164///
2165/// # SAFETY
2166///
2167/// - `value` must be a valid null-terminated string or NULL.
2168pub unsafe fn validate_name_space(value: *const xmlChar, space: c_int) -> c_int {
2169    if value.is_null() {
2170        return -1;
2171    }
2172    let bytes = string::xmlstr_to_bytes(value);
2173    let mut start = 0usize;
2174    if space != 0 {
2175        while start < bytes.len() && is_blank_byte(bytes[start]) {
2176            start += 1;
2177        }
2178    }
2179    let end = scan_name_offsets(bytes, start, 0);
2180    if end == start {
2181        return 1;
2182    }
2183    let mut end2 = end;
2184    if space != 0 {
2185        while end2 < bytes.len() && is_blank_byte(bytes[end2]) {
2186            end2 += 1;
2187        }
2188    }
2189    if end2 == bytes.len() {
2190        0
2191    } else {
2192        1
2193    }
2194}
2195
2196/// Modern 2-arg form, upstream tree.c `xmlValidateNMToken(value, space)`.
2197///
2198/// # SAFETY
2199///
2200/// - `value` must be a valid null-terminated string or NULL.
2201pub unsafe fn validate_nmtoken_space(value: *const xmlChar, space: c_int) -> c_int {
2202    if value.is_null() {
2203        return -1;
2204    }
2205    let bytes = string::xmlstr_to_bytes(value);
2206    let mut start = 0usize;
2207    if space != 0 {
2208        while start < bytes.len() && is_blank_byte(bytes[start]) {
2209            start += 1;
2210        }
2211    }
2212    let end = scan_name_offsets(bytes, start, XML_SCAN_NMTOKEN);
2213    if end == start {
2214        return 1;
2215    }
2216    let mut end2 = end;
2217    if space != 0 {
2218        while end2 < bytes.len() && is_blank_byte(bytes[end2]) {
2219            end2 += 1;
2220        }
2221    }
2222    if end2 == bytes.len() {
2223        0
2224    } else {
2225        1
2226    }
2227}
2228
2229/// 1-arg form, upstream valid.c `xmlValidate*ValueInternal(value, flags)`.
2230/// Returns 1 if valid, 0 if not (including NULL / empty).
2231unsafe fn validate_value_internal(value: *const xmlChar, flags: u32) -> c_int {
2232    if value.is_null() {
2233        return 0;
2234    }
2235    let bytes = string::xmlstr_to_bytes(value);
2236    if bytes.is_empty() {
2237        return 0;
2238    }
2239    let end = scan_name_offsets(bytes, 0, flags);
2240    if end == 0 {
2241        return 0;
2242    }
2243    if end == bytes.len() {
2244        1
2245    } else {
2246        0
2247    }
2248}
2249
2250/// 1-arg Names/Nmtokens list form. Separator is exactly 0x20 — upstream
2251/// valid.c deliberately does NOT use IS_BLANK here (XML erratum E20).
2252unsafe fn validate_values_internal(value: *const xmlChar, flags: u32) -> c_int {
2253    if value.is_null() {
2254        return 0;
2255    }
2256    let bytes = string::xmlstr_to_bytes(value);
2257    let mut cur = scan_name_offsets(bytes, 0, flags);
2258    if cur == 0 {
2259        return 0;
2260    }
2261    while cur < bytes.len() && bytes[cur] == b' ' {
2262        while cur < bytes.len() && bytes[cur] == b' ' {
2263            cur += 1;
2264        }
2265        let end = scan_name_offsets(bytes, cur, flags);
2266        if end == cur {
2267            return 0;
2268        }
2269        cur = end;
2270    }
2271    if cur == bytes.len() {
2272        1
2273    } else {
2274        0
2275    }
2276}
2277
2278/// Upstream `xmlValidateNameValue(value)` — 1 if valid, 0 otherwise.
2279///
2280/// # SAFETY
2281///
2282/// - `value` must be a valid null-terminated string or NULL.
2283pub unsafe fn validate_name_value(value: *const xmlChar) -> c_int {
2284    validate_value_internal(value, 0)
2285}
2286
2287/// Upstream `xmlValidateNamesValue(value)` — 1 if valid, 0 otherwise.
2288///
2289/// # SAFETY
2290///
2291/// - `value` must be a valid null-terminated string or NULL.
2292pub unsafe fn validate_names_value(value: *const xmlChar) -> c_int {
2293    validate_values_internal(value, 0)
2294}
2295
2296/// Upstream `xmlValidateNmtokenValue(value)` — 1 if valid, 0 otherwise.
2297///
2298/// # SAFETY
2299///
2300/// - `value` must be a valid null-terminated string or NULL.
2301pub unsafe fn validate_nmtoken_value(value: *const xmlChar) -> c_int {
2302    validate_value_internal(value, XML_SCAN_NMTOKEN)
2303}
2304
2305/// Upstream `xmlValidateNmtokensValue(value)` — 1 if valid, 0 otherwise.
2306///
2307/// # SAFETY
2308///
2309/// - `value` must be a valid null-terminated string or NULL.
2310pub unsafe fn validate_nmtokens_value(value: *const xmlChar) -> c_int {
2311    validate_values_internal(value, XML_SCAN_NMTOKEN)
2312}
2313
2314// ═══════════════════════════════════════════════════════════════════════════════
2315// DTD description lookups (upstream valid.c xmlGetDtd*Desc)
2316// ═══════════════════════════════════════════════════════════════════════════════
2317
2318/// Upstream `xmlGetDtdQElementDesc(dtd, name, prefix)`.
2319///
2320/// # SAFETY
2321///
2322/// - `dtd` must be a valid pointer or NULL; `name`/`prefix` NULL-terminated
2323///   strings or NULL.
2324pub unsafe fn get_dtd_qelement_desc(
2325    dtd: *mut _xmlDtd,
2326    name: *const xmlChar,
2327    prefix: *const xmlChar,
2328) -> *mut _xmlElement {
2329    if dtd.is_null() {
2330        return ptr::null_mut();
2331    }
2332    unsafe {
2333        let elements = (*dtd).elements;
2334        if elements.is_null() {
2335            return ptr::null_mut();
2336        }
2337        hash::hash_lookup2(elements as *mut hash::HashTable, name, prefix) as *mut _xmlElement
2338    }
2339}
2340
2341/// Upstream `xmlGetDtdQAttrDesc(dtd, elem, name, prefix)` — the attribute
2342/// declaration table is keyed by (name, prefix, elem).
2343///
2344/// # SAFETY
2345///
2346/// - `dtd` must be a valid pointer or NULL; `elem`/`name`/`prefix`
2347///   NULL-terminated strings or NULL.
2348pub unsafe fn get_dtd_qattr_desc(
2349    dtd: *mut _xmlDtd,
2350    elem: *const xmlChar,
2351    name: *const xmlChar,
2352    prefix: *const xmlChar,
2353) -> *mut _xmlAttribute {
2354    if dtd.is_null() || elem.is_null() || name.is_null() {
2355        return ptr::null_mut();
2356    }
2357    unsafe {
2358        let attrs = (*dtd).attributes;
2359        if attrs.is_null() {
2360            return ptr::null_mut();
2361        }
2362        hash::hash_lookup3(attrs as *mut hash::HashTable, name, prefix, elem) as *mut _xmlAttribute
2363    }
2364}
2365
2366/// Upstream `xmlGetDtdNotationDesc(dtd, name)`.
2367///
2368/// # SAFETY
2369///
2370/// - `dtd` must be a valid pointer or NULL; `name` a NULL-terminated string.
2371pub unsafe fn get_dtd_notation_desc(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlNotation {
2372    if dtd.is_null() || name.is_null() {
2373        return ptr::null_mut();
2374    }
2375    unsafe {
2376        let notations = (*dtd).notations;
2377        if notations.is_null() {
2378            return ptr::null_mut();
2379        }
2380        hash::hash_lookup(notations as *mut hash::HashTable, name) as *mut _xmlNotation
2381    }
2382}
2383
2384/// Split a QName at the FIRST ':' (upstream tree.c `xmlSplitQName4`):
2385/// `prefix` receives a duplicated prefix (or NULL) and the local name
2386/// (a pointer into the original string) is returned.
2387///
2388/// # SAFETY
2389///
2390/// - `name` must be a valid null-terminated string; `prefix` a valid
2391///   out-pointer.
2392pub unsafe fn split_qname4(name: *const xmlChar, prefix: *mut *mut xmlChar) -> *const xmlChar {
2393    if prefix.is_null() {
2394        return name;
2395    }
2396    unsafe {
2397        *prefix = ptr::null_mut();
2398        if name.is_null() {
2399            return ptr::null();
2400        }
2401        let bytes = string::xmlstr_to_bytes(name);
2402        match bytes.iter().position(|&b| b == b':') {
2403            None => name,
2404            Some(pos) => {
2405                let p = string::bytes_to_xmlstr(&bytes[..pos]);
2406                *prefix = p;
2407                name.add(pos + 1)
2408            }
2409        }
2410    }
2411}
2412
2413// ═══════════════════════════════════════════════════════════════════════════════
2414// ID / REF tables (upstream valid.c xmlAddID / xmlAddRef / xmlRemoveID / xmlRemoveRef)
2415// ═══════════════════════════════════════════════════════════════════════════════
2416
2417/// Free an xmlID entry (upstream xmlFreeID). Also clears the owning
2418/// attribute's id/atype back-references.
2419///
2420/// # Safety
2421///
2422/// - `id` must be NULL or a pointer to a heap-allocated `_xmlID` whose
2423///   `value` and `name` fields are NULL or pointers to strings allocated by
2424///   `xml_strdup`; the function frees both, clears the `id` and `atype`
2425///   fields of `attr` when `attr` is non-NULL (it must then be a valid
2426///   `_xmlAttr`), frees `id` itself, and leaves it dangling.
2427unsafe fn free_id(id: *mut _xmlID) {
2428    if id.is_null() {
2429        return;
2430    }
2431    unsafe {
2432        if !(*id).value.is_null() {
2433            allocator::xmlFreeImpl((*id).value as *mut c_void);
2434        }
2435        if !(*id).name.is_null() {
2436            allocator::xmlFreeImpl((*id).name as *mut c_void);
2437        }
2438        if !(*id).attr.is_null() {
2439            (*(*id).attr).id = ptr::null_mut();
2440            (*(*id).attr).atype = 0;
2441        }
2442        allocator::xmlFreeImpl(id as *mut c_void);
2443    }
2444}
2445
2446/// Hash-table deallocator for ID entries (name is *mut per
2447/// xmlHashDeallocator).
2448unsafe extern "C" fn free_id_entry(payload: *mut c_void, _name: *mut xmlChar) {
2449    free_id(payload as *mut _xmlID);
2450}
2451
2452/// Upstream xmlAddIDInternal: add an attribute value as an ID.
2453/// Returns 1 on success, 0 if the ID already exists, -1 on OOM.
2454///
2455/// # Safety
2456///
2457/// - `attr` must be non-NULL and a valid `_xmlAttr` whose `doc` field is a
2458///   valid `_xmlDoc`; `value` must be NULL or a valid null-terminated
2459///   string (a NULL or empty value returns 0); `id_ptr` may be NULL or a
2460///   valid writable out-pointer. On success the created `_xmlID` is owned by
2461///   the document's ID table and must later be released via `remove_id` or
2462///   `free_id_table`.
2463unsafe fn add_id_internal(
2464    attr: *mut _xmlAttr,
2465    value: *const xmlChar,
2466    id_ptr: *mut *mut _xmlID,
2467) -> c_int {
2468    unsafe {
2469        if !id_ptr.is_null() {
2470            *id_ptr = ptr::null_mut();
2471        }
2472        if value.is_null() || *value == 0 {
2473            return 0;
2474        }
2475        if attr.is_null() {
2476            return 0;
2477        }
2478        let doc = (*attr).doc;
2479        if doc.is_null() {
2480            return 0;
2481        }
2482
2483        let mut table = (*doc).ids as *mut hash::HashTable;
2484        if table.is_null() {
2485            (*doc).ids = hash::hash_create(0) as *mut c_void;
2486            table = (*doc).ids as *mut hash::HashTable;
2487            if table.is_null() {
2488                return -1;
2489            }
2490        } else if !hash::hash_lookup(table, value).is_null() {
2491            return 0;
2492        }
2493
2494        let id = allocator::xmlMallocZero(size_of::<_xmlID>() as usize) as *mut _xmlID;
2495        if id.is_null() {
2496            return -1;
2497        }
2498        (*id).doc = doc;
2499        (*id).value = string::xml_strdup(value);
2500        if (*id).value.is_null() {
2501            free_id(id);
2502            return -1;
2503        }
2504        // re-registering an attribute drops its previous ID
2505        if !(*attr).id.is_null() {
2506            remove_id(doc, attr);
2507        }
2508        if hash::hash_add_entry(table, value, id as *mut c_void) != 0 {
2509            free_id(id);
2510            return -1;
2511        }
2512        if !id_ptr.is_null() {
2513            *id_ptr = id;
2514        }
2515        (*id).attr = attr;
2516        (*id).lineno = tree::get_line_no((*attr).parent) as c_int;
2517        (*attr).atype = XML_ATTRIBUTE_ID as c_int;
2518        (*attr).id = id as *mut c_void;
2519        1
2520    }
2521}
2522
2523/// Upstream `xmlAddID(ctxt, doc, value, attr)` — returns the xmlID or NULL.
2524/// Reports "ID %s already defined" through the validation context on
2525/// duplicates and a memory error on OOM.
2526///
2527/// # SAFETY
2528///
2529/// - `ctxt` may be NULL; `doc`/`attr` must be valid pointers (attr->doc == doc).
2530pub unsafe fn add_id(
2531    ctxt: *mut _xmlValidCtxt,
2532    doc: *mut _xmlDoc,
2533    value: *const xmlChar,
2534    attr: *mut _xmlAttr,
2535) -> *mut _xmlID {
2536    unsafe {
2537        if attr.is_null() || doc != (*attr).doc {
2538            return ptr::null_mut();
2539        }
2540        let mut id = ptr::null_mut();
2541        let res = add_id_internal(attr, value, &mut id);
2542        if res < 0 {
2543            vctxt_error(
2544                ctxt,
2545                b"Memory allocation failed : xmlAddID\0" as *const u8 as *const c_char,
2546            );
2547        } else if res == 0 && !ctxt.is_null() {
2548            let msg = format!("ID {} already defined\0", string::xmlstr_to_string(value));
2549            vctxt_error(ctxt, msg.as_ptr() as *const c_char);
2550        }
2551        id
2552    }
2553}
2554
2555/// Upstream `xmlRemoveID(doc, attr)` — removes the attribute's ID entry.
2556/// Returns 0 on success, -1 otherwise.
2557///
2558/// # SAFETY
2559///
2560/// - `doc`/`attr` must be valid pointers or NULL.
2561pub unsafe fn remove_id(doc: *mut _xmlDoc, attr: *mut _xmlAttr) -> c_int {
2562    unsafe {
2563        if doc.is_null() {
2564            return -1;
2565        }
2566        if attr.is_null() || (*attr).id.is_null() {
2567            return -1;
2568        }
2569        let table = (*doc).ids as *mut hash::HashTable;
2570        if table.is_null() {
2571            return -1;
2572        }
2573        let value = (*((*attr).id as *mut _xmlID)).value;
2574        if hash::hash_remove_entry(table, value, Some(free_id_entry)) < 0 {
2575            return -1;
2576        }
2577        0
2578    }
2579}
2580
2581/// Free an xmlRef entry.
2582///
2583/// # Safety
2584///
2585/// - `r` must be NULL or a pointer to a heap-allocated `_xmlRef` whose
2586///   `value` and `name` fields are NULL or `xml_strdup`-allocated strings;
2587///   the function frees them and `r` itself, after which `r` is dangling.
2588unsafe fn free_ref(r: *mut _xmlRef) {
2589    if r.is_null() {
2590        return;
2591    }
2592    unsafe {
2593        if !(*r).value.is_null() {
2594            allocator::xmlFreeImpl((*r).value as *mut c_void);
2595        }
2596        if !(*r).name.is_null() {
2597            allocator::xmlFreeImpl((*r).name as *mut c_void);
2598        }
2599        allocator::xmlFreeImpl(r as *mut c_void);
2600    }
2601}
2602
2603/// xmlList deallocator for REF entries.
2604unsafe extern "C" fn free_ref_list_entry(data: *mut c_void) {
2605    free_ref(data as *mut _xmlRef);
2606}
2607
2608/// xmlList comparator (upstream xmlDummyCompare: never equal).
2609const unsafe extern "C" fn dummy_compare(_a: *const c_void, _b: *const c_void) -> c_int {
2610    1
2611}
2612
2613/// Hash-table deallocator for REF lists.
2614unsafe extern "C" fn free_ref_table_entry(payload: *mut c_void, _name: *mut xmlChar) {
2615    crate::xml::list::list_delete(payload as *mut crate::xml::list::List);
2616}
2617
2618/// Upstream `xmlAddRef(ctxt, doc, value, attr)` — registers an IDREF.
2619/// Returns the xmlRef or NULL.
2620///
2621/// # SAFETY
2622///
2623/// - `ctxt` may be NULL; `doc`/`attr`/`value` must be valid pointers.
2624pub unsafe fn add_ref(
2625    ctxt: *mut _xmlValidCtxt,
2626    doc: *mut _xmlDoc,
2627    value: *const xmlChar,
2628    attr: *mut _xmlAttr,
2629) -> *mut _xmlRef {
2630    unsafe {
2631        if doc.is_null() || value.is_null() || attr.is_null() {
2632            return ptr::null_mut();
2633        }
2634
2635        let mut table = (*doc).refs as *mut hash::HashTable;
2636        if table.is_null() {
2637            (*doc).refs = hash::hash_create(0) as *mut c_void;
2638            table = (*doc).refs as *mut hash::HashTable;
2639            if table.is_null() {
2640                vctxt_error(
2641                    ctxt,
2642                    b"Memory allocation failed : xmlAddRef\0" as *const u8 as *const c_char,
2643                );
2644                return ptr::null_mut();
2645            }
2646        }
2647
2648        let ret = allocator::xmlMallocZero(size_of::<_xmlRef>() as usize) as *mut _xmlRef;
2649        if ret.is_null() {
2650            vctxt_error(
2651                ctxt,
2652                b"Memory allocation failed : xmlAddRef\0" as *const u8 as *const c_char,
2653            );
2654            return ptr::null_mut();
2655        }
2656        (*ret).value = string::xml_strdup(value);
2657        if (*ret).value.is_null() {
2658            free_ref(ret);
2659            vctxt_error(
2660                ctxt,
2661                b"Memory allocation failed : xmlAddRef\0" as *const u8 as *const c_char,
2662            );
2663            return ptr::null_mut();
2664        }
2665        // Upstream xmlIsStreaming(ctxt): streaming (reader) mode stores the
2666        // attr name because the attribute node will be destroyed; tree mode
2667        // stores the attribute pointer.
2668        let streaming = !ctxt.is_null()
2669            && !(*ctxt).userData.is_null()
2670            && (*((*ctxt).userData as *mut _xmlParserCtxt)).parseMode
2671                == crate::abi::types::xmlParserMode::XML_PARSE_READER as c_int;
2672        if streaming {
2673            (*ret).name = string::xml_strdup((*attr).name);
2674            (*ret).attr = ptr::null_mut();
2675        } else {
2676            (*ret).name = ptr::null();
2677            (*ret).attr = attr;
2678        }
2679        (*ret).lineno = tree::get_line_no((*attr).parent) as c_int;
2680
2681        // References are lists of xmlRef per value.
2682        let ref_list = hash::hash_lookup(table, value) as *mut crate::xml::list::List;
2683        if ref_list.is_null() {
2684            let l = crate::xml::list::list_create(Some(free_ref_list_entry), Some(dummy_compare));
2685            if l.is_null() {
2686                free_ref(ret);
2687                vctxt_error(
2688                    ctxt,
2689                    b"Memory allocation failed : xmlAddRef\0" as *const u8 as *const c_char,
2690                );
2691                return ptr::null_mut();
2692            }
2693            if hash::hash_add_entry(table, value, l as *mut c_void) != 0 {
2694                crate::xml::list::list_delete(l);
2695                free_ref(ret);
2696                vctxt_error(
2697                    ctxt,
2698                    b"Memory allocation failed : xmlAddRef\0" as *const u8 as *const c_char,
2699                );
2700                return ptr::null_mut();
2701            }
2702            crate::xml::list::list_append(l, ret as *mut c_void);
2703        } else {
2704            if crate::xml::list::list_append(ref_list, ret as *mut c_void) != 0 {
2705                free_ref(ret);
2706                vctxt_error(
2707                    ctxt,
2708                    b"Memory allocation failed : xmlAddRef\0" as *const u8 as *const c_char,
2709                );
2710                return ptr::null_mut();
2711            }
2712        }
2713        ret
2714    }
2715}
2716
2717/// Upstream `xmlRemoveRef(doc, attr)` — removes the attribute's IDREF
2718/// entry. Returns 0 on success, -1 otherwise.
2719///
2720/// # SAFETY
2721///
2722/// - `doc`/`attr` must be valid pointers or NULL.
2723pub unsafe fn remove_ref(doc: *mut _xmlDoc, attr: *mut _xmlAttr) -> c_int {
2724    // The candidate does not track a back-pointer from attribute to ref
2725    // (upstream keeps the ref's value only in the table key). Re-scan the
2726    // ref table for entries owned by this attribute.
2727    if doc.is_null() || attr.is_null() {
2728        return -1;
2729    }
2730    unsafe {
2731        let table = (*doc).refs as *mut hash::HashTable;
2732        if table.is_null() {
2733            return -1;
2734        }
2735        let mut removed = -1;
2736        // iterate: hash_scan with a callback that removes matching entries
2737        struct ScanCtx {
2738            table: *mut hash::HashTable,
2739            attr: *mut _xmlAttr,
2740            removed: c_int,
2741        }
2742        extern "C" fn scan_remove(payload: *mut c_void, data: *mut c_void, name: *const xmlChar) {
2743            let ctx = unsafe { &mut *(data as *mut ScanCtx) };
2744            let l = payload as *mut crate::xml::list::List;
2745            // remove every list element whose attr matches
2746            let mut cur: *mut c_void = crate::xml::list::list_front(l);
2747            while !cur.is_null() {
2748                let next: *mut c_void = unsafe { (*(cur as *mut _xmlRef)).next as *mut c_void };
2749                let r = cur as *mut _xmlRef;
2750                if unsafe { (*r).attr } == ctx.attr {
2751                    unsafe {
2752                        crate::xml::list::list_remove_first(l, cur);
2753                    }
2754                    ctx.removed = 0;
2755                }
2756                cur = next;
2757            }
2758            if crate::xml::list::list_empty(l) != 0 {
2759                unsafe {
2760                    hash::hash_remove_entry(ctx.table, name, Some(free_ref_table_entry));
2761                }
2762            }
2763            let _ = name;
2764        }
2765        let mut ctx = ScanCtx {
2766            table,
2767            attr,
2768            removed: -1,
2769        };
2770        hash::hash_scan(
2771            table,
2772            Some(scan_remove),
2773            &mut ctx as *mut ScanCtx as *mut c_void,
2774        );
2775        removed = ctx.removed;
2776        removed
2777    }
2778}
2779
2780/// Upstream `xmlAddIDSafe(attr, value)` (2.13+): add an ID without a
2781/// validation context. Returns 1 on success, 0 if the ID already exists,
2782/// -1 on OOM.
2783///
2784/// # SAFETY
2785///
2786/// - `attr`/`value` must be valid pointers or NULL.
2787pub unsafe fn add_id_safe(attr: *mut _xmlAttr, value: *const xmlChar) -> c_int {
2788    add_id_internal(attr, value, ptr::null_mut())
2789}
2790
2791/// Upstream `xmlFreeIDTable(table)`.
2792///
2793/// # SAFETY
2794///
2795/// - `table` must be a valid ID hash table or NULL.
2796pub unsafe fn free_id_table(table: *mut hash::HashTable) {
2797    hash::hash_free(table, Some(free_id_entry));
2798}
2799
2800/// Upstream `xmlFreeRefTable(table)`.
2801///
2802/// # SAFETY
2803///
2804/// - `table` must be a valid ref hash table or NULL.
2805pub unsafe fn free_ref_table(table: *mut hash::HashTable) {
2806    hash::hash_free(table, Some(free_ref_table_entry));
2807}
2808
2809/// Upstream `xmlGetID(doc, ID)`: returns the attribute holding the ID, or
2810/// the document pointer itself when operating on a stream (attribute node no
2811/// longer exists).
2812///
2813/// # SAFETY
2814///
2815/// - `doc`/`ID` must be valid pointers or NULL.
2816pub unsafe fn get_id(doc: *mut _xmlDoc, id: *const xmlChar) -> *mut _xmlAttr {
2817    unsafe {
2818        if doc.is_null() || id.is_null() {
2819            return ptr::null_mut();
2820        }
2821        let table = (*doc).ids as *mut hash::HashTable;
2822        if table.is_null() {
2823            return ptr::null_mut();
2824        }
2825        let id_entry = hash::hash_lookup(table, id) as *mut _xmlID;
2826        if id_entry.is_null() {
2827            return ptr::null_mut();
2828        }
2829        if (*id_entry).attr.is_null() {
2830            // streaming mode: return the document as a well-known reference
2831            doc as *mut _xmlAttr
2832        } else {
2833            (*id_entry).attr
2834        }
2835    }
2836}
2837
2838/// Upstream `xmlGetRefs(doc, ID)`: returns the list of references for an ID.
2839///
2840/// # SAFETY
2841///
2842/// - `doc`/`ID` must be valid pointers or NULL.
2843pub unsafe fn get_refs(doc: *mut _xmlDoc, id: *const xmlChar) -> *mut crate::xml::list::List {
2844    unsafe {
2845        if doc.is_null() || id.is_null() {
2846            return ptr::null_mut();
2847        }
2848        let table = (*doc).refs as *mut hash::HashTable;
2849        if table.is_null() {
2850            return ptr::null_mut();
2851        }
2852        hash::hash_lookup(table, id) as *mut crate::xml::list::List
2853    }
2854}
2855
2856/// Upstream `xmlIsID(doc, elem, attr)`: is this attribute an ID? Handles the
2857/// HTML special cases (id attribute; name attribute on `<a>`) and the DTD
2858/// declaration lookup, plus the xml:id namespace convention.
2859///
2860/// # SAFETY
2861///
2862/// - `doc`/`elem`/`attr` must be valid pointers or NULL.
2863pub unsafe fn is_id(doc: *mut _xmlDoc, elem: *mut _xmlNode, attr: *mut _xmlAttr) -> c_int {
2864    unsafe {
2865        if attr.is_null() || (*attr).name.is_null() {
2866            return 0;
2867        }
2868        if !doc.is_null() && (*doc).type_ == XML_HTML_DOCUMENT_NODE as c_int {
2869            if string::xml_strcmp(b"id\0" as *const u8 as *const xmlChar, (*attr).name) == 0 {
2870                return 1;
2871            }
2872            if elem.is_null() || (*elem).type_ != XML_ELEMENT_NODE as c_int {
2873                return 0;
2874            }
2875            if string::xml_strcmp(b"name\0" as *const u8 as *const xmlChar, (*attr).name) == 0
2876                && string::xml_strcmp(b"a\0" as *const u8 as *const xmlChar, (*elem).name) == 0
2877            {
2878                return 1;
2879            }
2880        } else {
2881            // xml:id convention
2882            if !(*attr).ns.is_null()
2883                && !(*(*attr).ns).prefix.is_null()
2884                && string::xml_strcmp(
2885                    (*(*attr).ns).prefix,
2886                    b"xml\0" as *const u8 as *const xmlChar,
2887                ) == 0
2888                && string::xml_strcmp((*attr).name, b"id\0" as *const u8 as *const xmlChar) == 0
2889            {
2890                return 1;
2891            }
2892            if doc.is_null() || ((*doc).intSubset.is_null() && (*doc).extSubset.is_null()) {
2893                return 0;
2894            }
2895            if elem.is_null()
2896                || (*elem).type_ != XML_ELEMENT_NODE as c_int
2897                || (*elem).name.is_null()
2898            {
2899                return 0;
2900            }
2901            let mut fullname = (*elem).name;
2902            let mut owned = false;
2903            if !(*elem).ns.is_null() && !(*(*elem).ns).prefix.is_null() {
2904                let f = string::build_qname((*elem).name, (*(*elem).ns).prefix, ptr::null_mut(), 0);
2905                if f.is_null() {
2906                    return -1;
2907                }
2908                fullname = f;
2909                owned = true;
2910            }
2911            let aprefix = if !(*attr).ns.is_null() {
2912                (*(*attr).ns).prefix
2913            } else {
2914                ptr::null()
2915            };
2916            let mut attr_decl =
2917                get_dtd_qattr_desc((*doc).intSubset, fullname, (*attr).name, aprefix);
2918            if attr_decl.is_null() && !(*doc).extSubset.is_null() {
2919                attr_decl = get_dtd_qattr_desc((*doc).extSubset, fullname, (*attr).name, aprefix);
2920            }
2921            if owned {
2922                allocator::xmlFreeImpl(fullname as *mut c_void);
2923            }
2924            if !attr_decl.is_null() && (*attr_decl).atype == XML_ATTRIBUTE_ID as c_int {
2925                return 1;
2926            }
2927        }
2928        0
2929    }
2930}
2931
2932/// Upstream `xmlIsRef(doc, elem, attr)`: is this attribute an IDREF?
2933///
2934/// # SAFETY
2935///
2936/// - `doc`/`elem`/`attr` must be valid pointers or NULL.
2937pub unsafe fn is_ref(doc: *mut _xmlDoc, elem: *mut _xmlNode, attr: *mut _xmlAttr) -> c_int {
2938    unsafe {
2939        if attr.is_null() {
2940            return 0;
2941        }
2942        let doc = if doc.is_null() { (*attr).doc } else { doc };
2943        if doc.is_null() {
2944            return 0;
2945        }
2946        if (*doc).intSubset.is_null() && (*doc).extSubset.is_null() {
2947            return 0;
2948        }
2949        if (*doc).type_ == XML_HTML_DOCUMENT_NODE as c_int {
2950            return 0;
2951        }
2952        if elem.is_null() {
2953            return 0;
2954        }
2955        let aprefix = if !(*attr).ns.is_null() {
2956            (*(*attr).ns).prefix
2957        } else {
2958            ptr::null()
2959        };
2960        let mut attr_decl =
2961            get_dtd_qattr_desc((*doc).intSubset, (*elem).name, (*attr).name, aprefix);
2962        if attr_decl.is_null() && !(*doc).extSubset.is_null() {
2963            attr_decl = get_dtd_qattr_desc((*doc).extSubset, (*elem).name, (*attr).name, aprefix);
2964        }
2965        if !attr_decl.is_null()
2966            && ((*attr_decl).atype == XML_ATTRIBUTE_IDREF as c_int
2967                || (*attr_decl).atype == XML_ATTRIBUTE_IDREFS as c_int)
2968        {
2969            return 1;
2970        }
2971        0
2972    }
2973}
2974
2975/// Upstream `xmlGetDtdElementDesc(dtd, name)` — plain element declaration
2976/// lookup with QName splitting.
2977///
2978/// # SAFETY
2979///
2980/// - `dtd` must be a valid pointer or NULL; `name` a NULL-terminated string.
2981pub unsafe fn get_dtd_element_desc(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlElement {
2982    unsafe {
2983        if dtd.is_null() || name.is_null() {
2984            return ptr::null_mut();
2985        }
2986        let elements = (*dtd).elements;
2987        if elements.is_null() {
2988            return ptr::null_mut();
2989        }
2990        let mut prefix = ptr::null_mut();
2991        let local = split_qname4(name, &mut prefix);
2992        if local.is_null() {
2993            if !prefix.is_null() {
2994                allocator::xmlFreeImpl(prefix as *mut c_void);
2995            }
2996            return ptr::null_mut();
2997        }
2998        let cur =
2999            hash::hash_lookup2(elements as *mut hash::HashTable, local, prefix) as *mut _xmlElement;
3000        if !prefix.is_null() {
3001            allocator::xmlFreeImpl(prefix as *mut c_void);
3002        }
3003        cur
3004    }
3005}
3006
3007/// Upstream `xmlGetDtdAttrDesc(dtd, elem, name)` — attribute declaration
3008/// lookup splitting the attribute QName into (local, prefix).
3009///
3010/// # SAFETY
3011///
3012/// - `dtd` must be a valid pointer or NULL; `elem`/`name` NULL-terminated
3013///   strings.
3014pub unsafe fn get_dtd_attr_desc(
3015    dtd: *mut _xmlDtd,
3016    elem: *const xmlChar,
3017    name: *const xmlChar,
3018) -> *mut _xmlAttribute {
3019    unsafe {
3020        if dtd.is_null() || elem.is_null() || name.is_null() {
3021            return ptr::null_mut();
3022        }
3023        let attrs = (*dtd).attributes;
3024        if attrs.is_null() {
3025            return ptr::null_mut();
3026        }
3027        let mut prefix = ptr::null_mut();
3028        let local = split_qname4(name, &mut prefix);
3029        if local.is_null() {
3030            if !prefix.is_null() {
3031                allocator::xmlFreeImpl(prefix as *mut c_void);
3032            }
3033            return ptr::null_mut();
3034        }
3035        let cur = hash::hash_lookup3(attrs as *mut hash::HashTable, local, prefix, elem)
3036            as *mut _xmlAttribute;
3037        if !prefix.is_null() {
3038            allocator::xmlFreeImpl(prefix as *mut c_void);
3039        }
3040        cur
3041    }
3042}
3043
3044// ═══════════════════════════════════════════════════════════════════════════════
3045// Declaration validators (upstream valid.c xmlValidateElementDecl / NotationDecl
3046// / OneAttribute / OneElement / OneNamespace)
3047// ═══════════════════════════════════════════════════════════════════════════════
3048
3049/// Emit a validation error with node context, mirroring upstream
3050/// xmlErrValidNode's formatting. The candidate's valid context carries a
3051/// generic error callback only (no structured error slot), so the error
3052/// code is not stored — the message text matches upstream byte-for-byte.
3053unsafe fn vctxt_error_node(ctxt: *mut _xmlValidCtxt, _node: *mut _xmlNode, msg: *const c_char) {
3054    vctxt_error(ctxt, msg);
3055}
3056
3057/// Upstream `xmlValidateElementDecl(ctxt, doc, elem)`: verifies the
3058/// declaration is not duplicated and that MIXED content models do not list
3059/// the same element twice.
3060///
3061/// # SAFETY
3062///
3063/// - `ctxt`/`doc` may be NULL; `elem` a valid pointer or NULL.
3064pub unsafe fn validate_element_decl(
3065    ctxt: *mut _xmlValidCtxt,
3066    doc: *mut _xmlDoc,
3067    elem: *mut _xmlElement,
3068) -> c_int {
3069    unsafe {
3070        if doc.is_null() || (*doc).intSubset.is_null() && (*doc).extSubset.is_null() {
3071            return 1;
3072        }
3073        if elem.is_null() {
3074            return 1;
3075        }
3076        let mut ret = 1;
3077
3078        // No Duplicate Types (VC: No Duplicate Types) — only for MIXED
3079        // declarations: walk the OR chain and compare element names.
3080        if (*elem).etype == XML_ELEMENT_TYPE_MIXED as c_int {
3081            let mut cur = (*elem).content;
3082            while !cur.is_null() {
3083                if (*cur).type_ != XML_ELEMENT_CONTENT_OR as c_int {
3084                    break;
3085                }
3086                if (*cur).c1.is_null() {
3087                    break;
3088                }
3089                if (*(*cur).c1).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int {
3090                    let name = (*(*cur).c1).name;
3091                    let mut next = (*cur).c2;
3092                    while !next.is_null() {
3093                        if (*next).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int {
3094                            if string::xml_strcmp((*next).name, name) == 0
3095                                && string::xml_strcmp((*next).prefix, (*(*cur).c1).prefix) == 0
3096                            {
3097                                if (*(*cur).c1).prefix.is_null() {
3098                                    let msg = format!(
3099                                        "Definition of {} has duplicate references of {}\0",
3100                                        string::xmlstr_to_string((*elem).name),
3101                                        string::xmlstr_to_string(name)
3102                                    );
3103                                    vctxt_error_node(
3104                                        ctxt,
3105                                        elem as *mut _xmlNode,
3106                                        msg.as_ptr() as *const c_char,
3107                                    );
3108                                } else {
3109                                    let msg = format!(
3110                                        "Definition of {} has duplicate references of {}:{}\0",
3111                                        string::xmlstr_to_string((*elem).name),
3112                                        string::xmlstr_to_string((*(*cur).c1).prefix),
3113                                        string::xmlstr_to_string(name)
3114                                    );
3115                                    vctxt_error_node(
3116                                        ctxt,
3117                                        elem as *mut _xmlNode,
3118                                        msg.as_ptr() as *const c_char,
3119                                    );
3120                                }
3121                                ret = 0;
3122                            }
3123                            break;
3124                        }
3125                        if (*next).c1.is_null() {
3126                            break;
3127                        }
3128                        if (*(*next).c1).type_ != XML_ELEMENT_CONTENT_ELEMENT as c_int {
3129                            break;
3130                        }
3131                        if string::xml_strcmp((*(*next).c1).name, name) == 0
3132                            && string::xml_strcmp((*(*next).c1).prefix, (*(*cur).c1).prefix) == 0
3133                        {
3134                            if (*(*cur).c1).prefix.is_null() {
3135                                let msg = format!(
3136                                    "Definition of {} has duplicate references to {}\0",
3137                                    string::xmlstr_to_string((*elem).name),
3138                                    string::xmlstr_to_string(name)
3139                                );
3140                                vctxt_error_node(
3141                                    ctxt,
3142                                    elem as *mut _xmlNode,
3143                                    msg.as_ptr() as *const c_char,
3144                                );
3145                            } else {
3146                                let msg = format!(
3147                                    "Definition of {} has duplicate references to {}:{}\0",
3148                                    string::xmlstr_to_string((*elem).name),
3149                                    string::xmlstr_to_string((*(*cur).c1).prefix),
3150                                    string::xmlstr_to_string(name)
3151                                );
3152                                vctxt_error_node(
3153                                    ctxt,
3154                                    elem as *mut _xmlNode,
3155                                    msg.as_ptr() as *const c_char,
3156                                );
3157                            }
3158                            ret = 0;
3159                        }
3160                        next = (*next).c2;
3161                    }
3162                }
3163                cur = (*cur).c2;
3164            }
3165        }
3166
3167        // VC: Unique Element Type Declaration — the declaration must not
3168        // already exist (with the same prefix) in either subset.
3169        let mut prefix = ptr::null_mut();
3170        let local_name = split_qname4((*elem).name, &mut prefix);
3171        if local_name.is_null() {
3172            vctxt_error(
3173                ctxt,
3174                b"Memory allocation failed : xmlValidateElementDecl\0" as *const u8
3175                    as *const c_char,
3176            );
3177            if !prefix.is_null() {
3178                allocator::xmlFreeImpl(prefix as *mut c_void);
3179            }
3180            return 0;
3181        }
3182
3183        for subset in [(*doc).intSubset, (*doc).extSubset] {
3184            if subset.is_null() {
3185                continue;
3186            }
3187            let tst = get_dtd_qelement_desc(subset, local_name, prefix);
3188            if !tst.is_null()
3189                && tst != elem
3190                && ((*tst).prefix == (*elem).prefix
3191                    || string::xml_strcmp((*tst).prefix, (*elem).prefix) == 0)
3192                && (*tst).etype != XML_ELEMENT_TYPE_UNDEFINED as c_int
3193            {
3194                let msg = format!(
3195                    "Redefinition of element {}\0",
3196                    string::xmlstr_to_string((*elem).name)
3197                );
3198                vctxt_error_node(ctxt, elem as *mut _xmlNode, msg.as_ptr() as *const c_char);
3199                ret = 0;
3200            }
3201        }
3202        if !prefix.is_null() {
3203            allocator::xmlFreeImpl(prefix as *mut c_void);
3204        }
3205        ret
3206    }
3207}
3208
3209/// Upstream `xmlValidateNotationDecl(ctxt, doc, nota)`: modern libxml2 has
3210/// no validity constraint on notation declarations and returns 1 always
3211/// (verified by disassembly of the system DSO: `mov $1,%eax; ret`).
3212///
3213/// # SAFETY
3214///
3215/// - `_ctxt`, `_doc`, `_nota` must be valid pointers (or NULL
3216///   where the upstream C contract allows), obtained from the
3217///   matching constructor/owner and not yet freed; the callee may
3218///   take or keep ownership exactly as the C API specifies.
3219///
3220/// The caller must not race this call with concurrent mutation of the
3221/// same objects from other threads (per-object state is not internally
3222/// synchronized). Violating any of the above is undefined behavior.
3223///
3224/// Exercised by the C-API differential courts
3225/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3226/// courts; those pass byte-for-byte against the upstream oracle.
3227pub const unsafe fn validate_notation_decl(
3228    _ctxt: *mut _xmlValidCtxt,
3229    _doc: *mut _xmlDoc,
3230    _nota: *mut _xmlNotation,
3231) -> c_int {
3232    1
3233}
3234
3235/// Upstream `xmlValidateOneAttribute(ctxt, doc, elem, attr, value)`.
3236///
3237/// Performs [VC: Attribute Value Type], [VC: Fixed Attribute Default],
3238/// [VC: ID], [VC: IDREF], [VC: Notation Attributes], [VC: Enumeration],
3239/// and the ENTITY existence check via xmlValidateAttributeValue2.
3240///
3241/// # SAFETY
3242///
3243/// - `ctxt`/`doc` may be NULL; `elem`/`attr`/`value` valid pointers or NULL.
3244pub unsafe fn validate_one_attribute(
3245    ctxt: *mut _xmlValidCtxt,
3246    doc: *mut _xmlDoc,
3247    elem: *mut _xmlNode,
3248    attr: *mut _xmlAttr,
3249    value: *const xmlChar,
3250) -> c_int {
3251    unsafe {
3252        if doc.is_null() {
3253            return 0;
3254        }
3255        if elem.is_null() || (*elem).name.is_null() {
3256            return 0;
3257        }
3258        if attr.is_null() || (*attr).name.is_null() {
3259            return 0;
3260        }
3261        let mut ret = 1;
3262
3263        let aprefix = if !(*attr).ns.is_null() {
3264            (*(*attr).ns).prefix
3265        } else {
3266            ptr::null()
3267        };
3268
3269        let mut attr_decl = ptr::null_mut();
3270        if !(*elem).ns.is_null() && !(*(*elem).ns).prefix.is_null() {
3271            let fullname =
3272                string::build_qname((*elem).name, (*(*elem).ns).prefix, ptr::null_mut(), 0);
3273            if fullname.is_null() {
3274                vctxt_error(
3275                    ctxt,
3276                    b"Memory allocation failed : xmlValidateOneAttribute\0" as *const u8
3277                        as *const c_char,
3278                );
3279                return 0;
3280            }
3281            attr_decl = get_dtd_qattr_desc((*doc).intSubset, fullname, (*attr).name, aprefix);
3282            if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3283                attr_decl = get_dtd_qattr_desc((*doc).extSubset, fullname, (*attr).name, aprefix);
3284            }
3285            if !std::ptr::eq(fullname, (*elem).name) {
3286                allocator::xmlFreeImpl(fullname as *mut c_void);
3287            }
3288        }
3289        if attr_decl.is_null() {
3290            attr_decl = get_dtd_qattr_desc((*doc).intSubset, (*elem).name, (*attr).name, aprefix);
3291            if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3292                attr_decl =
3293                    get_dtd_qattr_desc((*doc).extSubset, (*elem).name, (*attr).name, aprefix);
3294            }
3295        }
3296
3297        // [VC: Attribute Value Type]
3298        if attr_decl.is_null() {
3299            let msg = format!(
3300                "No declaration for attribute {} of element {}\0",
3301                string::xmlstr_to_string((*attr).name),
3302                string::xmlstr_to_string((*elem).name)
3303            );
3304            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3305            return 0;
3306        }
3307        if !(*attr).id.is_null() {
3308            remove_id(doc, attr);
3309        }
3310        (*attr).atype = (*attr_decl).atype;
3311
3312        // syntax check against the declared type (with OLD10 doc flag)
3313        let val = if (*doc).properties & crate::abi::types::xmlDocProperties::XML_DOC_OLD10 as c_int
3314            != 0
3315        {
3316            // OLD10 name classes are not implemented; the modern classes are
3317            // a superset for ASCII and match for all BMP ranges used here.
3318            match (*attr_decl).atype as u32 {
3319                t if t == XML_ATTRIBUTE_ENTITIES as u32 || t == XML_ATTRIBUTE_IDREFS as u32 => {
3320                    validate_values_internal(value, 0)
3321                }
3322                t if t == XML_ATTRIBUTE_ENTITY as u32
3323                    || t == XML_ATTRIBUTE_IDREF as u32
3324                    || t == XML_ATTRIBUTE_ID as u32
3325                    || t == XML_ATTRIBUTE_NOTATION as u32 =>
3326                {
3327                    validate_value_internal(value, 0)
3328                }
3329                t if t == XML_ATTRIBUTE_NMTOKENS as u32
3330                    || t == XML_ATTRIBUTE_ENUMERATION as u32 =>
3331                {
3332                    validate_values_internal(value, XML_SCAN_NMTOKEN)
3333                }
3334                t if t == XML_ATTRIBUTE_NMTOKEN as u32 => {
3335                    validate_value_internal(value, XML_SCAN_NMTOKEN)
3336                }
3337                _ => 1,
3338            }
3339        } else {
3340            validate_attribute_value((*attr_decl).atype, value)
3341        };
3342        if val == 0 {
3343            let msg = format!(
3344                "Syntax of value for attribute {} of {} is not valid\0",
3345                string::xmlstr_to_string((*attr).name),
3346                string::xmlstr_to_string((*elem).name)
3347            );
3348            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3349            ret = 0;
3350        }
3351
3352        // [VC: Fixed Attribute Default]
3353        if (*attr_decl).def == XML_ATTRIBUTE_FIXED as c_int
3354            && string::xml_strcmp(value, (*attr_decl).defaultValue) != 0
3355        {
3356            let _msg = format!(
3357                "Value for attribute {} of {} is different from default \"{}\n\0",
3358                string::xmlstr_to_string((*attr).name),
3359                string::xmlstr_to_string((*elem).name),
3360                string::xmlstr_to_string((*attr_decl).defaultValue)
3361            );
3362            // upstream format: "Value for attribute %s of %s is different from default \"%s\"\n"
3363            let msg = format!(
3364                "Value for attribute {} of {} is different from default \"{}\"\0",
3365                string::xmlstr_to_string((*attr).name),
3366                string::xmlstr_to_string((*elem).name),
3367                string::xmlstr_to_string((*attr_decl).defaultValue)
3368            );
3369            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3370            ret = 0;
3371        }
3372
3373        // [VC: ID] uniqueness (skipped inside entities)
3374        const XML_VCTXT_IN_ENTITY: c_uint = 4; // upstream valid.h
3375        if (*attr_decl).atype == XML_ATTRIBUTE_ID as c_int
3376            && (ctxt.is_null() || (*ctxt).flags & XML_VCTXT_IN_ENTITY == 0)
3377            && add_id(ctxt, doc, value, attr).is_null()
3378        {
3379            ret = 0;
3380        }
3381        if ((*attr_decl).atype == XML_ATTRIBUTE_IDREF as c_int
3382            || (*attr_decl).atype == XML_ATTRIBUTE_IDREFS as c_int)
3383            && add_ref(ctxt, doc, value, attr).is_null()
3384        {
3385            ret = 0;
3386        }
3387
3388        // [VC: Notation Attributes]
3389        if (*attr_decl).atype == XML_ATTRIBUTE_NOTATION as c_int {
3390            let mut nota = get_dtd_notation_desc((*doc).intSubset, value);
3391            if nota.is_null() {
3392                nota = get_dtd_notation_desc((*doc).extSubset, value);
3393            }
3394            if nota.is_null() {
3395                let msg = format!(
3396                    "Value \"{}\" for attribute {} of {} is not a declared Notation\0",
3397                    string::xmlstr_to_string(value),
3398                    string::xmlstr_to_string((*attr).name),
3399                    string::xmlstr_to_string((*elem).name)
3400                );
3401                vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3402                ret = 0;
3403            }
3404            let mut tree = (*attr_decl).tree;
3405            while !tree.is_null() {
3406                if string::xml_strcmp((*tree).name, value) == 0 {
3407                    break;
3408                }
3409                tree = (*tree).next;
3410            }
3411            if tree.is_null() {
3412                let msg = format!(
3413                    "Value \"{}\" for attribute {} of {} is not among the enumerated notations\0",
3414                    string::xmlstr_to_string(value),
3415                    string::xmlstr_to_string((*attr).name),
3416                    string::xmlstr_to_string((*elem).name)
3417                );
3418                vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3419                ret = 0;
3420            }
3421        }
3422
3423        // [VC: Enumeration]
3424        if (*attr_decl).atype == XML_ATTRIBUTE_ENUMERATION as c_int {
3425            let mut tree = (*attr_decl).tree;
3426            while !tree.is_null() {
3427                if string::xml_strcmp((*tree).name, value) == 0 {
3428                    break;
3429                }
3430                tree = (*tree).next;
3431            }
3432            if tree.is_null() {
3433                let msg = format!(
3434                    "Value \"{}\" for attribute {} of {} is not among the enumerated set\0",
3435                    string::xmlstr_to_string(value),
3436                    string::xmlstr_to_string((*attr).name),
3437                    string::xmlstr_to_string((*elem).name)
3438                );
3439                vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3440                ret = 0;
3441            }
3442        }
3443
3444        // Fixed Attribute Default (second occurrence, upstream)
3445        if (*attr_decl).def == XML_ATTRIBUTE_FIXED as c_int
3446            && string::xml_strcmp((*attr_decl).defaultValue, value) != 0
3447        {
3448            let msg = format!(
3449                "Value for attribute {} of {} must be \"{}\"\0",
3450                string::xmlstr_to_string((*attr).name),
3451                string::xmlstr_to_string((*elem).name),
3452                string::xmlstr_to_string((*attr_decl).defaultValue)
3453            );
3454            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3455            ret = 0;
3456        }
3457
3458        // [VC: Entity Name] — ENTITY must name a declared unparsed entity
3459        if (*attr_decl).atype == XML_ATTRIBUTE_ENTITY as c_int {
3460            let ent = tree::get_doc_entity(doc, value);
3461            if ent.is_null() {
3462                let msg = format!(
3463                    "ENTITY attribute {} reference an unknown entity \"{}\"\0",
3464                    string::xmlstr_to_string((*attr).name),
3465                    string::xmlstr_to_string(value)
3466                );
3467                vctxt_error_node(ctxt, doc as *mut _xmlNode, msg.as_ptr() as *const c_char);
3468                ret = 0;
3469            } else if (*ent).etype != XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int {
3470                let msg = format!(
3471                    "ENTITY attribute {} reference an entity \"{}\" of wrong type\0",
3472                    string::xmlstr_to_string((*attr).name),
3473                    string::xmlstr_to_string(value)
3474                );
3475                vctxt_error_node(ctxt, doc as *mut _xmlNode, msg.as_ptr() as *const c_char);
3476                ret = 0;
3477            }
3478        }
3479        ret
3480    }
3481}
3482
3483/// Upstream `xmlValidateOneNamespace(ctxt, doc, elem, prefix, ns, value)` —
3484/// namespace-declaration attribute validation.
3485///
3486/// # SAFETY
3487///
3488/// - `ctxt` may be NULL; `doc`/`elem`/`ns` valid pointers or NULL.
3489pub unsafe fn validate_one_namespace(
3490    ctxt: *mut _xmlValidCtxt,
3491    doc: *mut _xmlDoc,
3492    elem: *mut _xmlNode,
3493    prefix: *const xmlChar,
3494    ns: *mut _xmlNs,
3495    value: *const xmlChar,
3496) -> c_int {
3497    unsafe {
3498        if doc.is_null() {
3499            return 0;
3500        }
3501        if elem.is_null() || (*elem).name.is_null() {
3502            return 0;
3503        }
3504        if ns.is_null() || (*ns).href.is_null() {
3505            return 0;
3506        }
3507        let mut ret = 1;
3508
3509        let mut attr_decl = ptr::null_mut();
3510        if !prefix.is_null() {
3511            let fullname = string::build_qname((*elem).name, prefix, ptr::null_mut(), 0);
3512            if fullname.is_null() {
3513                vctxt_error(
3514                    ctxt,
3515                    b"Memory allocation failed : xmlValidateOneNamespace\0" as *const u8
3516                        as *const c_char,
3517                );
3518                return 0;
3519            }
3520            if !(*ns).prefix.is_null() {
3521                attr_decl = get_dtd_qattr_desc(
3522                    (*doc).intSubset,
3523                    fullname,
3524                    (*ns).prefix,
3525                    b"xmlns\0" as *const u8 as *const xmlChar,
3526                );
3527                if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3528                    attr_decl = get_dtd_qattr_desc(
3529                        (*doc).extSubset,
3530                        fullname,
3531                        (*ns).prefix,
3532                        b"xmlns\0" as *const u8 as *const xmlChar,
3533                    );
3534                }
3535            } else {
3536                attr_decl = get_dtd_qattr_desc(
3537                    (*doc).intSubset,
3538                    fullname,
3539                    b"xmlns\0" as *const u8 as *const xmlChar,
3540                    ptr::null(),
3541                );
3542                if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3543                    attr_decl = get_dtd_qattr_desc(
3544                        (*doc).extSubset,
3545                        fullname,
3546                        b"xmlns\0" as *const u8 as *const xmlChar,
3547                        ptr::null(),
3548                    );
3549                }
3550            }
3551            if !std::ptr::eq(fullname, (*elem).name) {
3552                allocator::xmlFreeImpl(fullname as *mut c_void);
3553            }
3554        }
3555        if attr_decl.is_null() {
3556            if !(*ns).prefix.is_null() {
3557                attr_decl = get_dtd_qattr_desc(
3558                    (*doc).intSubset,
3559                    (*elem).name,
3560                    (*ns).prefix,
3561                    b"xmlns\0" as *const u8 as *const xmlChar,
3562                );
3563                if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3564                    attr_decl = get_dtd_qattr_desc(
3565                        (*doc).extSubset,
3566                        (*elem).name,
3567                        (*ns).prefix,
3568                        b"xmlns\0" as *const u8 as *const xmlChar,
3569                    );
3570                }
3571            } else {
3572                attr_decl = get_dtd_qattr_desc(
3573                    (*doc).intSubset,
3574                    (*elem).name,
3575                    b"xmlns\0" as *const u8 as *const xmlChar,
3576                    ptr::null(),
3577                );
3578                if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3579                    attr_decl = get_dtd_qattr_desc(
3580                        (*doc).extSubset,
3581                        (*elem).name,
3582                        b"xmlns\0" as *const u8 as *const xmlChar,
3583                        ptr::null(),
3584                    );
3585                }
3586            }
3587        }
3588
3589        // [VC: Attribute Value Type]
3590        if attr_decl.is_null() {
3591            let msg = if !(*ns).prefix.is_null() {
3592                format!(
3593                    "No declaration for attribute xmlns:{} of element {}\0",
3594                    string::xmlstr_to_string((*ns).prefix),
3595                    string::xmlstr_to_string((*elem).name)
3596                )
3597            } else {
3598                format!(
3599                    "No declaration for attribute xmlns of element {}\0",
3600                    string::xmlstr_to_string((*elem).name)
3601                )
3602            };
3603            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3604            return 0;
3605        }
3606
3607        let val = validate_attribute_value((*attr_decl).atype, value);
3608        if val == 0 {
3609            let msg = if !(*ns).prefix.is_null() {
3610                format!(
3611                    "Syntax of value for attribute xmlns:{} of {} is not valid\0",
3612                    string::xmlstr_to_string((*ns).prefix),
3613                    string::xmlstr_to_string((*elem).name)
3614                )
3615            } else {
3616                format!(
3617                    "Syntax of value for attribute xmlns of {} is not valid\0",
3618                    string::xmlstr_to_string((*elem).name)
3619                )
3620            };
3621            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3622            ret = 0;
3623        }
3624
3625        // [VC: Fixed Attribute Default]
3626        if (*attr_decl).def == XML_ATTRIBUTE_FIXED as c_int
3627            && string::xml_strcmp(value, (*attr_decl).defaultValue) != 0
3628        {
3629            let msg = if !(*ns).prefix.is_null() {
3630                format!(
3631                    "Value for attribute xmlns:{} of {} is different from default \"{}\"\0",
3632                    string::xmlstr_to_string((*ns).prefix),
3633                    string::xmlstr_to_string((*elem).name),
3634                    string::xmlstr_to_string((*attr_decl).defaultValue)
3635                )
3636            } else {
3637                format!(
3638                    "Value for attribute xmlns of {} is different from default \"{}\"\0",
3639                    string::xmlstr_to_string((*elem).name),
3640                    string::xmlstr_to_string((*attr_decl).defaultValue)
3641                )
3642            };
3643            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3644            ret = 0;
3645        }
3646        ret
3647    }
3648}
3649
3650/// Upstream `xmlValidateOneElement(ctxt, doc, elem)` — validates a single
3651/// element against its declaration (content model + attributes), WITHOUT
3652/// recursing into children.
3653///
3654/// # SAFETY
3655///
3656/// - `ctxt` may be NULL; `doc`/`elem` valid pointers or NULL.
3657pub unsafe fn validate_one_element(
3658    ctxt: *mut _xmlValidCtxt,
3659    doc: *mut _xmlDoc,
3660    elem: *mut _xmlNode,
3661) -> c_int {
3662    unsafe {
3663        if doc.is_null() {
3664            return 0;
3665        }
3666        if elem.is_null() {
3667            return 0;
3668        }
3669        match (*elem).type_ {
3670            t if t == XML_TEXT_NODE as c_int
3671                || t == XML_CDATA_SECTION_NODE as c_int
3672                || t == XML_ENTITY_REF_NODE as c_int
3673                || t == XML_PI_NODE as c_int
3674                || t == XML_COMMENT_NODE as c_int
3675                || t == XML_XINCLUDE_START as c_int
3676                || t == XML_XINCLUDE_END as c_int =>
3677            {
3678                return 1;
3679            }
3680            t if t == XML_ELEMENT_NODE as c_int => {}
3681            _ => {
3682                vctxt_error_node(
3683                    ctxt,
3684                    elem,
3685                    b"unexpected element type\0" as *const u8 as *const c_char,
3686                );
3687                return 0;
3688            }
3689        }
3690
3691        let mut ret = 1;
3692        let mut extsubset = 0;
3693        let elem_decl = valid_get_elem_decl(ctxt, doc, elem, &mut extsubset);
3694        if elem_decl.is_null() {
3695            return 0;
3696        }
3697
3698        // Continuous (push) validation already checks the content model via
3699        // the vstate stack; skip the tree walk when active.
3700        if (*ctxt).vstateNr == 0 {
3701            match (*elem_decl).etype as u32 {
3702                t if t == XML_ELEMENT_TYPE_UNDEFINED as u32 => {
3703                    let msg = format!(
3704                        "No declaration for element {}\0",
3705                        string::xmlstr_to_string((*elem).name)
3706                    );
3707                    vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3708                    return 0;
3709                }
3710                t if t == XML_ELEMENT_TYPE_EMPTY as u32 => {
3711                    if !(*elem).children.is_null() {
3712                        let msg = format!(
3713                            "Element {} was declared EMPTY this one has content\0",
3714                            string::xmlstr_to_string((*elem).name)
3715                        );
3716                        vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3717                        ret = 0;
3718                    }
3719                }
3720                t if t == XML_ELEMENT_TYPE_ANY as u32 => {}
3721                t if t == XML_ELEMENT_TYPE_MIXED as u32 => {
3722                    if !(*elem_decl).content.is_null()
3723                        && (*(*elem_decl).content).type_ == XML_ELEMENT_CONTENT_PCDATA as c_int
3724                    {
3725                        // #PCDATA-only: any element child is an error
3726                        let mut child = (*elem).children;
3727                        while !child.is_null() {
3728                            if (*child).type_ == XML_ELEMENT_NODE as c_int {
3729                                let msg = format!(
3730                                    "Element {} was declared #PCDATA but contains non text nodes\0",
3731                                    string::xmlstr_to_string((*elem).name)
3732                                );
3733                                vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3734                                ret = 0;
3735                                break;
3736                            }
3737                            child = (*child).next;
3738                        }
3739                    } else {
3740                        // check each child element against the mixed list
3741                        let mut child = (*elem).children;
3742                        while !child.is_null() {
3743                            if (*child).type_ == XML_ELEMENT_NODE as c_int {
3744                                let mut fullname = (*child).name;
3745                                let mut own = false;
3746                                if !(*child).ns.is_null() && !(*(*child).ns).prefix.is_null() {
3747                                    let fnp = string::build_qname(
3748                                        (*child).name,
3749                                        (*(*child).ns).prefix,
3750                                        ptr::null_mut(),
3751                                        0,
3752                                    );
3753                                    if fnp.is_null() {
3754                                        vctxt_error(
3755                                            ctxt,
3756                                            b"Memory allocation failed : xmlValidateOneElement\0"
3757                                                as *const u8
3758                                                as *const c_char,
3759                                        );
3760                                        return 0;
3761                                    }
3762                                    fullname = fnp;
3763                                    own = true;
3764                                }
3765                                if validate_check_mixed(ctxt, (*elem_decl).content, fullname) != 1 {
3766                                    let msg = format!(
3767                                        "Element {} is not declared in {} list of possible children\0",
3768                                        string::xmlstr_to_string(fullname),
3769                                        string::xmlstr_to_string((*elem).name)
3770                                    );
3771                                    vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3772                                    ret = 0;
3773                                }
3774                                if own {
3775                                    allocator::xmlFreeImpl(fullname as *mut c_void);
3776                                }
3777                            }
3778                            child = (*child).next;
3779                        }
3780                    }
3781                }
3782                t if t == XML_ELEMENT_TYPE_ELEMENT as u32 => {
3783                    // Element-only content: collect child element names and
3784                    // check against the content model.
3785                    let mut names: Vec<*const xmlChar> = Vec::new();
3786                    let mut owned: Vec<*mut xmlChar> = Vec::new();
3787                    let mut child = (*elem).children;
3788                    while !child.is_null() {
3789                        if (*child).type_ == XML_ELEMENT_NODE as c_int {
3790                            let mut fullname = (*child).name;
3791                            if !(*child).ns.is_null() && !(*(*child).ns).prefix.is_null() {
3792                                let fnp = string::build_qname(
3793                                    (*child).name,
3794                                    (*(*child).ns).prefix,
3795                                    ptr::null_mut(),
3796                                    0,
3797                                );
3798                                if !fnp.is_null() {
3799                                    fullname = fnp;
3800                                    owned.push(fnp);
3801                                }
3802                            }
3803                            names.push(fullname);
3804                        }
3805                        child = (*child).next;
3806                    }
3807                    let result = dtd::valid_content_model((*elem_decl).content, &names);
3808                    for n in owned {
3809                        allocator::xmlFreeImpl(n as *mut c_void);
3810                    }
3811                    if result != dtd::ContentModelResult::Valid {
3812                        let msg = format!(
3813                            "Element {} content does not follow the DTD\0",
3814                            string::xmlstr_to_string((*elem).name)
3815                        );
3816                        vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3817                        ret = 0;
3818                    }
3819                }
3820                _ => {}
3821            }
3822
3823            // Required attributes + attribute value checks
3824            let mut attr = (*elem).properties;
3825            while !attr.is_null() {
3826                let aval = if !(*attr).children.is_null() {
3827                    (*(*attr).children).content
3828                } else {
3829                    ptr::null()
3830                };
3831                if validate_one_attribute(ctxt, doc, elem, attr, aval) == 0 {
3832                    ret = 0;
3833                }
3834                attr = (*attr).next;
3835            }
3836        }
3837        ret
3838    }
3839}
3840
3841// ═══════════════════════════════════════════════════════════════════════════════
3842// Streaming (push) validation — upstream valid.c xmlValidatePushElement /
3843// PushCData / PopElement + xmlValidBuildContentModel
3844// ═══════════════════════════════════════════════════════════════════════════════
3845//
3846// Upstream keeps a stack of validation states (one per open element). Each
3847// state holds the element declaration and, for ELEMENT content, a regexp
3848// exec context over the compiled content model. The candidate reproduces
3849// the same observable contract: per-push checks against the current state,
3850// "Misplaced"/"Text not allowed"/"Expecting more children" diagnostics,
3851// and the vstate push/pop stack on the public _xmlValidCtxt layout
3852// (vstate/vstateNr/vstateMax/vstateTab).
3853
3854/// Mirror of upstream `_xmlValidState` (valid.c): one entry per open element.
3855#[repr(C)]
3856struct ValidState {
3857    elem_decl: *mut _xmlElement,
3858    node: *mut _xmlNode,
3859    exec: *mut ContentModelExec,
3860}
3861
3862/// Find the declaration for an element (upstream xmlValidGetElemDecl).
3863/// Reports "No declaration for element %s" when absent.
3864///
3865/// # Safety
3866///
3867/// - `ctxt`, `doc` and `elem` must be valid non-NULL pointers (checked)
3868///   with a non-NULL `name` field on `elem` (checked); the `ns` field of
3869///   `elem` and its `prefix` may be NULL; the `intSubset` and `extSubset`
3870///   fields of `doc` must be valid DTD pointers or NULL; `extsubset` may be
3871///   NULL or a valid writable `c_int` out-pointer that is set to 1 when the
3872///   declaration comes from the external subset.
3873unsafe fn valid_get_elem_decl(
3874    ctxt: *mut _xmlValidCtxt,
3875    doc: *mut _xmlDoc,
3876    elem: *mut _xmlNode,
3877    extsubset: *mut c_int,
3878) -> *mut _xmlElement {
3879    unsafe {
3880        if ctxt.is_null() || doc.is_null() || elem.is_null() || (*elem).name.is_null() {
3881            return ptr::null_mut();
3882        }
3883        if !extsubset.is_null() {
3884            *extsubset = 0;
3885        }
3886        let mut elem_decl = ptr::null_mut();
3887
3888        let prefix = if !(*elem).ns.is_null() && !(*(*elem).ns).prefix.is_null() {
3889            (*(*elem).ns).prefix
3890        } else {
3891            ptr::null()
3892        };
3893        if !prefix.is_null() {
3894            elem_decl = get_dtd_qelement_desc((*doc).intSubset, (*elem).name, prefix);
3895            if elem_decl.is_null() && !(*doc).extSubset.is_null() {
3896                elem_decl = get_dtd_qelement_desc((*doc).extSubset, (*elem).name, prefix);
3897                if !elem_decl.is_null() && !extsubset.is_null() {
3898                    *extsubset = 1;
3899                }
3900            }
3901        }
3902        if elem_decl.is_null() {
3903            // non-strict fallback: plain name against either subset
3904            elem_decl = get_dtd_qelement_desc((*doc).intSubset, (*elem).name, ptr::null());
3905            if elem_decl.is_null() && !(*doc).extSubset.is_null() {
3906                elem_decl = get_dtd_qelement_desc((*doc).extSubset, (*elem).name, ptr::null());
3907                if !elem_decl.is_null() && !extsubset.is_null() {
3908                    *extsubset = 1;
3909                }
3910            }
3911        }
3912        if elem_decl.is_null() {
3913            let msg = format!(
3914                "No declaration for element {}\0",
3915                string::xmlstr_to_string((*elem).name)
3916            );
3917            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3918        }
3919        elem_decl
3920    }
3921}
3922
3923/// Upstream xmlValidateCheckMixed: is `qname` in the MIXED content list?
3924///
3925/// # Safety
3926///
3927/// - `cont` must be NULL or a valid `_xmlElementContent` chain whose
3928///   `type_`, `c1`, `c2`, `name` and `prefix` fields are valid for the
3929///   traversal (leaf `name` and `prefix` are NULL or null-terminated
3930///   strings); `qname` must be a valid null-terminated string (NULL is
3931///   tolerated via `split_qname3` and `xml_strcmp`); `ctxt` must be a valid
3932///   context or NULL (`vctxt_error` tolerates NULL).
3933pub(crate) unsafe fn validate_check_mixed(
3934    ctxt: *mut _xmlValidCtxt,
3935    cont: *mut _xmlElementContent,
3936    qname: *const xmlChar,
3937) -> c_int {
3938    unsafe {
3939        let mut plen: c_int = 0;
3940        // upstream xmlSplitQName3 returns the local-name pointer (NULL when
3941        // the qname has no colon) and fills *plen with the prefix length;
3942        // the candidate's split_qname3 mirrors that contract (R-000176).
3943        let local = string::split_qname3(qname, &mut plen);
3944        let mut cur = cont;
3945        if local.is_null() {
3946            while !cur.is_null() {
3947                if (*cur).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int {
3948                    if (*cur).prefix.is_null() && string::xml_strcmp((*cur).name, qname) == 0 {
3949                        return 1;
3950                    }
3951                } else if (*cur).type_ == XML_ELEMENT_CONTENT_OR as c_int
3952                    && !(*cur).c1.is_null()
3953                    && (*(*cur).c1).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int
3954                {
3955                    if (*(*cur).c1).prefix.is_null()
3956                        && string::xml_strcmp((*(*cur).c1).name, qname) == 0
3957                    {
3958                        return 1;
3959                    }
3960                } else if (*cur).type_ != XML_ELEMENT_CONTENT_OR as c_int
3961                    || (*cur).c1.is_null()
3962                    || (*(*cur).c1).type_ != XML_ELEMENT_CONTENT_PCDATA as c_int
3963                {
3964                    vctxt_error(
3965                        ctxt,
3966                        b"Internal: MIXED struct corrupted\0" as *const u8 as *const c_char,
3967                    );
3968                    break;
3969                }
3970                cur = (*cur).c2;
3971            }
3972        } else {
3973            while !cur.is_null() {
3974                if (*cur).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int {
3975                    if !(*cur).prefix.is_null()
3976                        && prefix_matches((*cur).prefix, qname, plen)
3977                        && string::xml_strcmp((*cur).name, local) == 0
3978                    {
3979                        return 1;
3980                    }
3981                } else if (*cur).type_ == XML_ELEMENT_CONTENT_OR as c_int
3982                    && !(*cur).c1.is_null()
3983                    && (*(*cur).c1).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int
3984                {
3985                    if !(*(*cur).c1).prefix.is_null()
3986                        && prefix_matches((*(*cur).c1).prefix, qname, plen)
3987                        && string::xml_strcmp((*(*cur).c1).name, local) == 0
3988                    {
3989                        return 1;
3990                    }
3991                } else if (*cur).type_ != XML_ELEMENT_CONTENT_OR as c_int
3992                    || (*cur).c1.is_null()
3993                    || (*(*cur).c1).type_ != XML_ELEMENT_CONTENT_PCDATA as c_int
3994                {
3995                    vctxt_error(
3996                        ctxt,
3997                        b"Internal: MIXED struct corrupted\0" as *const u8 as *const c_char,
3998                    );
3999                    break;
4000                }
4001                cur = (*cur).c2;
4002            }
4003        }
4004        0
4005    }
4006}
4007
4008/// Does `prefix` equal the first `len` bytes of `qname` (upstream
4009/// xmlStrncmp(prefix, qname, plen))?
4010///
4011/// # Safety
4012///
4013/// - `prefix` and `qname` must be valid null-terminated strings (NULL
4014///   yields an empty slice via `xmlstr_to_bytes`); `len` must be the
4015///   non-negative byte count to compare, and only bytes within the shorter
4016///   slice are read because both lengths are checked before slicing.
4017unsafe fn prefix_matches(prefix: *const xmlChar, qname: *const xmlChar, len: c_int) -> bool {
4018    unsafe {
4019        let p = string::xmlstr_to_bytes(prefix);
4020        let q = string::xmlstr_to_bytes(qname);
4021        p.len() == len as usize && q.len() >= len as usize && p[..len as usize] == q[..len as usize]
4022    }
4023}
4024
4025/// Incremental content-model matcher stored in `_xmlElement.cont_model`.
4026///
4027/// The candidate's regex engine matches character-by-character, which does
4028/// not model upstream's whole-name content-model tokens, so the content
4029/// model is compiled into a dedicated small NFA over full element names.
4030/// Upstream builds the same automaton (xmlValidBuildAContentModel) and then
4031/// converts it with xmlRegFromAutomata; the observable push/pop contract is
4032/// identical (per-push "Misplaced" errors, completion checks on pop).
4033#[derive(Debug)]
4034#[repr(C)]
4035pub struct ContentModelNfa {
4036    /// Flat transition list: (from_state, name, to_state); name NULL = epsilon.
4037    transitions: Vec<(u32, *const xmlChar, u32)>,
4038    /// start state index
4039    start: u32,
4040    /// accepting state indices (match complete)
4041    accept: Vec<u32>,
4042}
4043
4044/// Runtime exec state for one open element's content model.
4045#[derive(Debug)]
4046#[repr(C)]
4047pub struct ContentModelExec {
4048    /// the compiled NFA
4049    nfa: *mut ContentModelNfa,
4050    /// current state set after epsilon closure
4051    current: Vec<u32>,
4052}
4053
4054/// Thompson-style NFA builder over the content tree.
4055struct NfaBuilder {
4056    transitions: Vec<(u32, *const xmlChar, u32)>,
4057    n_states: u32,
4058}
4059
4060impl NfaBuilder {
4061    const fn new() -> Self {
4062        NfaBuilder {
4063            transitions: Vec::new(),
4064            n_states: 0,
4065        }
4066    }
4067    const fn new_state(&mut self) -> u32 {
4068        let s = self.n_states;
4069        self.n_states += 1;
4070        s
4071    }
4072    fn eps(&mut self, from: u32, to: u32) {
4073        self.transitions.push((from, ptr::null(), to));
4074    }
4075    fn name_trans(&mut self, from: u32, name: *const xmlChar, to: u32) {
4076        self.transitions.push((from, name, to));
4077    }
4078}
4079
4080/// Compile one content-model subtree. Returns (in_state, out_states); the
4081/// occurrence quantifier on the node is applied by wrapping the fragment
4082/// with epsilon edges (standard Thompson construction, matching upstream's
4083/// automaton shape for OPT/MULT/PLUS).
4084///
4085/// # Safety
4086///
4087/// - `model` must be NULL or a valid `_xmlElementContent` tree: `type_` and
4088///   `ocur` must be known enum values, `name` must be NULL or a valid
4089///   null-terminated string, and `c1`/`c2` must be NULL or valid child
4090///   nodes; the tree must be acyclic so that recursion terminates. NULL is
4091///   handled by emitting an empty fragment.
4092unsafe fn compile_content_sub(
4093    b: &mut NfaBuilder,
4094    model: *mut _xmlElementContent,
4095) -> (u32, Vec<u32>) {
4096    if model.is_null() {
4097        let s = b.new_state();
4098        return (s, vec![s]);
4099    }
4100    let m = unsafe { &*model };
4101    let (mut in_s, outs) = match m.type_ as u32 {
4102        t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
4103            let s = b.new_state();
4104            let to = b.new_state();
4105            b.name_trans(s, m.name, to);
4106            (s, vec![to])
4107        }
4108        t if t == XML_ELEMENT_CONTENT_SEQ as u32 => {
4109            let (in1, out1) = compile_content_sub(b, m.c1);
4110            let (in2, out2) = compile_content_sub(b, m.c2);
4111            for &o in &out1 {
4112                b.eps(o, in2);
4113            }
4114            (in1, out2)
4115        }
4116        t if t == XML_ELEMENT_CONTENT_OR as u32 => {
4117            let (in1, out1) = compile_content_sub(b, m.c1);
4118            let (in2, out2) = compile_content_sub(b, m.c2);
4119            let s = b.new_state();
4120            b.eps(s, in1);
4121            b.eps(s, in2);
4122            let mut all = out1;
4123            all.extend(out2);
4124            (s, all)
4125        }
4126        // PCDATA cannot appear in an ELEMENT content model; the caller
4127        // rejects it before compiling (upstream xmlValidBuildAContentModel
4128        // emits "Found PCDATA in content model of %s"). A PCDATA node here
4129        // compiles to an empty fragment so a malformed tree cannot crash.
4130        _ => {
4131            let s = b.new_state();
4132            (s, vec![s])
4133        }
4134    };
4135    match m.ocur as u32 {
4136        o if o == XML_ELEMENT_CONTENT_OPT as u32 => {
4137            let s = b.new_state();
4138            b.eps(s, in_s);
4139            for &o2 in &outs {
4140                b.eps(s, o2);
4141            }
4142            in_s = s;
4143        }
4144        o if o == XML_ELEMENT_CONTENT_MULT as u32 => {
4145            let s = b.new_state();
4146            b.eps(s, in_s);
4147            for &o2 in &outs {
4148                b.eps(s, o2);
4149                b.eps(o2, s);
4150            }
4151            in_s = s;
4152        }
4153        o if o == XML_ELEMENT_CONTENT_PLUS as u32 => {
4154            let s = b.new_state();
4155            b.eps(s, in_s);
4156            for &o2 in &outs {
4157                b.eps(o2, s);
4158            }
4159            in_s = s;
4160        }
4161        _ => {}
4162    }
4163    (in_s, outs)
4164}
4165
4166/// Does the content tree contain a PCDATA node (illegal in ELEMENT models)?
4167unsafe fn content_has_pcdata(model: *mut _xmlElementContent) -> bool {
4168    if model.is_null() {
4169        return false;
4170    }
4171    unsafe {
4172        let m = &*model;
4173        if m.type_ == XML_ELEMENT_CONTENT_PCDATA as c_int {
4174            return true;
4175        }
4176        content_has_pcdata(m.c1) || content_has_pcdata(m.c2)
4177    }
4178}
4179
4180/// Compile an element content tree into a ContentModelNfa.
4181///
4182/// # SAFETY
4183///
4184/// - `content` must be a valid content tree or NULL (returns NULL).
4185unsafe fn build_content_nfa(content: *mut _xmlElementContent) -> *mut ContentModelNfa {
4186    unsafe {
4187        if content.is_null() {
4188            return ptr::null_mut();
4189        }
4190        let mut b = NfaBuilder::new();
4191        let (start, outs) = compile_content_sub(&mut b, content);
4192        let nfa = Box::new(ContentModelNfa {
4193            transitions: b.transitions,
4194            start,
4195            accept: outs,
4196        });
4197        Box::into_raw(nfa)
4198    }
4199}
4200
4201/// Free a compiled content-model NFA (called from xmlFreeElement).
4202///
4203/// # SAFETY
4204///
4205/// - `nfa` must be a pointer from build_content_nfa or NULL.
4206pub unsafe fn free_content_model_nfa(nfa: *mut ContentModelNfa) {
4207    if nfa.is_null() {
4208        return;
4209    }
4210    unsafe {
4211        ptr::drop_in_place(nfa);
4212        allocator::xmlFreeImpl(nfa as *mut c_void);
4213    }
4214}
4215
4216/// Epsilon closure of a state set.
4217unsafe fn eps_closure(nfa: &ContentModelNfa, states: &[u32]) -> Vec<u32> {
4218    let mut out = states.to_vec();
4219    let mut stack = states.to_vec();
4220    while let Some(s) = stack.pop() {
4221        for &(from, name, to) in &nfa.transitions {
4222            if from == s && name.is_null() && !out.contains(&to) {
4223                out.push(to);
4224                stack.push(to);
4225            }
4226        }
4227    }
4228    out.sort_unstable();
4229    out.dedup();
4230    out
4231}
4232
4233/// Create an exec context over a compiled content model. Returns NULL on OOM.
4234///
4235/// # Safety
4236///
4237/// - `nfa` must be non-NULL and point to a valid compiled `ContentModelNfa`
4238///   (from `build_content_nfa`) that outlives the returned exec; the caller
4239///   must free the returned `ContentModelExec` with `free_content_exec`.
4240unsafe fn new_content_exec(nfa: *mut ContentModelNfa) -> *mut ContentModelExec {
4241    unsafe {
4242        let exec = allocator::xmlMallocImpl(size_of::<ContentModelExec>()) as *mut ContentModelExec;
4243        if exec.is_null() {
4244            return ptr::null_mut();
4245        }
4246        let cur = eps_closure(&*nfa, &[(*nfa).start]);
4247        ptr::write(&mut (*exec).nfa, nfa);
4248        ptr::write(&mut (*exec).current, cur);
4249        exec
4250    }
4251}
4252
4253/// Free an exec context.
4254///
4255/// # Safety
4256///
4257/// - `exec` must be NULL or a pointer previously returned by
4258///   `new_content_exec`; the `current` state set is dropped in place and the
4259///   struct is freed, leaving `exec` dangling. The referenced NFA is not
4260///   freed.
4261unsafe fn free_content_exec(exec: *mut ContentModelExec) {
4262    if exec.is_null() {
4263        return;
4264    }
4265    unsafe {
4266        ptr::drop_in_place(&mut (*exec).current);
4267        allocator::xmlFreeImpl(exec as *mut c_void);
4268    }
4269}
4270
4271/// Push a full element name (or NULL = end of input) into the exec context.
4272///
4273/// Mirrors upstream xmlRegExecPushString contract: 1 = match complete,
4274/// 0 = more input needed, -1 = cannot continue (Misplaced).
4275///
4276/// # Safety
4277///
4278/// - `exec` must be non-NULL and a valid `ContentModelExec` created by
4279///   `new_content_exec`, with `nfa` pointing to a valid compiled
4280///   `ContentModelNfa`; `value` must be NULL (end of input) or a valid
4281///   null-terminated element name.
4282unsafe fn content_exec_push(exec: *mut ContentModelExec, value: *const xmlChar) -> c_int {
4283    unsafe {
4284        if exec.is_null() {
4285            return -1;
4286        }
4287        let nfa = &*(*exec).nfa;
4288        if value.is_null() {
4289            let cur = eps_closure(nfa, &(*exec).current);
4290            return if cur.iter().any(|&s| nfa.accept.contains(&s)) {
4291                1
4292            } else {
4293                0
4294            };
4295        }
4296        let mut next: Vec<u32> = Vec::new();
4297        for &s in &(*exec).current {
4298            for &(from, name, to) in &nfa.transitions {
4299                if from == s && !name.is_null() && string::xml_strcmp(name, value) == 0 {
4300                    next.push(to);
4301                }
4302            }
4303        }
4304        next.sort_unstable();
4305        next.dedup();
4306        if next.is_empty() {
4307            return -1;
4308        }
4309        let closed = eps_closure(nfa, &next);
4310        (*exec).current = closed;
4311        if (*exec).current.iter().any(|&s| nfa.accept.contains(&s)) {
4312            1
4313        } else {
4314            0
4315        }
4316    }
4317}
4318
4319/// Upstream vstateVPush: push a validation state for an open element.
4320///
4321/// # Safety
4322///
4323/// - `ctxt` must be a non-NULL valid `_xmlValidCtxt` with a consistent
4324///   `vstateTab`/`vstateNr`/`vstateMax` (an unallocated `vstateTab` is
4325///   allowed while `vstateMax` is 0; `xmlReallocImpl` grows it); `elem_decl`
4326///   may be NULL or a valid `_xmlElement` declaration; `node` may be NULL or
4327///   a valid `_xmlNode`. The pushed state must later be popped with
4328///   `vstate_vpop`.
4329unsafe fn vstate_vpush(
4330    ctxt: *mut _xmlValidCtxt,
4331    elem_decl: *mut _xmlElement,
4332    node: *mut _xmlNode,
4333) -> c_int {
4334    unsafe {
4335        if (*ctxt).vstateNr >= (*ctxt).vstateMax {
4336            let new_max = if (*ctxt).vstateMax == 0 {
4337                10
4338            } else {
4339                (*ctxt).vstateMax * 2
4340            };
4341            let new_tab = allocator::xmlReallocImpl(
4342                (*ctxt).vstateTab,
4343                (new_max as usize) * size_of::<ValidState>(),
4344            ) as *mut ValidState;
4345            if new_tab.is_null() {
4346                vctxt_error(
4347                    ctxt,
4348                    b"Memory allocation failed : xmlValidCtxt\0" as *const u8 as *const c_char,
4349                );
4350                return -1;
4351            }
4352            (*ctxt).vstateTab = new_tab as *mut c_void;
4353            (*ctxt).vstateMax = new_max;
4354        }
4355        let idx = (*ctxt).vstateNr as usize;
4356        let tab = (*ctxt).vstateTab as *mut ValidState;
4357        (*tab.add(idx)).elem_decl = elem_decl;
4358        (*tab.add(idx)).node = node;
4359        (*tab.add(idx)).exec = ptr::null_mut();
4360        if !elem_decl.is_null() && (*elem_decl).etype == XML_ELEMENT_TYPE_ELEMENT as c_int {
4361            if (*elem_decl).cont_model.is_null() {
4362                validate_build_content_model(ctxt, elem_decl);
4363            }
4364            if !(*elem_decl).cont_model.is_null() {
4365                let exec = new_content_exec((*elem_decl).cont_model as *mut ContentModelNfa);
4366                if exec.is_null() {
4367                    vctxt_error(
4368                        ctxt,
4369                        b"Memory allocation failed : xmlValidCtxt\0" as *const u8 as *const c_char,
4370                    );
4371                    return -1;
4372                }
4373                (*tab.add(idx)).exec = exec;
4374            } else {
4375                let msg = format!(
4376                    "Failed to build content model regexp for {}\0",
4377                    string::xmlstr_to_string((*elem_decl).name)
4378                );
4379                vctxt_error_node(ctxt, node, msg.as_ptr() as *const c_char);
4380            }
4381        }
4382        (*ctxt).vstate = tab.add(idx) as *mut c_void;
4383        (*ctxt).vstateNr += 1;
4384        0
4385    }
4386}
4387
4388/// Upstream vstateVPop: pop the current validation state, freeing its exec.
4389///
4390/// # Safety
4391///
4392/// - `ctxt` must be a non-NULL valid `_xmlValidCtxt` whose `vstateTab` is
4393///   non-NULL and whose `vstateNr` counts states previously pushed by
4394///   `vstate_vpush`; the exec stored in the popped slot is freed, so the
4395///   slot must not be used afterwards.
4396unsafe fn vstate_vpop(ctxt: *mut _xmlValidCtxt) -> c_int {
4397    unsafe {
4398        if (*ctxt).vstateNr < 1 {
4399            return -1;
4400        }
4401        (*ctxt).vstateNr -= 1;
4402        let idx = (*ctxt).vstateNr as usize;
4403        let tab = (*ctxt).vstateTab as *mut ValidState;
4404        let elem_decl = (*tab.add(idx)).elem_decl;
4405        (*tab.add(idx)).elem_decl = ptr::null_mut();
4406        (*tab.add(idx)).node = ptr::null_mut();
4407        if !elem_decl.is_null()
4408            && (*elem_decl).etype == XML_ELEMENT_TYPE_ELEMENT as c_int
4409            && !(*tab.add(idx)).exec.is_null()
4410        {
4411            free_content_exec((*tab.add(idx)).exec);
4412        }
4413        (*tab.add(idx)).exec = ptr::null_mut();
4414        if (*ctxt).vstateNr >= 1 {
4415            (*ctxt).vstate = tab.add((*ctxt).vstateNr as usize - 1) as *mut c_void;
4416        } else {
4417            (*ctxt).vstate = ptr::null_mut();
4418        }
4419        0
4420    }
4421}
4422
4423/// Upstream `xmlValidBuildContentModel(ctxt, elem)`: compile the element's
4424/// content tree into a content-model NFA cached in `elem->contModel`.
4425/// Returns 1 on success, 0 on failure.
4426///
4427/// # SAFETY
4428///
4429/// - `ctxt` may be NULL; `elem` a valid pointer.
4430pub unsafe fn validate_build_content_model(
4431    ctxt: *mut _xmlValidCtxt,
4432    elem: *mut _xmlElement,
4433) -> c_int {
4434    unsafe {
4435        if ctxt.is_null() {
4436            return 0;
4437        }
4438        if (*elem).type_ != XML_ELEMENT_DECL as c_int {
4439            return 0;
4440        }
4441        if (*elem).etype != XML_ELEMENT_TYPE_ELEMENT as c_int {
4442            return 1;
4443        }
4444        if !(*elem).cont_model.is_null() {
4445            return 1;
4446        }
4447        if (*elem).content.is_null() {
4448            return 1;
4449        }
4450        if content_has_pcdata((*elem).content) {
4451            let msg = format!(
4452                "Found PCDATA in content model of {}\0",
4453                string::xmlstr_to_string((*elem).name)
4454            );
4455            vctxt_error_node(ctxt, elem as *mut _xmlNode, msg.as_ptr() as *const c_char);
4456            return 0;
4457        }
4458        let nfa = build_content_nfa((*elem).content);
4459        if nfa.is_null() {
4460            vctxt_error(
4461                ctxt,
4462                b"Memory allocation failed : xmlValidBuildContentModel\0" as *const u8
4463                    as *const c_char,
4464            );
4465            return 0;
4466        }
4467        (*elem).cont_model = nfa as *mut c_void;
4468        1
4469    }
4470}
4471
4472/// Upstream `xmlValidatePushElement(ctxt, doc, elem, qname)`: validate a
4473/// start tag against the parent's content model and push the new element's
4474/// validation state.
4475///
4476/// # SAFETY
4477///
4478/// - `ctxt` may be NULL; `doc`/`elem`/`qname` valid pointers or NULL.
4479pub unsafe fn validate_push_element(
4480    ctxt: *mut _xmlValidCtxt,
4481    doc: *mut _xmlDoc,
4482    elem: *mut _xmlNode,
4483    qname: *const xmlChar,
4484) -> c_int {
4485    unsafe {
4486        let mut ret = 1;
4487        if ctxt.is_null() {
4488            return 0;
4489        }
4490        if (*ctxt).vstateNr > 0 && !(*ctxt).vstate.is_null() {
4491            let state = (*ctxt).vstate as *mut ValidState;
4492            let elem_decl = (*state).elem_decl;
4493            if !elem_decl.is_null() {
4494                match (*elem_decl).etype as u32 {
4495                    t if t == XML_ELEMENT_TYPE_UNDEFINED as u32 => ret = 0,
4496                    t if t == XML_ELEMENT_TYPE_EMPTY as u32 => {
4497                        let msg = format!(
4498                            "Element {} was declared EMPTY this one has content\0",
4499                            string::xmlstr_to_string((*(*state).node).name)
4500                        );
4501                        vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4502                        ret = 0;
4503                    }
4504                    t if t == XML_ELEMENT_TYPE_ANY as u32 => {}
4505                    t if t == XML_ELEMENT_TYPE_MIXED as u32 => {
4506                        if !(*elem_decl).content.is_null()
4507                            && (*(*elem_decl).content).type_ == XML_ELEMENT_CONTENT_PCDATA as c_int
4508                        {
4509                            let msg = format!(
4510                                "Element {} was declared #PCDATA but contains non text nodes\0",
4511                                string::xmlstr_to_string((*(*state).node).name)
4512                            );
4513                            vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4514                            ret = 0;
4515                        } else {
4516                            ret = validate_check_mixed(ctxt, (*elem_decl).content, qname);
4517                            if ret != 1 {
4518                                let msg = format!(
4519                                    "Element {} is not declared in {} list of possible children\0",
4520                                    string::xmlstr_to_string(qname),
4521                                    string::xmlstr_to_string((*(*state).node).name)
4522                                );
4523                                vctxt_error_node(
4524                                    ctxt,
4525                                    (*state).node,
4526                                    msg.as_ptr() as *const c_char,
4527                                );
4528                            }
4529                        }
4530                    }
4531                    t if t == XML_ELEMENT_TYPE_ELEMENT as u32 && !(*state).exec.is_null() => {
4532                        ret = content_exec_push((*state).exec, qname);
4533                        if ret < 0 {
4534                            let msg = format!(
4535                                "Element {} content does not follow the DTD, Misplaced {}\0",
4536                                string::xmlstr_to_string((*(*state).node).name),
4537                                string::xmlstr_to_string(qname)
4538                            );
4539                            vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4540                            ret = 0;
4541                        } else {
4542                            ret = 1;
4543                        }
4544                    }
4545                    _ => {}
4546                }
4547            }
4548        }
4549        let mut extsubset = 0;
4550        let e_decl = valid_get_elem_decl(ctxt, doc, elem, &mut extsubset);
4551        // upstream ignores the vstateVPush return here
4552        let _ = vstate_vpush(ctxt, e_decl, elem);
4553        ret
4554    }
4555}
4556
4557/// Upstream `xmlValidatePushCData(ctxt, data, len)`: character data is only
4558/// legal as whitespace inside ELEMENT content.
4559///
4560/// # SAFETY
4561///
4562/// - `ctxt` may be NULL; `data` a valid buffer of `len` bytes or NULL.
4563pub unsafe fn validate_push_cdata(
4564    ctxt: *mut _xmlValidCtxt,
4565    data: *const xmlChar,
4566    len: c_int,
4567) -> c_int {
4568    unsafe {
4569        let mut ret = 1;
4570        if ctxt.is_null() {
4571            return 0;
4572        }
4573        if len <= 0 {
4574            return 1;
4575        }
4576        if (*ctxt).vstateNr > 0 && !(*ctxt).vstate.is_null() {
4577            let state = (*ctxt).vstate as *mut ValidState;
4578            let elem_decl = (*state).elem_decl;
4579            if !elem_decl.is_null() {
4580                match (*elem_decl).etype as u32 {
4581                    t if t == XML_ELEMENT_TYPE_UNDEFINED as u32 => ret = 0,
4582                    t if t == XML_ELEMENT_TYPE_EMPTY as u32 => {
4583                        let msg = format!(
4584                            "Element {} was declared EMPTY this one has content\0",
4585                            string::xmlstr_to_string((*(*state).node).name)
4586                        );
4587                        vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4588                        ret = 0;
4589                    }
4590                    t if t == XML_ELEMENT_TYPE_ANY as u32 || t == XML_ELEMENT_TYPE_MIXED as u32 => {
4591                    }
4592                    t if t == XML_ELEMENT_TYPE_ELEMENT as u32 => {
4593                        let bytes = core::slice::from_raw_parts(data, len as usize);
4594                        for &b in bytes {
4595                            if !is_blank_byte(b) {
4596                                let msg = format!(
4597                                    "Element {} content does not follow the DTD, Text not allowed\0",
4598                                    string::xmlstr_to_string((*(*state).node).name)
4599                                );
4600                                vctxt_error_node(
4601                                    ctxt,
4602                                    (*state).node,
4603                                    msg.as_ptr() as *const c_char,
4604                                );
4605                                ret = 0;
4606                                break;
4607                            }
4608                        }
4609                    }
4610                    _ => {}
4611                }
4612            }
4613        }
4614        ret
4615    }
4616}
4617
4618/// Upstream `xmlValidatePopElement(ctxt, doc, elem, qname)`: verify the
4619/// parent content model completed and pop the validation state.
4620///
4621/// # SAFETY
4622///
4623/// - `ctxt` may be NULL; `doc`/`elem`/`qname` valid pointers or NULL.
4624pub unsafe fn validate_pop_element(
4625    ctxt: *mut _xmlValidCtxt,
4626    _doc: *mut _xmlDoc,
4627    _elem: *mut _xmlNode,
4628    _qname: *const xmlChar,
4629) -> c_int {
4630    unsafe {
4631        let mut ret = 1;
4632        if ctxt.is_null() {
4633            return 0;
4634        }
4635        if (*ctxt).vstateNr > 0 && !(*ctxt).vstate.is_null() {
4636            let state = (*ctxt).vstate as *mut ValidState;
4637            let elem_decl = (*state).elem_decl;
4638            if !elem_decl.is_null()
4639                && (*elem_decl).etype == XML_ELEMENT_TYPE_ELEMENT as c_int
4640                && !(*state).exec.is_null()
4641            {
4642                ret = content_exec_push((*state).exec, ptr::null());
4643                if ret <= 0 {
4644                    let msg = format!(
4645                        "Element {} content does not follow the DTD, Expecting more children\0",
4646                        string::xmlstr_to_string((*(*state).node).name)
4647                    );
4648                    vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4649                    ret = 0;
4650                } else {
4651                    ret = 1;
4652                }
4653            }
4654            let _ = vstate_vpop(ctxt);
4655        }
4656        ret
4657    }
4658}
4659
4660// ═══════════════════════════════════════════════════════════════════════════════
4661// Tests
4662// ═══════════════════════════════════════════════════════════════════════════════
4663
4664#[cfg(test)]
4665mod tests {
4666    use super::*;
4667    use crate::abi::allocator;
4668
4669    use crate::xml::dtd;
4670    use crate::xml::tree;
4671
4672    // ── Helpers ───────────────────────────────────────────────────────────
4673
4674    /// Create a null-terminated xmlChar* from a Rust string.
4675    unsafe fn c_str(s: &str) -> *const xmlChar {
4676        let bytes = s.as_bytes();
4677        let ptr = allocator::xmlMallocImpl(bytes.len() + 1) as *mut xmlChar;
4678        assert!(!ptr.is_null());
4679        std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len());
4680        *ptr.add(bytes.len()) = 0;
4681        ptr
4682    }
4683
4684    /// Create a simple document with a DTD for testing.
4685    unsafe fn make_test_doc() -> (*mut _xmlDoc, *mut _xmlDtd) {
4686        let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
4687        assert!(!doc.is_null());
4688
4689        let name = c_str("root");
4690        let ext_id = c_str("--//Test//DTD//EN");
4691        let sys_id = c_str("test.dtd");
4692        let dtd = dtd::create_int_subset(doc, name, ext_id, sys_id);
4693        assert!(!dtd.is_null());
4694
4695        (doc, dtd)
4696    }
4697
4698    /// Add an element declaration to a DTD.
4699    #[allow(unused)]
4700    unsafe fn add_elem_decl(
4701        dtd: *mut _xmlDtd,
4702        name: *const xmlChar,
4703        elem_type: c_int,
4704        content: *mut _xmlElementContent,
4705    ) -> *mut _xmlElement {
4706        dtd::add_element_decl(dtd, name, elem_type, content)
4707    }
4708
4709    /// Create a root element node.
4710    unsafe fn create_root_elem(doc: *mut _xmlDoc, name: *const xmlChar) -> *mut _xmlNode {
4711        let node = tree::new_node(ptr::null_mut(), name);
4712        assert!(!node.is_null());
4713        tree::add_child(doc as *mut _xmlNode, node);
4714        node
4715    }
4716
4717    /// Create a child element node.
4718    #[allow(unused)]
4719    unsafe fn create_child_elem(parent: *mut _xmlNode, name: *const xmlChar) -> *mut _xmlNode {
4720        let node = tree::new_node(ptr::null_mut(), name);
4721        assert!(!node.is_null());
4722        tree::add_child(parent, node);
4723        node
4724    }
4725
4726    // ── xmlValidateName tests ─────────────────────────────────────────────
4727
4728    /// Verify that `validate_name` accepts a NULL value and returns 0.
4729    ///
4730    /// # Safety
4731    ///
4732    /// - The only pointer passed is NULL; `validate_name` checks
4733    ///   `value.is_null()` before any dereference, so no pointer validity
4734    ///   requirement applies beyond the callee's NULL tolerance.
4735    #[test]
4736    fn test_validate_name_null() {
4737        unsafe {
4738            assert_eq!(validate_name(ptr::null()), 0);
4739        }
4740    }
4741
4742    /// Verify that `validate_name` rejects an empty string.
4743    ///
4744    /// # Safety
4745    ///
4746    /// - `s` points to a 1-byte stack array containing a NUL terminator;
4747    ///   `validate_name` reads it as a null-terminated string and the array
4748    ///   is live for the whole call.
4749    #[test]
4750    fn test_validate_name_empty() {
4751        unsafe {
4752            let s = b"\0" as *const u8 as *const xmlChar;
4753            assert_eq!(validate_name(s), 0);
4754        }
4755    }
4756
4757    /// Verify that valid XML Names are accepted by `validate_name`.
4758    ///
4759    /// # Safety
4760    ///
4761    /// - Each `s` is a heap allocation from `c_str` holding the
4762    ///   NUL-terminated test string, live for the `validate_name` call, and
4763    ///   freed exactly once with `xmlFreeImpl` before the loop moves on.
4764    #[test]
4765    fn test_validate_name_valid() {
4766        unsafe {
4767            let tests = ["foo", "_bar", ":baz", "hello-world", "ns:elem", "a123"];
4768            for t in &tests {
4769                let s = c_str(t);
4770                assert_eq!(validate_name(s), 1, "Expected '{}' to be a valid Name", t);
4771                allocator::xmlFreeImpl(s as *mut c_void);
4772            }
4773        }
4774    }
4775
4776    /// Verify that invalid XML Names are rejected by `validate_name`.
4777    ///
4778    /// # Safety
4779    ///
4780    /// - Each `s` is a heap allocation from `c_str` holding the
4781    ///   NUL-terminated test string, live for the `validate_name` call, and
4782    ///   freed exactly once with `xmlFreeImpl` before the loop moves on.
4783    #[test]
4784    fn test_validate_name_invalid() {
4785        unsafe {
4786            let tests = ["123abc", "-foo", ".bar", "foo bar", "a b"];
4787            for t in &tests {
4788                let s = c_str(t);
4789                assert_eq!(validate_name(s), 0, "Expected '{}' to be invalid", t);
4790                allocator::xmlFreeImpl(s as *mut c_void);
4791            }
4792        }
4793    }
4794
4795    /// Verify that a whitespace-separated list of Names validates.
4796    ///
4797    /// # Safety
4798    ///
4799    /// - `s` is a `c_str` allocation holding the NUL-terminated list, live
4800    ///   for the `validate_names` call and freed exactly once with
4801    ///   `xmlFreeImpl`.
4802    #[test]
4803    fn test_validate_names_valid() {
4804        unsafe {
4805            let s = c_str("foo bar baz");
4806            assert_eq!(validate_names(s), 1);
4807            allocator::xmlFreeImpl(s as *mut c_void);
4808        }
4809    }
4810
4811    /// Verify that a list containing an invalid Name is rejected.
4812    ///
4813    /// # Safety
4814    ///
4815    /// - `s` is a `c_str` allocation holding the NUL-terminated list, live
4816    ///   for the `validate_names` call and freed exactly once with
4817    ///   `xmlFreeImpl`.
4818    #[test]
4819    fn test_validate_names_invalid() {
4820        unsafe {
4821            let s = c_str("foo 123bar baz");
4822            assert_eq!(validate_names(s), 0);
4823            allocator::xmlFreeImpl(s as *mut c_void);
4824        }
4825    }
4826
4827    // ── xmlValidateNmtoken tests ──────────────────────────────────────────
4828
4829    /// Verify that `validate_nmtoken` accepts a NULL value and returns 0.
4830    ///
4831    /// # Safety
4832    ///
4833    /// - The only pointer passed is NULL; `validate_nmtoken` checks for NULL
4834    ///   before dereferencing, so no pointer validity requirement applies.
4835    #[test]
4836    fn test_validate_nmtoken_null() {
4837        unsafe {
4838            assert_eq!(validate_nmtoken(ptr::null()), 0);
4839        }
4840    }
4841
4842    /// Verify that valid NMTOKENs are accepted by `validate_nmtoken`.
4843    ///
4844    /// # Safety
4845    ///
4846    /// - Each `s` is a heap allocation from `c_str` holding the
4847    ///   NUL-terminated test string, live for the `validate_nmtoken` call,
4848    ///   and freed exactly once with `xmlFreeImpl`.
4849    #[test]
4850    fn test_validate_nmtoken_valid() {
4851        unsafe {
4852            let tests = ["foo", "123abc", "-foo", ".bar", "_test", ":ns"];
4853            for t in &tests {
4854                let s = c_str(t);
4855                assert_eq!(
4856                    validate_nmtoken(s),
4857                    1,
4858                    "Expected '{}' to be a valid NMTOKEN",
4859                    t
4860                );
4861                allocator::xmlFreeImpl(s as *mut c_void);
4862            }
4863        }
4864    }
4865
4866    /// Verify that a string with whitespace is not a valid NMTOKEN.
4867    ///
4868    /// # Safety
4869    ///
4870    /// - `s` is a `c_str` allocation holding the NUL-terminated string, live
4871    ///   for the `validate_nmtoken` call and freed exactly once with
4872    ///   `xmlFreeImpl`.
4873    #[test]
4874    fn test_validate_nmtoken_invalid() {
4875        unsafe {
4876            let s = c_str("foo bar");
4877            assert_eq!(validate_nmtoken(s), 0);
4878            allocator::xmlFreeImpl(s as *mut c_void);
4879        }
4880    }
4881
4882    /// Verify that a whitespace-separated list of NMTOKENs validates.
4883    ///
4884    /// # Safety
4885    ///
4886    /// - `s` is a `c_str` allocation holding the NUL-terminated list, live
4887    ///   for the `validate_nmtokens` call and freed exactly once with
4888    ///   `xmlFreeImpl`.
4889    #[test]
4890    fn test_validate_nmtokens_valid() {
4891        unsafe {
4892            let s = c_str("foo 123bar -baz");
4893            assert_eq!(validate_nmtokens(s), 1);
4894            allocator::xmlFreeImpl(s as *mut c_void);
4895        }
4896    }
4897
4898    // ── xmlValidateAttributeValue tests ───────────────────────────────────
4899
4900    /// Verify that CDATA attribute values always validate, including empty.
4901    ///
4902    /// # Safety
4903    ///
4904    /// - `s` is a `c_str` allocation (NUL-terminated, live for the call,
4905    ///   freed exactly once after); `empty` is a stack byte array with a NUL
4906    ///   terminator, live for the call; `validate_attribute_value` reads
4907    ///   both as null-terminated strings.
4908    #[test]
4909    fn test_validate_attribute_value_cdata() {
4910        unsafe {
4911            let s = c_str("anything goes here!@#$%^&*()");
4912            assert_eq!(validate_attribute_value(XML_ATTRIBUTE_CDATA as c_int, s), 1);
4913            allocator::xmlFreeImpl(s as *mut c_void);
4914
4915            // Empty CDATA is valid
4916            let empty = b"\0" as *const u8 as *const xmlChar;
4917            assert_eq!(
4918                validate_attribute_value(XML_ATTRIBUTE_CDATA as c_int, empty),
4919                1
4920            );
4921        }
4922    }
4923
4924    /// Verify that ID attribute values require a valid Name.
4925    ///
4926    /// # Safety
4927    ///
4928    /// - `valid` and `invalid` are `c_str` allocations holding
4929    ///   NUL-terminated strings, live for their calls and each freed exactly
4930    ///   once with `xmlFreeImpl`.
4931    #[test]
4932    fn test_validate_attribute_value_id() {
4933        unsafe {
4934            let valid = c_str("myId");
4935            assert_eq!(
4936                validate_attribute_value(XML_ATTRIBUTE_ID as c_int, valid),
4937                1
4938            );
4939            allocator::xmlFreeImpl(valid as *mut c_void);
4940
4941            let invalid = c_str("123id");
4942            assert_eq!(
4943                validate_attribute_value(XML_ATTRIBUTE_ID as c_int, invalid),
4944                0
4945            );
4946            allocator::xmlFreeImpl(invalid as *mut c_void);
4947        }
4948    }
4949
4950    /// Verify that a valid IDREF attribute value validates.
4951    ///
4952    /// # Safety
4953    ///
4954    /// - `valid` is a `c_str` allocation holding the NUL-terminated string,
4955    ///   live for the call and freed exactly once with `xmlFreeImpl`.
4956    #[test]
4957    fn test_validate_attribute_value_idref() {
4958        unsafe {
4959            let valid = c_str("someId");
4960            assert_eq!(
4961                validate_attribute_value(XML_ATTRIBUTE_IDREF as c_int, valid),
4962                1
4963            );
4964            allocator::xmlFreeImpl(valid as *mut c_void);
4965        }
4966    }
4967
4968    /// Verify that IDREFS values accept a valid list and reject bad Names.
4969    ///
4970    /// # Safety
4971    ///
4972    /// - `valid` and `invalid` are `c_str` allocations holding
4973    ///   NUL-terminated strings, live for their calls and each freed exactly
4974    ///   once with `xmlFreeImpl`.
4975    #[test]
4976    fn test_validate_attribute_value_idrefs() {
4977        unsafe {
4978            let valid = c_str("id1 id2 id3");
4979            assert_eq!(
4980                validate_attribute_value(XML_ATTRIBUTE_IDREFS as c_int, valid),
4981                1
4982            );
4983            allocator::xmlFreeImpl(valid as *mut c_void);
4984
4985            let invalid = c_str("id1 123id");
4986            assert_eq!(
4987                validate_attribute_value(XML_ATTRIBUTE_IDREFS as c_int, invalid),
4988                0
4989            );
4990            allocator::xmlFreeImpl(invalid as *mut c_void);
4991        }
4992    }
4993
4994    /// Verify that an ENTITY attribute value validates as a Name.
4995    ///
4996    /// # Safety
4997    ///
4998    /// - `valid` is a `c_str` allocation holding the NUL-terminated string,
4999    ///   live for the call and freed exactly once with `xmlFreeImpl`.
5000    #[test]
5001    fn test_validate_attribute_value_entity() {
5002        unsafe {
5003            let valid = c_str("myEntity");
5004            assert_eq!(
5005                validate_attribute_value(XML_ATTRIBUTE_ENTITY as c_int, valid),
5006                1
5007            );
5008            allocator::xmlFreeImpl(valid as *mut c_void);
5009        }
5010    }
5011
5012    /// Verify that NMTOKEN attribute values accept valid NMTOKENs and
5013    ///   reject whitespace.
5014    ///
5015    /// # Safety
5016    ///
5017    /// - `valid` and `invalid` are `c_str` allocations holding
5018    ///   NUL-terminated strings, live for their calls and each freed exactly
5019    ///   once with `xmlFreeImpl`.
5020    #[test]
5021    fn test_validate_attribute_value_nmtoken() {
5022        unsafe {
5023            let valid = c_str("123abc");
5024            assert_eq!(
5025                validate_attribute_value(XML_ATTRIBUTE_NMTOKEN as c_int, valid),
5026                1
5027            );
5028            allocator::xmlFreeImpl(valid as *mut c_void);
5029
5030            let invalid = c_str("foo bar");
5031            assert_eq!(
5032                validate_attribute_value(XML_ATTRIBUTE_NMTOKEN as c_int, invalid),
5033                0
5034            );
5035            allocator::xmlFreeImpl(invalid as *mut c_void);
5036        }
5037    }
5038
5039    /// Verify NULL attribute values: CDATA returns 1, ID returns 0.
5040    ///
5041    /// # Safety
5042    ///
5043    /// - Only NULL `value` pointers are passed; the callee checks for NULL
5044    ///   before dereferencing (CDATA returns 1, the ID path returns 0), so
5045    ///   no pointer validity requirement applies.
5046    #[test]
5047    fn test_validate_attribute_value_null() {
5048        unsafe {
5049            // UPSTREAM-PARITY: xmlValidateAttributeValueInternal's switch
5050            // breaks out of CDATA and returns 1 (valid.c 2.15.0), even for
5051            // a NULL value; unknown types also fall through to 1.
5052            assert_eq!(
5053                validate_attribute_value(XML_ATTRIBUTE_CDATA as c_int, ptr::null()),
5054                1
5055            );
5056            assert_eq!(
5057                validate_attribute_value(XML_ATTRIBUTE_ID as c_int, ptr::null()),
5058                0
5059            );
5060        }
5061    }
5062
5063    // ── xmlValidateEnumeration tests ──────────────────────────────────────
5064
5065    /// Verify that a value matching one enumeration entry validates.
5066    ///
5067    /// # Safety
5068    ///
5069    /// - `ctxt` is a valid `_xmlValidCtxt` from `new_valid_ctxt` (released
5070    ///   via `free_valid_ctxt` at the end); `e1` is a valid enumeration
5071    ///   chain of zero-initialized nodes whose `name` fields are `xml_strdup`
5072    ///   strings and whose `next` links are valid (last is NULL); `value` is
5073    ///   a NUL-terminated string; all stay live for the call.
5074    #[test]
5075    fn test_validate_enumeration_valid() {
5076        unsafe {
5077            let ctxt = new_valid_ctxt();
5078            assert!(!ctxt.is_null());
5079
5080            let red = c_str("red");
5081            let green = c_str("green");
5082            let blue = c_str("blue");
5083
5084            let e3 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>()) as *mut _xmlEnumeration;
5085            (*e3).name = string::xml_strdup(blue);
5086            (*e3).next = ptr::null_mut();
5087
5088            let e2 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>()) as *mut _xmlEnumeration;
5089            (*e2).name = string::xml_strdup(green);
5090            (*e2).next = e3;
5091
5092            let e1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>()) as *mut _xmlEnumeration;
5093            (*e1).name = string::xml_strdup(red);
5094            (*e1).next = e2;
5095
5096            let value = c_str("green");
5097            assert_eq!(validate_enumeration(ctxt, value, e1), 1);
5098            assert_eq!((*ctxt).valid, 1);
5099
5100            allocator::xmlFreeImpl(value as *mut c_void);
5101            allocator::xmlFreeImpl(red as *mut c_void);
5102            allocator::xmlFreeImpl(green as *mut c_void);
5103            allocator::xmlFreeImpl(blue as *mut c_void);
5104            free_valid_ctxt(ctxt);
5105        }
5106    }
5107
5108    /// Verify that a value matching no enumeration entry fails.
5109    ///
5110    /// # Safety
5111    ///
5112    /// - `ctxt` is a valid `_xmlValidCtxt` from `new_valid_ctxt`; `e1` is a
5113    ///   valid single enumeration node with a `xml_strdup` name and NULL
5114    ///   `next`; `value` is a NUL-terminated string; all stay live for the
5115    ///   call.
5116    #[test]
5117    fn test_validate_enumeration_invalid() {
5118        unsafe {
5119            let ctxt = new_valid_ctxt();
5120            assert!(!ctxt.is_null());
5121
5122            let e1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>()) as *mut _xmlEnumeration;
5123            (*e1).name = string::xml_strdup(b"red\0" as *const u8 as *const xmlChar);
5124            (*e1).next = ptr::null_mut();
5125
5126            let value = c_str("yellow");
5127            assert_eq!(validate_enumeration(ctxt, value, e1), 0);
5128
5129            allocator::xmlFreeImpl(value as *mut c_void);
5130            free_valid_ctxt(ctxt);
5131        }
5132    }
5133
5134    // ── xmlValidateNotationUse tests ──────────────────────────────────────
5135
5136    /// Verify that a declared notation is accepted by `validate_notation_use`.
5137    ///
5138    /// # Safety
5139    ///
5140    /// - `doc` and `dtd` come from `make_test_doc` and are valid and linked;
5141    ///   `notation_name` is a NUL-terminated `c_str` allocation; `ctxt` is a
5142    ///   valid `_xmlValidCtxt`; all remain live until the call completes and
5143    ///   are released with `free_valid_ctxt` and `tree::free_doc`.
5144    #[test]
5145    fn test_validate_notation_use_valid() {
5146        unsafe {
5147            let (doc, dtd) = make_test_doc();
5148
5149            let notation_name = c_str("GIF");
5150            dtd::add_notation_decl(dtd, notation_name, ptr::null(), ptr::null());
5151
5152            let ctxt = new_valid_ctxt();
5153            assert!(!ctxt.is_null());
5154
5155            assert_eq!(validate_notation_use(ctxt, doc, notation_name), 1);
5156
5157            free_valid_ctxt(ctxt);
5158            tree::free_doc(doc);
5159        }
5160    }
5161
5162    /// Verify that an undeclared notation is rejected.
5163    ///
5164    /// # Safety
5165    ///
5166    /// - `doc` comes from `make_test_doc` and is valid; `ctxt` is a valid
5167    ///   `_xmlValidCtxt`; `notation_name` is a NUL-terminated `c_str`
5168    ///   allocation freed after the call; all remain live until the call
5169    ///   completes.
5170    #[test]
5171    fn test_validate_notation_use_invalid() {
5172        unsafe {
5173            let (doc, _dtd) = make_test_doc();
5174
5175            let ctxt = new_valid_ctxt();
5176            assert!(!ctxt.is_null());
5177
5178            let notation_name = c_str("UNDECLARED");
5179            assert_eq!(validate_notation_use(ctxt, doc, notation_name), 0);
5180
5181            free_valid_ctxt(ctxt);
5182            allocator::xmlFreeImpl(notation_name as *mut c_void);
5183            tree::free_doc(doc);
5184        }
5185    }
5186
5187    // ── xmlNewValidCtxt / xmlFreeValidCtxt tests ─────────────────────────
5188
5189    /// Verify `new_valid_ctxt` initializes a context and `free_valid_ctxt`
5190    ///   releases it.
5191    ///
5192    /// # Safety
5193    ///
5194    /// - `ctxt` is a valid, zero-initialized `_xmlValidCtxt` returned by
5195    ///   `new_valid_ctxt`; it is dereferenced while live and freed exactly
5196    ///   once by `free_valid_ctxt`.
5197    #[test]
5198    fn test_new_free_valid_ctxt() {
5199        unsafe {
5200            let ctxt = new_valid_ctxt();
5201            assert!(!ctxt.is_null());
5202            assert_eq!((*ctxt).valid, 1);
5203            assert!((*ctxt).node.is_null());
5204            free_valid_ctxt(ctxt);
5205        }
5206    }
5207
5208    /// Verify `free_valid_ctxt` tolerates a NULL pointer.
5209    ///
5210    /// # Safety
5211    ///
5212    /// - NULL is passed to `free_valid_ctxt`, which returns immediately on a
5213    ///   NULL argument without dereferencing it.
5214    #[test]
5215    fn test_free_valid_ctxt_null() {
5216        unsafe {
5217            free_valid_ctxt(ptr::null_mut());
5218        }
5219    }
5220
5221    // ── xmlSetValidErrors tests ──────────────────────────────────────────
5222
5223    /// Verify `set_valid_errors` tolerates a NULL context.
5224    ///
5225    /// # Safety
5226    ///
5227    /// - NULL is passed for `ctxt`; `set_valid_errors` returns immediately
5228    ///   when `ctxt` is NULL without dereferencing it.
5229    #[test]
5230    fn test_set_valid_errors_null() {
5231        unsafe {
5232            set_valid_errors(ptr::null_mut(), None, None, ptr::null_mut());
5233        }
5234    }
5235
5236    // ── xmlValidateElement tests ──────────────────────────────────────────
5237
5238    /// Verify an element validates when the document has no DTD.
5239    ///
5240    /// # Safety
5241    ///
5242    /// - `doc` is a valid `_xmlDoc` from `tree::new_doc`; `root` is a valid
5243    ///   `_xmlNode` created by `create_root_elem` and attached to `doc`;
5244    ///   `ctxt` is a valid `_xmlValidCtxt`; all are released with
5245    ///   `free_valid_ctxt` and `tree::free_doc` after the call.
5246    #[test]
5247    fn test_validate_element_no_dtd() {
5248        unsafe {
5249            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5250            assert!(!doc.is_null());
5251
5252            let root_name = c_str("root");
5253            let root = create_root_elem(doc, root_name);
5254
5255            let ctxt = new_valid_ctxt();
5256            assert!(!ctxt.is_null());
5257
5258            // No DTD — validation passes (returns 1)
5259            assert_eq!(validate_element(ctxt, doc, root), 1);
5260
5261            free_valid_ctxt(ctxt);
5262            tree::free_doc(doc);
5263        }
5264    }
5265
5266    /// Verify an EMPTY-declared element validates.
5267    ///
5268    /// # Safety
5269    ///
5270    /// - `doc`/`dtd` come from `make_test_doc` and are valid and linked;
5271    ///   `root` is a valid `_xmlNode` attached to `doc`; `ctxt` is a valid
5272    ///   `_xmlValidCtxt`; all live for the call and are freed afterward.
5273    #[test]
5274    fn test_validate_element_empty_valid() {
5275        unsafe {
5276            let (doc, dtd) = make_test_doc();
5277
5278            let root_name = c_str("root");
5279            add_elem_decl(
5280                dtd,
5281                root_name,
5282                XML_ELEMENT_TYPE_EMPTY as c_int,
5283                ptr::null_mut(),
5284            );
5285
5286            let root = create_root_elem(doc, root_name);
5287
5288            let ctxt = new_valid_ctxt();
5289            assert!(!ctxt.is_null());
5290
5291            assert_eq!(validate_element(ctxt, doc, root), 1);
5292
5293            free_valid_ctxt(ctxt);
5294            tree::free_doc(doc);
5295        }
5296    }
5297
5298    /// Verify an undeclared element fails validation.
5299    ///
5300    /// # Safety
5301    ///
5302    /// - `doc`/`dtd` come from `make_test_doc` and are valid and linked;
5303    ///   `root` is a valid `_xmlNode` attached to `doc`; `ctxt` is a valid
5304    ///   `_xmlValidCtxt`; all live for the call and are freed afterward.
5305    #[test]
5306    fn test_validate_element_undeclared() {
5307        unsafe {
5308            let (doc, _dtd) = make_test_doc();
5309
5310            let root_name = c_str("root");
5311            let root = create_root_elem(doc, root_name);
5312
5313            let ctxt = new_valid_ctxt();
5314            assert!(!ctxt.is_null());
5315
5316            // Element not declared — validation fails
5317            assert_eq!(validate_element(ctxt, doc, root), 0);
5318
5319            free_valid_ctxt(ctxt);
5320            tree::free_doc(doc);
5321        }
5322    }
5323
5324    /// Verify an element whose content matches its `child+` model validates.
5325    ///
5326    /// # Safety
5327    ///
5328    /// - `doc`/`dtd` are valid and linked; `child_content` is a valid
5329    ///   `_xmlElementContent` tree created by `dtd::create_content_model`;
5330    ///   `root` and its child are valid `_xmlNode`s attached to `doc`;
5331    ///   `ctxt` is a valid `_xmlValidCtxt`; all live for the call and are
5332    ///   freed afterward.
5333    #[test]
5334    fn test_validate_element_with_content() {
5335        unsafe {
5336            let (doc, dtd) = make_test_doc();
5337
5338            // Create element declarations
5339            let root_name = c_str("root");
5340            let child_name = c_str("child");
5341
5342            // Root content model: child+
5343            let child_content =
5344                dtd::create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
5345            assert!(!child_content.is_null());
5346            (*child_content).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
5347
5348            add_elem_decl(
5349                dtd,
5350                root_name,
5351                XML_ELEMENT_TYPE_ELEMENT as c_int,
5352                child_content,
5353            );
5354            add_elem_decl(
5355                dtd,
5356                child_name,
5357                XML_ELEMENT_TYPE_EMPTY as c_int,
5358                ptr::null_mut(),
5359            );
5360
5361            let root = create_root_elem(doc, root_name);
5362            let _child = create_child_elem(root, child_name);
5363
5364            let ctxt = new_valid_ctxt();
5365            assert!(!ctxt.is_null());
5366
5367            assert_eq!(validate_element(ctxt, doc, root), 1);
5368
5369            free_valid_ctxt(ctxt);
5370            tree::free_doc(doc);
5371        }
5372    }
5373
5374    /// Verify an element whose children do not match its model fails.
5375    ///
5376    /// # Safety
5377    ///
5378    /// - `doc`/`dtd` are valid and linked; `child_content` is a valid
5379    ///   `_xmlElementContent` tree; `root` and the `wrong` child are valid
5380    ///   `_xmlNode`s attached to `doc`; `ctxt` is a valid `_xmlValidCtxt`;
5381    ///   all live for the call and are freed afterward.
5382    #[test]
5383    fn test_validate_element_invalid_content() {
5384        unsafe {
5385            let (doc, dtd) = make_test_doc();
5386
5387            let root_name = c_str("root");
5388            let child_name = c_str("child");
5389            let wrong_name = c_str("wrong");
5390
5391            // Root content model: child+
5392            let child_content =
5393                dtd::create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
5394            assert!(!child_content.is_null());
5395            (*child_content).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
5396
5397            add_elem_decl(
5398                dtd,
5399                root_name,
5400                XML_ELEMENT_TYPE_ELEMENT as c_int,
5401                child_content,
5402            );
5403            add_elem_decl(
5404                dtd,
5405                child_name,
5406                XML_ELEMENT_TYPE_EMPTY as c_int,
5407                ptr::null_mut(),
5408            );
5409            add_elem_decl(
5410                dtd,
5411                wrong_name,
5412                XML_ELEMENT_TYPE_EMPTY as c_int,
5413                ptr::null_mut(),
5414            );
5415
5416            let root = create_root_elem(doc, root_name);
5417            // Add "wrong" child instead of "child"
5418            create_child_elem(root, wrong_name);
5419
5420            let ctxt = new_valid_ctxt();
5421            assert!(!ctxt.is_null());
5422
5423            assert_eq!(validate_element(ctxt, doc, root), 0);
5424
5425            free_valid_ctxt(ctxt);
5426            tree::free_doc(doc);
5427        }
5428    }
5429
5430    // ── xmlValidateRoot tests ─────────────────────────────────────────────
5431
5432    /// Verify `validate_root` passes when the root matches the DTD.
5433    ///
5434    /// # Safety
5435    ///
5436    /// - `doc`/`dtd` are valid and linked; `root` is a valid `_xmlNode`
5437    ///   attached to `doc`; `ctxt` is a valid `_xmlValidCtxt`; all live for
5438    ///   the call and are released with `free_valid_ctxt` and `tree::free_doc`.
5439    #[test]
5440    fn test_validate_root_match() {
5441        unsafe {
5442            let (doc, dtd) = make_test_doc();
5443
5444            let root_name = c_str("root");
5445            add_elem_decl(
5446                dtd,
5447                root_name,
5448                XML_ELEMENT_TYPE_EMPTY as c_int,
5449                ptr::null_mut(),
5450            );
5451            create_root_elem(doc, root_name);
5452
5453            let ctxt = new_valid_ctxt();
5454            assert!(!ctxt.is_null());
5455
5456            assert_eq!(validate_root(ctxt, doc), 1);
5457
5458            free_valid_ctxt(ctxt);
5459            tree::free_doc(doc);
5460        }
5461    }
5462
5463    /// Verify `validate_root` passes when the document has no DTD.
5464    ///
5465    /// # Safety
5466    ///
5467    /// - `doc` is a valid `_xmlDoc` from `tree::new_doc`; `root` is a valid
5468    ///   `_xmlNode` attached to `doc`; `ctxt` is a valid `_xmlValidCtxt`;
5469    ///   all live for the call and are freed afterward.
5470    #[test]
5471    fn test_validate_root_no_dtd() {
5472        unsafe {
5473            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5474            assert!(!doc.is_null());
5475
5476            let root_name = c_str("root");
5477            create_root_elem(doc, root_name);
5478
5479            let ctxt = new_valid_ctxt();
5480            assert!(!ctxt.is_null());
5481
5482            // No DTD — passes
5483            assert_eq!(validate_root(ctxt, doc), 1);
5484
5485            free_valid_ctxt(ctxt);
5486            tree::free_doc(doc);
5487        }
5488    }
5489
5490    // ── xmlValidateDocument tests ─────────────────────────────────────────
5491
5492    /// Verify `validate_document` passes for a valid document.
5493    ///
5494    /// # Safety
5495    ///
5496    /// - `doc`/`dtd` are valid and linked; `root` is a valid `_xmlNode`
5497    ///   attached to `doc`; `ctxt` is a valid `_xmlValidCtxt`; all live for
5498    ///   the call and are freed afterward.
5499    #[test]
5500    fn test_validate_document_valid() {
5501        unsafe {
5502            let (doc, dtd) = make_test_doc();
5503
5504            let root_name = c_str("root");
5505            add_elem_decl(
5506                dtd,
5507                root_name,
5508                XML_ELEMENT_TYPE_EMPTY as c_int,
5509                ptr::null_mut(),
5510            );
5511            create_root_elem(doc, root_name);
5512
5513            let ctxt = new_valid_ctxt();
5514            assert!(!ctxt.is_null());
5515
5516            assert_eq!(validate_document(ctxt, doc), 1);
5517
5518            free_valid_ctxt(ctxt);
5519            tree::free_doc(doc);
5520        }
5521    }
5522
5523    /// Verify `validate_document` fails when the document has no root.
5524    ///
5525    /// # Safety
5526    ///
5527    /// - `doc` is a valid `_xmlDoc` from `tree::new_doc`; `ctxt` is a valid
5528    ///   `_xmlValidCtxt`; both stay live for the call and are freed
5529    ///   afterward.
5530    #[test]
5531    fn test_validate_document_no_root() {
5532        unsafe {
5533            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5534            assert!(!doc.is_null());
5535
5536            let ctxt = new_valid_ctxt();
5537            assert!(!ctxt.is_null());
5538
5539            assert_eq!(validate_document(ctxt, doc), 0);
5540
5541            free_valid_ctxt(ctxt);
5542            tree::free_doc(doc);
5543        }
5544    }
5545
5546    // ── xmlValidateContent tests ──────────────────────────────────────────
5547
5548    /// Verify `validate_content` passes when children match the model.
5549    ///
5550    /// # Safety
5551    ///
5552    /// - `doc`/`dtd` are valid and linked; `child_content` is a valid
5553    ///   `_xmlElementContent` tree; `root` and its child are valid `_xmlNode`s
5554    ///   attached to `doc`; `ctxt` is a valid `_xmlValidCtxt`; all remain
5555    ///   live for the call.
5556    #[test]
5557    fn test_validate_content_valid() {
5558        unsafe {
5559            let (doc, dtd) = make_test_doc();
5560
5561            let root_name = c_str("root");
5562            let child_name = c_str("child");
5563
5564            let child_content =
5565                dtd::create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
5566            assert!(!child_content.is_null());
5567
5568            add_elem_decl(
5569                dtd,
5570                root_name,
5571                XML_ELEMENT_TYPE_ELEMENT as c_int,
5572                child_content,
5573            );
5574            add_elem_decl(
5575                dtd,
5576                child_name,
5577                XML_ELEMENT_TYPE_EMPTY as c_int,
5578                ptr::null_mut(),
5579            );
5580
5581            let root = create_root_elem(doc, root_name);
5582            create_child_elem(root, child_name);
5583
5584            let ctxt = new_valid_ctxt();
5585            assert!(!ctxt.is_null());
5586
5587            assert_eq!(validate_content(ctxt, root, doc), 1);
5588
5589            free_valid_ctxt(ctxt);
5590            tree::free_doc(doc);
5591        }
5592    }
5593
5594    // ── xmlIsMixedElement / xmlIsEmptyElement tests ───────────────────────
5595
5596    /// Verify `is_mixed_element` detects MIXED declarations.
5597    ///
5598    /// # Safety
5599    ///
5600    /// - `doc`/`dtd` are valid and linked; `name` and `other` are
5601    ///   NUL-terminated `c_str` allocations live for their calls; `other` is
5602    ///   freed afterward; `doc` is freed via `tree::free_doc`.
5603    #[test]
5604    fn test_is_mixed_element() {
5605        unsafe {
5606            let (doc, dtd) = make_test_doc();
5607            let name = c_str("mixedElem");
5608            add_elem_decl(dtd, name, XML_ELEMENT_TYPE_MIXED as c_int, ptr::null_mut());
5609
5610            assert_eq!(is_mixed_element(doc, name), 1);
5611
5612            let other = c_str("other");
5613            assert_eq!(is_mixed_element(doc, other), 0);
5614
5615            allocator::xmlFreeImpl(other as *mut c_void);
5616            tree::free_doc(doc);
5617        }
5618    }
5619
5620    /// Verify `is_empty_element` detects EMPTY declarations.
5621    ///
5622    /// # Safety
5623    ///
5624    /// - `doc`/`dtd` are valid and linked; `name` and `other` are
5625    ///   NUL-terminated `c_str` allocations live for their calls; `other` is
5626    ///   freed afterward; `doc` is freed via `tree::free_doc`.
5627    #[test]
5628    fn test_is_empty_element() {
5629        unsafe {
5630            let (doc, dtd) = make_test_doc();
5631            let name = c_str("emptyElem");
5632            add_elem_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
5633
5634            assert_eq!(is_empty_element(doc, name), 1);
5635
5636            let other = c_str("other");
5637            assert_eq!(is_empty_element(doc, other), 0);
5638
5639            allocator::xmlFreeImpl(other as *mut c_void);
5640            tree::free_doc(doc);
5641        }
5642    }
5643
5644    /// Verify `is_mixed_element` returns 0 when there is no DTD.
5645    ///
5646    /// # Safety
5647    ///
5648    /// - `doc` is a valid `_xmlDoc`; `name` is a NUL-terminated `c_str`
5649    ///   allocation live for the call and freed afterward.
5650    #[test]
5651    fn test_is_mixed_element_no_dtd() {
5652        unsafe {
5653            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5654            assert!(!doc.is_null());
5655
5656            let name = c_str("foo");
5657            assert_eq!(is_mixed_element(doc, name), 0);
5658
5659            allocator::xmlFreeImpl(name as *mut c_void);
5660            tree::free_doc(doc);
5661        }
5662    }
5663
5664    /// Verify `is_empty_element` returns 0 when there is no DTD.
5665    ///
5666    /// # Safety
5667    ///
5668    /// - `doc` is a valid `_xmlDoc`; `name` is a NUL-terminated `c_str`
5669    ///   allocation live for the call and freed afterward.
5670    #[test]
5671    fn test_is_empty_element_no_dtd() {
5672        unsafe {
5673            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5674            assert!(!doc.is_null());
5675
5676            let name = c_str("foo");
5677            assert_eq!(is_empty_element(doc, name), 0);
5678
5679            allocator::xmlFreeImpl(name as *mut c_void);
5680            tree::free_doc(doc);
5681        }
5682    }
5683
5684    // ── xmlValidateDtd tests ──────────────────────────────────────────────
5685
5686    /// Verify `validate_dtd` tolerates NULL doc and dtd arguments.
5687    ///
5688    /// # Safety
5689    ///
5690    /// - `ctxt` is a valid `_xmlValidCtxt`; NULL is passed for `doc` and
5691    ///   `dtd`, which `validate_dtd` checks before dereferencing.
5692    #[test]
5693    fn test_validate_dtd_null() {
5694        unsafe {
5695            let ctxt = new_valid_ctxt();
5696            assert!(!ctxt.is_null());
5697            assert_eq!(validate_dtd(ctxt, ptr::null_mut(), ptr::null_mut()), 0);
5698            free_valid_ctxt(ctxt);
5699        }
5700    }
5701
5702    // ── Additional edge case tests ────────────────────────────────────────
5703
5704    /// Verify `validate_element` returns 0 for a NULL element.
5705    ///
5706    /// # Safety
5707    ///
5708    /// - `doc` is a valid `_xmlDoc`; `ctxt` is a valid `_xmlValidCtxt`;
5709    ///   NULL is passed for `elem`, which the callee checks before
5710    ///   dereferencing.
5711    #[test]
5712    fn test_validate_element_null() {
5713        unsafe {
5714            let (doc, _dtd) = make_test_doc();
5715            let ctxt = new_valid_ctxt();
5716            assert!(!ctxt.is_null());
5717
5718            assert_eq!(validate_element(ctxt, doc, ptr::null_mut()), 0);
5719
5720            free_valid_ctxt(ctxt);
5721            tree::free_doc(doc);
5722        }
5723    }
5724
5725    /// Verify `validate_document` tolerates NULL arguments.
5726    ///
5727    /// # Safety
5728    ///
5729    /// - `ctxt` is a valid `_xmlValidCtxt`; NULL is passed for `doc` and
5730    ///   also for `ctxt`, both of which the callee checks before
5731    ///   dereferencing.
5732    #[test]
5733    fn test_validate_document_null() {
5734        unsafe {
5735            let ctxt = new_valid_ctxt();
5736            assert!(!ctxt.is_null());
5737
5738            assert_eq!(validate_document(ctxt, ptr::null_mut()), 0);
5739            assert_eq!(validate_document(ptr::null_mut(), ptr::null_mut()), 0);
5740
5741            free_valid_ctxt(ctxt);
5742        }
5743    }
5744
5745    /// Verify `validate_document_final` tolerates NULL arguments.
5746    ///
5747    /// # Safety
5748    ///
5749    /// - `ctxt` is a valid `_xmlValidCtxt`; NULL is passed for `doc` and
5750    ///   also for `ctxt`, both of which the callee checks before
5751    ///   dereferencing.
5752    #[test]
5753    fn test_validate_document_final_null() {
5754        unsafe {
5755            let ctxt = new_valid_ctxt();
5756            assert!(!ctxt.is_null());
5757
5758            assert_eq!(validate_document_final(ctxt, ptr::null_mut()), 0);
5759            assert_eq!(validate_document_final(ptr::null_mut(), ptr::null_mut()), 0);
5760
5761            free_valid_ctxt(ctxt);
5762        }
5763    }
5764
5765    /// Verify `validate_attribute_decl` tolerates NULL arguments.
5766    ///
5767    /// # Safety
5768    ///
5769    /// - `ctxt` is a valid `_xmlValidCtxt`; NULL is passed for `doc` and
5770    ///   `attr`, which the callee checks before dereferencing.
5771    #[test]
5772    fn test_validate_attribute_decl_null() {
5773        unsafe {
5774            let ctxt = new_valid_ctxt();
5775            assert!(!ctxt.is_null());
5776
5777            assert_eq!(
5778                validate_attribute_decl(ctxt, ptr::null_mut(), ptr::null_mut()),
5779                0
5780            );
5781
5782            free_valid_ctxt(ctxt);
5783        }
5784    }
5785
5786    /// Verify `validate_content` tolerates NULL node and doc arguments.
5787    ///
5788    /// # Safety
5789    ///
5790    /// - `ctxt` is a valid `_xmlValidCtxt`; NULL is passed for `node` and
5791    ///   `doc`, which the callee checks before dereferencing.
5792    #[test]
5793    fn test_validate_content_null() {
5794        unsafe {
5795            let ctxt = new_valid_ctxt();
5796            assert!(!ctxt.is_null());
5797
5798            assert_eq!(validate_content(ctxt, ptr::null_mut(), ptr::null_mut()), 0);
5799
5800            free_valid_ctxt(ctxt);
5801        }
5802    }
5803
5804    /// Verify `validate_root` tolerates NULL arguments.
5805    ///
5806    /// # Safety
5807    ///
5808    /// - NULL is passed for both `ctxt` and `doc`; `validate_root` checks
5809    ///   them before dereferencing.
5810    #[test]
5811    fn test_validate_root_null() {
5812        unsafe {
5813            assert_eq!(validate_root(ptr::null_mut(), ptr::null_mut()), 0);
5814        }
5815    }
5816
5817    /// Verify `validate_enumeration` tolerates NULL value and tree.
5818    ///
5819    /// # Safety
5820    ///
5821    /// - `ctxt` is a valid `_xmlValidCtxt`; NULL is passed for `value` and
5822    ///   `tree`, which the callee checks before dereferencing.
5823    #[test]
5824    fn test_validate_enumeration_null() {
5825        unsafe {
5826            let ctxt = new_valid_ctxt();
5827            assert!(!ctxt.is_null());
5828
5829            assert_eq!(validate_enumeration(ctxt, ptr::null(), ptr::null_mut()), 0);
5830
5831            free_valid_ctxt(ctxt);
5832        }
5833    }
5834
5835    /// Verify `validate_notation_use` tolerates NULL arguments.
5836    ///
5837    /// # Safety
5838    ///
5839    /// - `ctxt` is a valid `_xmlValidCtxt`; NULL is passed for `doc` and
5840    ///   `notation_name`, which the callee checks before dereferencing.
5841    #[test]
5842    fn test_validate_notation_use_null() {
5843        unsafe {
5844            let ctxt = new_valid_ctxt();
5845            assert!(!ctxt.is_null());
5846
5847            assert_eq!(validate_notation_use(ctxt, ptr::null_mut(), ptr::null()), 0);
5848
5849            free_valid_ctxt(ctxt);
5850        }
5851    }
5852
5853    /// Verify names starting with Unicode letters validate.
5854    ///
5855    /// # Safety
5856    ///
5857    /// - `name` is a `c_str` heap allocation holding the NUL-terminated
5858    ///   UTF-8 string, live for the `validate_name` call and freed exactly
5859    ///   once with `xmlFreeImpl`.
5860    #[test]
5861    fn test_validate_name_start_characters() {
5862        unsafe {
5863            // Test some Unicode name characters
5864            let name = c_str("\u{C0}lph\u{E0}");
5865            assert_eq!(validate_name(name), 1);
5866            allocator::xmlFreeImpl(name as *mut c_void);
5867        }
5868    }
5869
5870    /// Verify a single-Name list validates.
5871    ///
5872    /// # Safety
5873    ///
5874    /// - `s` is a `c_str` allocation holding the NUL-terminated string, live
5875    ///   for the `validate_names` call and freed exactly once with
5876    ///   `xmlFreeImpl`.
5877    #[test]
5878    fn test_validate_names_single() {
5879        unsafe {
5880            let s = c_str("singleName");
5881            assert_eq!(validate_names(s), 1);
5882            allocator::xmlFreeImpl(s as *mut c_void);
5883        }
5884    }
5885
5886    /// Verify a single NMTOKEN list validates.
5887    ///
5888    /// # Safety
5889    ///
5890    /// - `s` is a `c_str` allocation holding the NUL-terminated string, live
5891    ///   for the `validate_nmtokens` call and freed exactly once with
5892    ///   `xmlFreeImpl`.
5893    #[test]
5894    fn test_validate_nmtokens_single() {
5895        unsafe {
5896            let s = c_str("123abc");
5897            assert_eq!(validate_nmtokens(s), 1);
5898            allocator::xmlFreeImpl(s as *mut c_void);
5899        }
5900    }
5901
5902    /// Verify NMTOKENS handling of tabs and invalid characters.
5903    ///
5904    /// # Safety
5905    ///
5906    /// - `s` and `s2` are `c_str` allocations holding NUL-terminated
5907    ///   strings, live for their calls and each freed exactly once with
5908    ///   `xmlFreeImpl`.
5909    #[test]
5910    fn test_validate_nmtokens_invalid() {
5911        unsafe {
5912            let s = c_str("foo\tbar"); // tab separated
5913            assert_eq!(validate_nmtokens(s), 1); // tab is whitespace
5914            allocator::xmlFreeImpl(s as *mut c_void);
5915
5916            // An NMTOKEN with invalid characters should fail
5917            let s2 = c_str("foo@bar");
5918            assert_eq!(validate_nmtokens(s2), 0);
5919            allocator::xmlFreeImpl(s2 as *mut c_void);
5920        }
5921    }
5922
5923    /// Verify non-CDATA attribute types reject an empty value.
5924    ///
5925    /// # Safety
5926    ///
5927    /// - `empty` is a 1-byte stack array with a NUL terminator, live for the
5928    ///   calls; `validate_attribute_value` reads it as a null-terminated
5929    ///   string.
5930    #[test]
5931    fn test_validate_attribute_value_empty_non_cdata() {
5932        unsafe {
5933            let empty = b"\0" as *const u8 as *const xmlChar;
5934            assert_eq!(
5935                validate_attribute_value(XML_ATTRIBUTE_ID as c_int, empty),
5936                0
5937            );
5938            assert_eq!(
5939                validate_attribute_value(XML_ATTRIBUTE_IDREF as c_int, empty),
5940                0
5941            );
5942            assert_eq!(
5943                validate_attribute_value(XML_ATTRIBUTE_NMTOKEN as c_int, empty),
5944                0
5945            );
5946        }
5947    }
5948
5949    /// Verify an unknown attribute type falls through to a valid result.
5950    ///
5951    /// # Safety
5952    ///
5953    /// - `s` is a `c_str` allocation holding the NUL-terminated string, live
5954    ///   for the call and freed exactly once with `xmlFreeImpl`.
5955    #[test]
5956    fn test_validate_attribute_value_unknown_type() {
5957        unsafe {
5958            // UPSTREAM-PARITY: unknown attribute types fall through to the
5959            // default return of 1 (valid.c xmlValidateAttributeValueInternal).
5960            let s = c_str("test");
5961            assert_eq!(validate_attribute_value(999, s), 1);
5962            allocator::xmlFreeImpl(s as *mut c_void);
5963        }
5964    }
5965
5966    /// Verify ANY content models accept any children.
5967    ///
5968    /// # Safety
5969    ///
5970    /// - `doc`/`dtd` are valid and linked; `root` and `child` are valid
5971    ///   `_xmlNode`s attached to `doc`; `ctxt` is a valid `_xmlValidCtxt`;
5972    ///   all live for the call and are freed afterward.
5973    #[test]
5974    fn test_validate_element_any_content() {
5975        unsafe {
5976            let (doc, dtd) = make_test_doc();
5977
5978            let root_name = c_str("root");
5979            add_elem_decl(
5980                dtd,
5981                root_name,
5982                XML_ELEMENT_TYPE_ANY as c_int,
5983                ptr::null_mut(),
5984            );
5985
5986            let child_name = c_str("child");
5987            add_elem_decl(
5988                dtd,
5989                child_name,
5990                XML_ELEMENT_TYPE_EMPTY as c_int,
5991                ptr::null_mut(),
5992            );
5993
5994            let root = create_root_elem(doc, root_name);
5995            create_child_elem(root, child_name);
5996
5997            let ctxt = new_valid_ctxt();
5998            assert!(!ctxt.is_null());
5999
6000            // ANY content allows any children
6001            assert_eq!(validate_element(ctxt, doc, root), 1);
6002
6003            free_valid_ctxt(ctxt);
6004            tree::free_doc(doc);
6005        }
6006    }
6007
6008    /// Verify an EMPTY element with a child fails validation.
6009    ///
6010    /// # Safety
6011    ///
6012    /// - `doc`/`dtd` are valid and linked; `root` and `child` are valid
6013    ///   `_xmlNode`s attached to `doc`; `ctxt` is a valid `_xmlValidCtxt`;
6014    ///   all live for the call and are freed afterward.
6015    #[test]
6016    fn test_validate_element_empty_with_child() {
6017        unsafe {
6018            let (doc, dtd) = make_test_doc();
6019
6020            let root_name = c_str("root");
6021            add_elem_decl(
6022                dtd,
6023                root_name,
6024                XML_ELEMENT_TYPE_EMPTY as c_int,
6025                ptr::null_mut(),
6026            );
6027
6028            let child_name = c_str("child");
6029            add_elem_decl(
6030                dtd,
6031                child_name,
6032                XML_ELEMENT_TYPE_EMPTY as c_int,
6033                ptr::null_mut(),
6034            );
6035
6036            let root = create_root_elem(doc, root_name);
6037            create_child_elem(root, child_name);
6038
6039            let ctxt = new_valid_ctxt();
6040            assert!(!ctxt.is_null());
6041
6042            // EMPTY element with child — validation fails
6043            assert_eq!(validate_element(ctxt, doc, root), 0);
6044
6045            free_valid_ctxt(ctxt);
6046            tree::free_doc(doc);
6047        }
6048    }
6049
6050    /// Verify `validate_dtd_final` tolerates NULL arguments.
6051    ///
6052    /// # Safety
6053    ///
6054    /// - NULL is passed for both `ctxt` and `doc`; `validate_dtd_final`
6055    ///   forwards to `validate_document_final`, which checks them before
6056    ///   dereferencing.
6057    #[test]
6058    fn test_validate_dtd_final_null() {
6059        unsafe {
6060            assert_eq!(validate_dtd_final(ptr::null_mut(), ptr::null_mut()), 0);
6061        }
6062    }
6063}