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