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