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