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::structs::*;
35use crate::abi::types::xmlAttributeDefault::*;
36use crate::abi::types::xmlAttributeType::*;
37use crate::abi::types::xmlElementContentOccur::*;
38use crate::abi::types::xmlElementContentType::*;
39use crate::abi::types::xmlElementType::*;
40use crate::abi::types::xmlElementTypeVal::*;
41use crate::abi::types::xmlEntityType::*;
42use crate::abi::types::*;
43use crate::xml::dtd;
44use crate::xml::hash;
45use crate::xml::string;
46use crate::xml::tree;
47
48// ═══════════════════════════════════════════════════════════════════════════════
49// Constants
50// ═══════════════════════════════════════════════════════════════════════════════
51
52/// Maximum allowed depth for recursive validation walks.
53#[allow(dead_code)]
54const VALID_CTXT_DEPTH_MAX: c_int = 256;
55
56// ═══════════════════════════════════════════════════════════════════════════════
57// Validation Context
58// ═══════════════════════════════════════════════════════════════════════════════
59
60/// Create a new validation context.
61///
62/// # UPSTREAM-PARITY
63///
64/// ```c
65/// xmlValidCtxtPtr xmlNewValidCtxt(void);
66/// ```
67///
68/// Returns a new zero-initialized validation context, or NULL on OOM.
69///
70/// # SAFETY
71///
72/// The function touches crate-global state only; it is safe
73/// as long as the caller respects the library's global
74/// initialization/cleanup ordering (xmlInitParser before use,
75/// xmlCleanupParser only after all users are done).
76///
77/// Violating the global lifecycle ordering, or calling this after
78/// teardown or from a signal handler, is undefined behavior.
79pub unsafe fn new_valid_ctxt() -> *mut _xmlValidCtxt {
80    // SAFETY: Allocate zero-initialized memory for the validation context.
81    let ctxt = allocator::xmlMallocZero(size_of::<_xmlValidCtxt>() as usize) as *mut _xmlValidCtxt;
82    if ctxt.is_null() {
83        return ptr::null_mut();
84    }
85
86    unsafe {
87        (*ctxt).valid = 1;
88        (*ctxt).node = ptr::null_mut();
89        (*ctxt).doc = ptr::null_mut();
90        (*ctxt).nodeNr = 0;
91        (*ctxt).nodeMax = 0;
92        (*ctxt).nodeTab = ptr::null_mut();
93        (*ctxt).flags = 0;
94        (*ctxt).vstate = ptr::null_mut();
95        (*ctxt).vstateNr = 0;
96        (*ctxt).vstateMax = 0;
97        (*ctxt).vstateTab = ptr::null_mut();
98        (*ctxt).am = ptr::null_mut();
99        (*ctxt).state = ptr::null_mut();
100        (*ctxt).error = None;
101        (*ctxt).warning = None;
102        (*ctxt).userData = ptr::null_mut();
103    }
104
105    ctxt
106}
107
108/// Free a validation context.
109///
110/// # UPSTREAM-PARITY
111///
112/// ```c
113/// void xmlFreeValidCtxt(xmlValidCtxtPtr ctxt);
114/// ```
115///
116/// # SAFETY
117///
118/// - `ctxt` must be a valid pointer to an _xmlValidCtxt, or NULL.
119pub unsafe fn free_valid_ctxt(ctxt: *mut _xmlValidCtxt) {
120    if ctxt.is_null() {
121        return;
122    }
123
124    unsafe {
125        let c = &mut *ctxt;
126
127        // Free node stack
128        if !c.nodeTab.is_null() {
129            allocator::xmlFreeImpl(c.nodeTab as *mut c_void);
130        }
131
132        // Free automata
133        if !c.am.is_null() {
134            // Automata free — currently a no-op since am is opaque.
135            // UPSTREAM-PARITY: xmlFreeAutomata(c.am) in upstream.
136        }
137
138        // Free state
139        if !c.state.is_null() {
140            // State free — currently a no-op.
141        }
142
143        allocator::xmlFreeImpl(ctxt as *mut c_void);
144    }
145}
146
147/// Set error and warning callbacks on a validation context.
148///
149/// # UPSTREAM-PARITY
150///
151/// ```c
152/// void xmlSetValidErrors(xmlValidCtxtPtr ctxt,
153///                        xmlGenericErrorFunc err,
154///                        xmlGenericErrorFunc warn,
155///                        void *data);
156/// ```
157///
158/// # SAFETY
159///
160/// - `ctxt` may be NULL (no-op).
161/// - `err`, `warn`, `data` may be NULL.
162pub unsafe fn set_valid_errors(
163    ctxt: *mut _xmlValidCtxt,
164    err: Option<xmlGenericErrorFunc>,
165    warn: Option<xmlGenericErrorFunc>,
166    data: *mut c_void,
167) {
168    if ctxt.is_null() {
169        return;
170    }
171
172    unsafe {
173        // UPSTREAM-PARITY: libxml2 stores these as xmlValidityErrorFunc
174        // but accepts xmlGenericErrorFunc in the setter.
175        (*ctxt).error = err;
176        (*ctxt).warning = warn;
177        (*ctxt).userData = data;
178    }
179}
180
181// ═══════════════════════════════════════════════════════════════════════════════
182// Internal helpers
183// ═══════════════════════════════════════════════════════════════════════════════
184
185/// Report a validation error through the context.
186///
187/// # SAFETY
188///
189/// - `ctxt` may be NULL.
190/// - `msg` must be a valid null-terminated C string.
191unsafe fn vctxt_error(ctxt: *mut _xmlValidCtxt, msg: *const c_char) {
192    if ctxt.is_null() {
193        return;
194    }
195    unsafe {
196        let c = &mut *ctxt;
197        c.valid = 0;
198        if let Some(err) = c.error {
199            err(c.userData, msg);
200        }
201    }
202}
203
204/// Push a node onto the validation context's node stack.
205///
206/// Returns 0 on success, -1 on failure.
207///
208/// # SAFETY
209///
210/// - `ctxt` must be a valid pointer.
211unsafe fn vctxt_push_node(ctxt: *mut _xmlValidCtxt, node: *mut _xmlNode) -> c_int {
212    unsafe {
213        let c = &mut *ctxt;
214
215        if c.nodeNr >= c.nodeMax {
216            let new_max = if c.nodeMax == 0 { 4 } else { c.nodeMax * 2 };
217            let new_tab = allocator::xmlReallocImpl(
218                c.nodeTab as *mut c_void,
219                (new_max as usize) * size_of::<*mut _xmlNode>(),
220            ) as *mut *mut _xmlNode;
221            if new_tab.is_null() {
222                return -1;
223            }
224            c.nodeTab = new_tab;
225            c.nodeMax = new_max;
226        }
227
228        *c.nodeTab.add(c.nodeNr as usize) = node;
229        c.nodeNr += 1;
230        c.node = node;
231    }
232    0
233}
234
235/// Pop a node from the validation context's node stack.
236///
237/// # SAFETY
238///
239/// - `ctxt` must be a valid pointer.
240unsafe fn vctxt_pop_node(ctxt: *mut _xmlValidCtxt) {
241    unsafe {
242        let c = &mut *ctxt;
243        if c.nodeNr > 0 {
244            c.nodeNr -= 1;
245        }
246        if c.nodeNr > 0 {
247            c.node = *c.nodeTab.add((c.nodeNr - 1) as usize);
248        } else {
249            c.node = ptr::null_mut();
250        }
251    }
252}
253
254/// Get the DTD to validate against for a given document.
255///
256/// Returns the internal subset first, then the external subset.
257///
258/// # SAFETY
259///
260/// - `doc` must be a valid pointer or NULL.
261unsafe fn get_valid_dtd(doc: *mut _xmlDoc) -> *mut _xmlDtd {
262    if doc.is_null() {
263        return ptr::null_mut();
264    }
265    unsafe {
266        let d = &*doc;
267        if !d.intSubset.is_null() {
268            d.intSubset
269        } else {
270            d.extSubset
271        }
272    }
273}
274
275// ═══════════════════════════════════════════════════════════════════════════════
276// XML Name / NMTOKEN Character Classification
277// ═══════════════════════════════════════════════════════════════════════════════
278
279/// Check if a character is a valid XML Name start character.
280///
281/// # UPSTREAM-PARITY
282///
283/// Matches the XML 1.0 Fifth Edition NameStartChar production:
284/// `[a-zA-Z_:] | [\xC0-\xD6] | [\xD8-\xF6] | [\xF8-\u{2FF}] |
285///  [\u{370}-\u{37D}] | [\u{37F}-\u{1FFF}] | [\u{200C}-\u{200D}] |
286///  [\u{2070}-\u{218F}] | [\u{2C00}-\u{2FEF}] | [\u{3001}-\u{D7FF}] |
287///  [\u{F900}-\u{FDCF}] | [\u{FDF0}-\u{FFFD}]`
288pub(crate) const fn is_xml_name_start(c: char) -> bool {
289    matches!(c,
290        'a'..='z' | 'A'..='Z' | '_' | ':' |
291        '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{2FF}' |
292        '\u{370}'..='\u{37D}' | '\u{37F}'..='\u{1FFF}' |
293        '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}' |
294        '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' |
295        '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' |
296        '\u{10000}'..='\u{EFFFF}'
297    )
298}
299
300/// Check if a character is a valid XML Name character.
301///
302/// # UPSTREAM-PARITY
303///
304/// Matches NameChar production: NameStartChar | '-' | '.' | [0-9] |
305/// \u{B7} | [\u{0300}-\u{036F}] | [\u{203F}-\u{2040}]
306pub(crate) const fn is_xml_name_char(c: char) -> bool {
307    is_xml_name_start(c)
308        || matches!(c,
309            '-' | '.' | '0'..='9' | '\u{B7}' |
310            '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
311        )
312}
313
314// ═══════════════════════════════════════════════════════════════════════════════
315// xmlValidateName / xmlValidateNames
316// ═══════════════════════════════════════════════════════════════════════════════
317
318/// Validate whether `value` is a valid XML Name.
319///
320/// # UPSTREAM-PARITY
321///
322/// ```c
323/// int xmlValidateName(const xmlChar *value);
324/// ```
325///
326/// Returns 1 if valid, 0 if not.
327///
328/// # SAFETY
329///
330/// - `value` must be a valid null-terminated string or NULL.
331pub unsafe fn validate_name(value: *const xmlChar) -> c_int {
332    if value.is_null() {
333        return 0;
334    }
335
336    let s = unsafe { string::xmlstr_to_bytes(value) };
337    let s = core::str::from_utf8(s).unwrap_or("");
338
339    if s.is_empty() {
340        return 0;
341    }
342
343    let mut chars = s.chars();
344
345    // First character must be a NameStartChar
346    match chars.next() {
347        Some(c) if is_xml_name_start(c) => {}
348        _ => return 0,
349    }
350
351    // Remaining characters must be NameChars
352    for c in chars {
353        if !is_xml_name_char(c) {
354            return 0;
355        }
356    }
357
358    1
359}
360
361/// Validate whether `value` is a whitespace-separated list of XML Names.
362///
363/// # UPSTREAM-PARITY
364///
365/// ```c
366/// int xmlValidateNames(const xmlChar *value);
367/// ```
368///
369/// Returns 1 if valid, 0 if not.
370///
371/// # SAFETY
372///
373/// - `value` must be a valid null-terminated string or NULL.
374pub unsafe fn validate_names(value: *const xmlChar) -> c_int {
375    if value.is_null() {
376        return 0;
377    }
378
379    let s = unsafe { string::xmlstr_to_bytes(value) };
380    let s = core::str::from_utf8(s).unwrap_or("");
381
382    if s.is_empty() {
383        return 0;
384    }
385
386    for token in s.split_whitespace() {
387        if token.is_empty() {
388            return 0;
389        }
390        let mut chars = token.chars();
391        match chars.next() {
392            Some(c) if is_xml_name_start(c) => {}
393            _ => return 0,
394        }
395        for c in chars {
396            if !is_xml_name_char(c) {
397                return 0;
398            }
399        }
400    }
401
402    1
403}
404
405// ═══════════════════════════════════════════════════════════════════════════════
406// xmlValidateNmtoken / xmlValidateNmtokens
407// ═══════════════════════════════════════════════════════════════════════════════
408
409/// Validate whether `value` is a valid XML NMTOKEN.
410///
411/// # UPSTREAM-PARITY
412///
413/// ```c
414/// int xmlValidateNmtoken(const xmlChar *value);
415/// ```
416///
417/// An NMTOKEN is like a Name but the first character can also be a NameChar
418/// (not just a NameStartChar). Returns 1 if valid, 0 if not.
419///
420/// # SAFETY
421///
422/// - `value` must be a valid null-terminated string or NULL.
423pub unsafe fn validate_nmtoken(value: *const xmlChar) -> c_int {
424    if value.is_null() {
425        return 0;
426    }
427
428    let s = unsafe { string::xmlstr_to_bytes(value) };
429    let s = core::str::from_utf8(s).unwrap_or("");
430
431    if s.is_empty() {
432        return 0;
433    }
434
435    for c in s.chars() {
436        if !is_xml_name_char(c) {
437            return 0;
438        }
439    }
440
441    1
442}
443
444/// Validate whether `value` is a whitespace-separated list of XML NMTOKENs.
445///
446/// # UPSTREAM-PARITY
447///
448/// ```c
449/// int xmlValidateNmtokens(const xmlChar *value);
450/// ```
451///
452/// Returns 1 if valid, 0 if not.
453///
454/// # SAFETY
455///
456/// - `value` must be a valid null-terminated string or NULL.
457pub unsafe fn validate_nmtokens(value: *const xmlChar) -> c_int {
458    if value.is_null() {
459        return 0;
460    }
461
462    let s = unsafe { string::xmlstr_to_bytes(value) };
463    let s = core::str::from_utf8(s).unwrap_or("");
464
465    if s.is_empty() {
466        return 0;
467    }
468
469    for token in s.split_whitespace() {
470        if token.is_empty() {
471            return 0;
472        }
473        for c in token.chars() {
474            if !is_xml_name_char(c) {
475                return 0;
476            }
477        }
478    }
479
480    1
481}
482
483// ═══════════════════════════════════════════════════════════════════════════════
484// xmlValidateAttributeValue
485// ═══════════════════════════════════════════════════════════════════════════════
486
487/// Validate an attribute value against its declared type.
488///
489/// # UPSTREAM-PARITY
490///
491/// ```c
492/// int xmlValidateAttributeValue(int type, const xmlChar *value);
493/// ```
494///
495/// Returns 1 if the value is valid for the given attribute type, 0 otherwise.
496///
497/// # SAFETY
498///
499/// - `value` must be a valid null-terminated string or NULL.
500pub unsafe fn validate_attribute_value(atype: c_int, value: *const xmlChar) -> c_int {
501    // UPSTREAM-PARITY: upstream xmlValidateAttributeValue dispatches to
502    // xmlValidateAttributeValueInternal(NULL, type, value) whose switch
503    // matches this exactly; CDATA (and unknown types) fall through to 1.
504    match atype as u32 {
505        t if t == XML_ATTRIBUTE_ENTITIES as u32 || t == XML_ATTRIBUTE_IDREFS as u32 => {
506            validate_values_internal(value, 0)
507        }
508        t if t == XML_ATTRIBUTE_ENTITY as u32
509            || t == XML_ATTRIBUTE_IDREF as u32
510            || t == XML_ATTRIBUTE_ID as u32
511            || t == XML_ATTRIBUTE_NOTATION as u32 =>
512        {
513            validate_value_internal(value, 0)
514        }
515        t if t == XML_ATTRIBUTE_NMTOKENS as u32 || t == XML_ATTRIBUTE_ENUMERATION as u32 => {
516            validate_values_internal(value, XML_SCAN_NMTOKEN)
517        }
518        t if t == XML_ATTRIBUTE_NMTOKEN as u32 => validate_value_internal(value, XML_SCAN_NMTOKEN),
519        _ => 1, // CDATA / unknown
520    }
521}
522
523// ═══════════════════════════════════════════════════════════════════════════════
524// xmlValidateEnumeration
525// ═══════════════════════════════════════════════════════════════════════════════
526
527/// Validate that `value` is one of the values in the enumeration.
528///
529/// # UPSTREAM-PARITY
530///
531/// ```c
532/// int xmlValidateEnumeration(xmlValidCtxtPtr ctxt,
533///                            const xmlChar *value,
534///                            xmlEnumerationPtr tree);
535/// ```
536///
537/// Returns 1 if the value is in the enumeration, 0 otherwise.
538///
539/// # SAFETY
540///
541/// - `ctxt` may be NULL.
542/// - `value` must be a valid null-terminated string or NULL.
543/// - `tree` may be NULL (returns 0).
544pub unsafe fn validate_enumeration(
545    ctxt: *mut _xmlValidCtxt,
546    value: *const xmlChar,
547    tree: *mut _xmlEnumeration,
548) -> c_int {
549    if value.is_null() || tree.is_null() {
550        return 0;
551    }
552
553    let mut cur = tree;
554    while !cur.is_null() {
555        unsafe {
556            if string::xml_strcmp(value, (*cur).name) == 0 {
557                return 1;
558            }
559            cur = (*cur).next;
560        }
561    }
562
563    // Value not found in enumeration
564    unsafe {
565        let msg = string::xmlstr_to_string(value);
566        let err_msg = format!("Value '{}' is not a valid enumeration value\0", msg);
567        vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
568    }
569    0
570}
571
572// ═══════════════════════════════════════════════════════════════════════════════
573// xmlValidateNotationUse
574// ═══════════════════════════════════════════════════════════════════════════════
575
576/// Validate that `notationName` is a declared notation in the document's DTD.
577///
578/// # UPSTREAM-PARITY
579///
580/// ```c
581/// int xmlValidateNotationUse(xmlValidCtxtPtr ctxt,
582///                            xmlDocPtr doc,
583///                            const xmlChar *notationName);
584/// ```
585///
586/// Returns 1 if the notation is declared, 0 otherwise.
587///
588/// # SAFETY
589///
590/// - `ctxt`, `doc`, `notationName` may be NULL.
591pub unsafe fn validate_notation_use(
592    ctxt: *mut _xmlValidCtxt,
593    doc: *mut _xmlDoc,
594    notation_name: *const xmlChar,
595) -> c_int {
596    if notation_name.is_null() {
597        return 0;
598    }
599
600    let dtd = unsafe { get_valid_dtd(doc) };
601    if dtd.is_null() {
602        unsafe {
603            vctxt_error(
604                ctxt,
605                b"No DTD available for notation validation\0" as *const u8 as *const c_char,
606            );
607        }
608        return 0;
609    }
610
611    // Look up the notation in the DTD's notation hash table
612    unsafe {
613        let notations = (*dtd).notations;
614        if notations.is_null() {
615            vctxt_error(
616                ctxt,
617                b"No notations declared in DTD\0" as *const u8 as *const c_char,
618            );
619            return 0;
620        }
621
622        let notation = hash::hash_lookup(notations as *mut hash::HashTable, notation_name);
623        if notation.is_null() {
624            let msg = string::xmlstr_to_string(notation_name);
625            let err_msg = format!("Notation '{}' is not declared\0", msg);
626            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
627            return 0;
628        }
629    }
630
631    1
632}
633
634// ═══════════════════════════════════════════════════════════════════════════════
635// xmlValidateID / xmlValidateIDRef / xmlValidateIDRefs
636// ═══════════════════════════════════════════════════════════════════════════════
637
638/// Validate an ID value: check that the value is a valid XML Name and
639/// that no duplicate ID values exist in the document.
640///
641/// # UPSTREAM-PARITY
642///
643/// ```c
644/// int xmlValidateID(xmlValidCtxtPtr ctxt,
645///                   xmlDocPtr doc,
646///                   xmlNodePtr node,
647///                   const xmlChar *value);
648/// ```
649///
650/// Returns 1 if the ID is valid, 0 otherwise.
651///
652/// # SAFETY
653///
654/// - `ctxt`, `doc`, `node`, `value` may be NULL.
655pub unsafe fn validate_id(
656    ctxt: *mut _xmlValidCtxt,
657    doc: *mut _xmlDoc,
658    node: *mut _xmlNode,
659    value: *const xmlChar,
660) -> c_int {
661    if value.is_null() || doc.is_null() {
662        return 0;
663    }
664
665    // First, check that the value is a valid XML Name
666    // UPSTREAM-PARITY: xmlValidateID uses xmlValidateNameValue semantics.
667    if unsafe { validate_name_value(value) } == 0 {
668        unsafe {
669            let msg = string::xmlstr_to_string(value);
670            let err_msg = format!("ID value '{}' is not a valid XML Name\0", msg);
671            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
672        }
673        return 0;
674    }
675
676    // Check for duplicate ID in the document's ID hash table
677    unsafe {
678        let doc_ref = &*doc;
679        if !doc_ref.ids.is_null() {
680            let existing = hash::hash_lookup(doc_ref.ids as *mut hash::HashTable, value);
681            if !existing.is_null() {
682                let msg = string::xmlstr_to_string(value);
683                let err_msg = format!("Duplicate ID value '{}'\0", msg);
684                vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
685                return 0;
686            }
687        }
688    }
689
690    // Register the ID in the document's ID hash table
691    unsafe {
692        if (*doc).ids.is_null() {
693            (*doc).ids = hash::hash_create(16) as *mut c_void;
694        }
695        hash::hash_add_entry(
696            (*doc).ids as *mut hash::HashTable,
697            value,
698            node as *mut c_void,
699        );
700    }
701
702    1
703}
704
705/// Validate an IDREF value: check that the referenced ID exists in the document.
706///
707/// # UPSTREAM-PARITY
708///
709/// ```c
710/// int xmlValidateIDRef(xmlValidCtxtPtr ctxt,
711///                      xmlDocPtr doc,
712///                      xmlNodePtr node,
713///                      const xmlChar *value);
714/// ```
715///
716/// Returns 1 if the IDREF is valid (references a known ID), 0 otherwise.
717///
718/// # SAFETY
719///
720/// - `ctxt`, `doc`, `node`, `value` may be NULL.
721pub unsafe fn validate_id_ref(
722    ctxt: *mut _xmlValidCtxt,
723    doc: *mut _xmlDoc,
724    _node: *mut _xmlNode,
725    value: *const xmlChar,
726) -> c_int {
727    if value.is_null() || doc.is_null() {
728        return 0;
729    }
730
731    // Check that the value is a valid XML Name
732    // UPSTREAM-PARITY: xmlValidateIDRef uses xmlValidateNameValue semantics.
733    if unsafe { validate_name_value(value) } == 0 {
734        unsafe {
735            let msg = string::xmlstr_to_string(value);
736            let err_msg = format!("IDREF value '{}' is not a valid XML Name\0", msg);
737            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
738        }
739        return 0;
740    }
741
742    // Check if the referenced ID exists
743    unsafe {
744        let doc_ref = &*doc;
745        if doc_ref.ids.is_null()
746            || hash::hash_lookup(doc_ref.ids as *mut hash::HashTable, value).is_null()
747        {
748            // UPSTREAM-PARITY: Forward references are allowed during
749            // validation but are reported as warnings. The final check
750            // happens in xmlValidateDocumentFinal.
751            let msg = string::xmlstr_to_string(value);
752            let err_msg = format!("IDREF '{}' references an unknown ID\0", msg);
753            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
754            return 0;
755        }
756    }
757
758    1
759}
760
761/// Validate IDREFS (whitespace-separated list of IDREF values).
762///
763/// # UPSTREAM-PARITY
764///
765/// ```c
766/// int xmlValidateIDRefs(xmlValidCtxtPtr ctxt,
767///                       xmlDocPtr doc,
768///                       xmlNodePtr node,
769///                       const xmlChar *value);
770/// ```
771///
772/// Returns 1 if all IDREFs are valid, 0 otherwise.
773///
774/// # SAFETY
775///
776/// - `ctxt`, `doc`, `node`, `value` may be NULL.
777pub unsafe fn validate_id_refs(
778    ctxt: *mut _xmlValidCtxt,
779    doc: *mut _xmlDoc,
780    node: *mut _xmlNode,
781    value: *const xmlChar,
782) -> c_int {
783    if value.is_null() || doc.is_null() {
784        return 0;
785    }
786
787    let s = unsafe { string::xmlstr_to_bytes(value) };
788    let s = core::str::from_utf8(s).unwrap_or("");
789
790    if s.is_empty() {
791        return 0;
792    }
793
794    let mut valid = 1;
795    for token in s.split_whitespace() {
796        if token.is_empty() {
797            continue;
798        }
799        // Create a null-terminated xmlChar string for each token
800        let token_ptr = unsafe { string::bytes_to_xmlstr(token.as_bytes()) };
801        if token_ptr.is_null() {
802            valid = 0;
803            break;
804        }
805        let result = unsafe { validate_id_ref(ctxt, doc, node, token_ptr) };
806        unsafe {
807            allocator::xmlFreeImpl(token_ptr as *mut c_void);
808        }
809        if result == 0 {
810            valid = 0;
811        }
812    }
813
814    valid
815}
816
817// ═══════════════════════════════════════════════════════════════════════════════
818// xmlValidateAttributeDecl
819// ═══════════════════════════════════════════════════════════════════════════════
820
821/// Validate an attribute's value against its declaration.
822///
823/// # UPSTREAM-PARITY
824///
825/// ```c
826/// int xmlValidateAttributeDecl(xmlValidCtxtPtr ctxt,
827///                              xmlDocPtr doc,
828///                              xmlNodePtr elem,
829///                              xmlAttributePtr attr);
830/// ```
831///
832/// Checks:
833/// - Attribute value type (CDATA, ID, IDREF, etc.)
834/// - Enumeration membership
835/// - NOTATION declaration
836/// - Default value validity
837///
838/// Returns 1 if valid, 0 otherwise.
839///
840/// # SAFETY
841///
842/// - `ctxt`, `doc`, `elem`, `attr` may be NULL.
843pub unsafe fn validate_attribute_decl(
844    ctxt: *mut _xmlValidCtxt,
845    doc: *mut _xmlDoc,
846    _elem: *mut _xmlNode,
847    attr: *mut _xmlAttribute,
848) -> c_int {
849    if attr.is_null() {
850        return 0;
851    }
852
853    unsafe {
854        let a = &*attr;
855        let atype = a.atype as c_int;
856
857        // Validate the default value if present
858        if !a.defaultValue.is_null() && validate_attribute_value(atype, a.defaultValue) == 0 {
859            let name_str = string::xmlstr_to_string(a.name);
860            let val_str = string::xmlstr_to_string(a.defaultValue);
861            let err_msg = format!(
862                "Default value '{}' for attribute '{}' is not valid for its type\0",
863                val_str, name_str
864            );
865            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
866            return 0;
867        }
868
869        // Validate enumeration values
870        if atype == XML_ATTRIBUTE_ENUMERATION as c_int && !a.tree.is_null() {
871            // Validate each enumeration value is a valid NMTOKEN
872            let mut cur = a.tree;
873            while !cur.is_null() {
874                if !(*cur).name.is_null() && validate_nmtoken_value((*cur).name) == 0 {
875                    let val_str = string::xmlstr_to_string((*cur).name);
876                    let err_msg =
877                        format!("Enumeration value '{}' is not a valid NMTOKEN\0", val_str);
878                    vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
879                    return 0;
880                }
881                cur = (*cur).next;
882            }
883        }
884
885        // Validate NOTATION values reference declared notations
886        if atype == XML_ATTRIBUTE_NOTATION as c_int && !a.tree.is_null() {
887            let mut cur = a.tree;
888            while !cur.is_null() {
889                if !(*cur).name.is_null() && validate_notation_use(ctxt, doc, (*cur).name) == 0 {
890                    let val_str = string::xmlstr_to_string((*cur).name);
891                    let err_msg = format!(
892                        "NOTATION value '{}' references undeclared notation\0",
893                        val_str
894                    );
895                    vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
896                    return 0;
897                }
898                cur = (*cur).next;
899            }
900        }
901
902        1
903    }
904}
905
906// ═══════════════════════════════════════════════════════════════════════════════
907// xmlValidateElement — Core element validation
908// ═══════════════════════════════════════════════════════════════════════════════
909
910/// Validate a single element node against its DTD element and attribute
911/// declarations.
912///
913/// # UPSTREAM-PARITY
914///
915/// ```c
916/// int xmlValidateElement(xmlValidCtxtPtr ctxt,
917///                        xmlDocPtr doc,
918///                        xmlNodePtr elem);
919/// ```
920///
921/// Validates:
922/// 1. Element declaration exists for the element name
923/// 2. Content model matches child elements
924/// 3. Required attributes are present
925/// 4. Attribute values match their declared types
926/// 5. ID uniqueness
927/// 6. IDREF references resolve
928///
929/// Returns 1 if valid, 0 otherwise.
930///
931/// # SAFETY
932///
933/// - `ctxt`, `doc`, `elem` may be NULL.
934pub unsafe fn validate_element(
935    ctxt: *mut _xmlValidCtxt,
936    doc: *mut _xmlDoc,
937    elem: *mut _xmlNode,
938) -> c_int {
939    if elem.is_null() || doc.is_null() || ctxt.is_null() {
940        return 0;
941    }
942
943    unsafe {
944        let e = &*elem;
945
946        // Skip non-element nodes
947        if e.type_ != XML_ELEMENT_NODE as c_int {
948            return 1;
949        }
950
951        // Push node onto stack
952        if vctxt_push_node(ctxt, elem) != 0 {
953            return 0;
954        }
955
956        let mut valid = 1;
957
958        // Get the DTD
959        let dtd = get_valid_dtd(doc);
960        if dtd.is_null() {
961            // No DTD — no validation to perform
962            // UPSTREAM-PARITY: libxml2 returns 1 if there's no DTD.
963            vctxt_pop_node(ctxt);
964            return 1;
965        }
966
967        let dtd_ref = &*dtd;
968
969        // Look up element declaration
970        let elem_name = e.name;
971        let elem_decl = if !dtd_ref.elements.is_null() {
972            hash::hash_lookup(dtd_ref.elements as *mut hash::HashTable, elem_name)
973        } else {
974            ptr::null_mut()
975        };
976
977        if elem_decl.is_null() {
978            let name_str = string::xmlstr_to_string(elem_name);
979            let err_msg = format!("No declaration for element {}\0", name_str);
980            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
981            vctxt_pop_node(ctxt);
982            return 0;
983        }
984
985        let elem_decl_ref = &*(elem_decl as *mut _xmlElement);
986
987        // ── Content model validation ──────────────────────────────────────
988        let elem_type = elem_decl_ref.etype as u32;
989
990        if elem_type == XML_ELEMENT_TYPE_EMPTY as u32 {
991            // Element must have no children (except text nodes)
992            let mut child = e.children;
993            while !child.is_null() {
994                let child_type = (*child).type_ as u32;
995                if child_type != XML_TEXT_NODE as u32 && child_type != XML_CDATA_SECTION_NODE as u32
996                {
997                    valid = 0;
998                    let name_str = string::xmlstr_to_string(elem_name);
999                    let err_msg = format!(
1000                        "Element '{}' is declared EMPTY but has child elements\0",
1001                        name_str
1002                    );
1003                    vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1004                    break;
1005                }
1006                child = (*child).next;
1007            }
1008        } else if elem_type == XML_ELEMENT_TYPE_ANY as u32 {
1009            // ANY: any content is allowed
1010        } else if elem_type == XML_ELEMENT_TYPE_MIXED as u32 {
1011            // MIXED: PCDATA plus optionally declared child elements
1012            let mut child = e.children;
1013            while !child.is_null() {
1014                let child_type = (*child).type_ as u32;
1015                if child_type == XML_ELEMENT_NODE as u32 {
1016                    // Validate that child element name is in the mixed content model
1017                    let child_name = (*child).name;
1018                    let result = dtd::valid_content_model(elem_decl_ref.content, &[child_name]);
1019                    if result != dtd::ContentModelResult::Valid {
1020                        let cname_str = string::xmlstr_to_string(child_name);
1021                        let ename_str = string::xmlstr_to_string(elem_name);
1022                        let err_msg = format!(
1023                            "Element '{}' is not allowed in mixed content of '{}'\0",
1024                            cname_str, ename_str
1025                        );
1026                        vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1027                        valid = 0;
1028                    }
1029                }
1030                child = (*child).next;
1031            }
1032        } else if elem_type == XML_ELEMENT_TYPE_ELEMENT as u32 {
1033            // Element-only content: collect child element names and validate
1034            let mut child_names: Vec<*const xmlChar> = Vec::new();
1035            let mut child = e.children;
1036            while !child.is_null() {
1037                if (*child).type_ == XML_ELEMENT_NODE as c_int {
1038                    child_names.push((*child).name);
1039                }
1040                child = (*child).next;
1041            }
1042
1043            let result = dtd::valid_content_model(elem_decl_ref.content, &child_names);
1044            if result != dtd::ContentModelResult::Valid {
1045                let ename_str = string::xmlstr_to_string(elem_name);
1046                let err_msg = format!(
1047                    "Content model validation failed for element '{}'\0",
1048                    ename_str
1049                );
1050                vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1051                valid = 0;
1052            }
1053        }
1054
1055        // ── Attribute validation ──────────────────────────────────────────
1056        if !dtd_ref.attributes.is_null() {
1057            // Walk all attributes on the element node
1058            let mut attr_prop = e.properties;
1059            while !attr_prop.is_null() {
1060                let attr_ref = &*attr_prop;
1061                let attr_name = attr_ref.name;
1062
1063                // Look up the attribute declaration (keyed by name, prefix,
1064                // elem — upstream xmlHashLookup3).
1065                let attr_decl = hash::hash_lookup3(
1066                    dtd_ref.attributes as *mut hash::HashTable,
1067                    attr_name,
1068                    ptr::null(),
1069                    elem_name,
1070                );
1071
1072                if attr_decl.is_null() {
1073                    // Undeclared attribute — not a validation error per se
1074                    // in DTD validation, but might be in Schema validation.
1075                    // UPSTREAM-PARITY: libxml2 skips undeclared attrs in
1076                    // DTD validation mode.
1077                    attr_prop = attr_ref.next;
1078                    continue;
1079                }
1080
1081                let attr_decl_ref = &*(attr_decl as *mut _xmlAttribute);
1082                let atype = attr_decl_ref.atype as c_int;
1083
1084                // Get attribute value from content
1085                let attr_value = if !attr_ref.children.is_null() {
1086                    // Get text content of the attribute node
1087                    let text_node = attr_ref.children;
1088                    if (*text_node).type_ == XML_TEXT_NODE as c_int
1089                        || (*text_node).type_ == XML_CDATA_SECTION_NODE as c_int
1090                    {
1091                        (*text_node).content
1092                    } else {
1093                        ptr::null()
1094                    }
1095                } else {
1096                    ptr::null()
1097                };
1098
1099                // Validate the attribute value against its type
1100                if !attr_value.is_null() {
1101                    if atype == XML_ATTRIBUTE_ENUMERATION as c_int && !attr_decl_ref.tree.is_null()
1102                    {
1103                        if validate_enumeration(ctxt, attr_value, attr_decl_ref.tree) == 0 {
1104                            let aname_str = string::xmlstr_to_string(attr_name);
1105                            let aval_str = string::xmlstr_to_string(attr_value);
1106                            let err_msg = format!(
1107                                "Attribute '{}' has value '{}' not in enumeration\0",
1108                                aname_str, aval_str
1109                            );
1110                            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1111                            valid = 0;
1112                        }
1113                    } else if atype == XML_ATTRIBUTE_NOTATION as c_int {
1114                        if validate_notation_use(ctxt, doc, attr_value) == 0 {
1115                            let aname_str = string::xmlstr_to_string(attr_name);
1116                            let aval_str = string::xmlstr_to_string(attr_value);
1117                            let err_msg = format!(
1118                                "Attribute '{}' references undeclared notation '{}'\0",
1119                                aname_str, aval_str
1120                            );
1121                            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1122                            valid = 0;
1123                        }
1124                    } else if validate_attribute_value(atype, attr_value) == 0 {
1125                        let aname_str = string::xmlstr_to_string(attr_name);
1126                        let aval_str = string::xmlstr_to_string(attr_value);
1127                        let err_msg = format!(
1128                            "Attribute '{}' has invalid value '{}' for its type\0",
1129                            aname_str, aval_str
1130                        );
1131                        vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1132                        valid = 0;
1133                    }
1134
1135                    // ID/IDREF specific validation
1136                    if atype == XML_ATTRIBUTE_ID as c_int {
1137                        if validate_id(ctxt, doc, elem, attr_value) == 0 {
1138                            valid = 0;
1139                        }
1140                    } else if atype == XML_ATTRIBUTE_IDREF as c_int {
1141                        if validate_id_ref(ctxt, doc, elem, attr_value) == 0 {
1142                            valid = 0;
1143                        }
1144                    } else if atype == XML_ATTRIBUTE_IDREFS as c_int
1145                        && validate_id_refs(ctxt, doc, elem, attr_value) == 0
1146                    {
1147                        valid = 0;
1148                    }
1149                }
1150
1151                attr_prop = attr_ref.next;
1152            }
1153
1154            // ── Check for required attributes ─────────────────────────────
1155            struct RequiredAttrCheck {
1156                ctxt: *mut _xmlValidCtxt,
1157                elem_name: *const xmlChar,
1158                elem_props: *mut _xmlAttr,
1159                valid: *mut c_int,
1160            }
1161
1162            extern "C" fn check_required_attr(
1163                payload: *mut c_void,
1164                data: *mut c_void,
1165                name: *const xmlChar,
1166                name2: *const xmlChar,
1167                _name3: *const xmlChar,
1168            ) {
1169                if payload.is_null() || data.is_null() || name2.is_null() {
1170                    return;
1171                }
1172
1173                // SAFETY: Called from hash_scan_full.
1174                let check = unsafe { &*(data as *mut RequiredAttrCheck) };
1175                unsafe {
1176                    // Only check attributes belonging to this element
1177                    if string::xml_strcmp(name, check.elem_name) != 0 {
1178                        return;
1179                    }
1180
1181                    let attr_decl = &*(payload as *mut _xmlAttribute);
1182
1183                    // If the attribute is REQUIRED, check if it's present
1184                    if attr_decl.def == XML_ATTRIBUTE_REQUIRED as c_int {
1185                        // Check if this attribute name is in the element's properties
1186                        let mut found = 0;
1187                        let mut prop = check.elem_props;
1188                        while !prop.is_null() {
1189                            if string::xml_strcmp((*prop).name, name2) == 0 {
1190                                found = 1;
1191                                break;
1192                            }
1193                            prop = (*prop).next;
1194                        }
1195
1196                        if found == 0 {
1197                            let aname_str = string::xmlstr_to_string(name2);
1198                            let ename_str = string::xmlstr_to_string(check.elem_name);
1199                            let err_msg = format!(
1200                                "Required attribute '{}' missing on element '{}'\0",
1201                                aname_str, ename_str
1202                            );
1203                            vctxt_error(check.ctxt, err_msg.as_ptr() as *const c_char);
1204                            *(check.valid) = 0;
1205                        }
1206                    }
1207                }
1208            }
1209
1210            let mut required_valid = valid;
1211            let check = RequiredAttrCheck {
1212                ctxt,
1213                elem_name,
1214                elem_props: e.properties,
1215                valid: &mut required_valid,
1216            };
1217
1218            hash::hash_scan_full(
1219                dtd_ref.attributes as *mut hash::HashTable,
1220                Some(check_required_attr),
1221                &check as *const RequiredAttrCheck as *mut c_void,
1222            );
1223
1224            valid = required_valid;
1225        }
1226
1227        // ── Recurse into children ─────────────────────────────────────────
1228        let mut child = e.children;
1229        while !child.is_null() {
1230            if (*child).type_ == XML_ELEMENT_NODE as c_int
1231                && validate_element(ctxt, doc, child) == 0
1232            {
1233                valid = 0;
1234            }
1235            child = (*child).next;
1236        }
1237
1238        vctxt_pop_node(ctxt);
1239        valid
1240    }
1241}
1242
1243// ═══════════════════════════════════════════════════════════════════════════════
1244// xmlValidateDocument
1245// ═══════════════════════════════════════════════════════════════════════════════
1246
1247/// Validate an entire document against its DTD.
1248///
1249/// # UPSTREAM-PARITY
1250///
1251/// ```c
1252/// int xmlValidateDocument(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
1253/// ```
1254///
1255/// Validates the root element and all its descendants, plus the DTD itself.
1256///
1257/// Returns 1 if valid, 0 otherwise.
1258///
1259/// # SAFETY
1260///
1261/// - `ctxt`, `doc` may be NULL.
1262pub unsafe fn validate_document(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
1263    if ctxt.is_null() || doc.is_null() {
1264        return 0;
1265    }
1266
1267    unsafe {
1268        let c = &mut *ctxt;
1269        c.doc = doc;
1270        c.valid = 1;
1271
1272        let d = &*doc;
1273
1274        // UPSTREAM-PARITY: xmlValidateDocumentInternal rejects documents with
1275        // no internal or external subset (valid.c:6266-6271):
1276        //
1277        // ```c
1278        // if ((doc->intSubset == NULL) && (doc->extSubset == NULL)) {
1279        //     xmlErrValid(vctxt, XML_DTD_NO_DTD, "no DTD found!\n", NULL);
1280        //     return(0);
1281        // }
1282        // ```
1283        if d.intSubset.is_null() && d.extSubset.is_null() {
1284            vctxt_error(ctxt, b"no DTD found!\0" as *const u8 as *const c_char);
1285            return 0;
1286        }
1287
1288        // Find the root element (first child that's an element node)
1289        let mut root = d.children;
1290        while !root.is_null() {
1291            if (*root).type_ == XML_ELEMENT_NODE as c_int {
1292                break;
1293            }
1294            root = (*root).next;
1295        }
1296
1297        if root.is_null() {
1298            vctxt_error(
1299                ctxt,
1300                b"No root element found in document\0" as *const u8 as *const c_char,
1301            );
1302            return 0;
1303        }
1304
1305        // Validate the root element
1306        if validate_element(ctxt, doc, root) == 0 {
1307            return 0;
1308        }
1309
1310        c.valid
1311    }
1312}
1313
1314// ═══════════════════════════════════════════════════════════════════════════════
1315// xmlValidateDocumentFinal
1316// ═══════════════════════════════════════════════════════════════════════════════
1317
1318/// Final validation: check that all IDREFs resolve to existing IDs.
1319///
1320/// # UPSTREAM-PARITY
1321///
1322/// ```c
1323/// int xmlValidateDocumentFinal(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
1324/// ```
1325///
1326/// This is called after the document is fully parsed, to verify ID/IDREF
1327/// consistency. During parsing, forward IDREFs may not be resolvable, so
1328/// this final pass checks them.
1329///
1330/// Returns 1 if all IDREFs resolve, 0 otherwise.
1331///
1332/// # SAFETY
1333///
1334/// - `ctxt`, `doc` may be NULL.
1335pub unsafe fn validate_document_final(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
1336    if ctxt.is_null() || doc.is_null() {
1337        return 0;
1338    }
1339
1340    unsafe {
1341        let c = &mut *ctxt;
1342        c.doc = doc;
1343
1344        let d = &*doc;
1345
1346        // If there's no refs table, no IDREFs were found
1347        if d.refs.is_null() {
1348            return c.valid;
1349        }
1350
1351        // Check each IDREF against the IDs table
1352        struct IdRefCheckContext {
1353            ctxt: *mut _xmlValidCtxt,
1354            doc: *mut _xmlDoc,
1355        }
1356
1357        extern "C" fn check_idref(
1358            _payload: *mut c_void,
1359            data: *mut c_void,
1360            _name: *const xmlChar,
1361            name2: *const xmlChar,
1362            _name3: *const xmlChar,
1363        ) {
1364            if data.is_null() || name2.is_null() {
1365                return;
1366            }
1367
1368            // SAFETY: Called from hash_scan_full.
1369            let cx = unsafe { &*(data as *mut IdRefCheckContext) };
1370            unsafe {
1371                let doc_ref = &*cx.doc;
1372
1373                // Look up the IDREF value in the IDs table
1374                if doc_ref.ids.is_null()
1375                    || hash::hash_lookup(doc_ref.ids as *mut hash::HashTable, name2).is_null()
1376                {
1377                    let ref_str = string::xmlstr_to_string(name2);
1378                    let err_msg = format!("IDREF '{}' does not reference a declared ID\0", ref_str);
1379                    vctxt_error(cx.ctxt, err_msg.as_ptr() as *const c_char);
1380                }
1381            }
1382        }
1383
1384        let ctx = IdRefCheckContext { ctxt, doc };
1385        hash::hash_scan_full(
1386            d.refs as *mut hash::HashTable,
1387            Some(check_idref),
1388            &ctx as *const IdRefCheckContext as *mut c_void,
1389        );
1390
1391        c.valid
1392    }
1393}
1394
1395// ═══════════════════════════════════════════════════════════════════════════════
1396// xmlValidateRoot
1397// ═══════════════════════════════════════════════════════════════════════════════
1398
1399/// Validate the root element of a document.
1400///
1401/// # UPSTREAM-PARITY
1402///
1403/// ```c
1404/// int xmlValidateRoot(xmlValidCtxtPtr ctxt, xmlDocPtr doc);
1405/// ```
1406///
1407/// Returns 1 if the root element is valid, 0 otherwise.
1408///
1409/// # SAFETY
1410///
1411/// - `ctxt`, `doc` may be NULL.
1412pub unsafe fn validate_root(ctxt: *mut _xmlValidCtxt, doc: *mut _xmlDoc) -> c_int {
1413    if ctxt.is_null() || doc.is_null() {
1414        return 0;
1415    }
1416
1417    unsafe {
1418        let c = &mut *ctxt;
1419        c.doc = doc;
1420        c.valid = 1;
1421
1422        let d = &*doc;
1423
1424        // Find root element
1425        let mut root = d.children;
1426        while !root.is_null() {
1427            if (*root).type_ == XML_ELEMENT_NODE as c_int {
1428                break;
1429            }
1430            root = (*root).next;
1431        }
1432
1433        if root.is_null() {
1434            vctxt_error(
1435                ctxt,
1436                b"No root element found\0" as *const u8 as *const c_char,
1437            );
1438            return 0;
1439        }
1440
1441        // Get the DTD
1442        let dtd = get_valid_dtd(doc);
1443        if dtd.is_null() {
1444            // No DTD — nothing to validate against
1445            return 1;
1446        }
1447
1448        // UPSTREAM-PARITY: libxml2 checks that the root element name matches
1449        // the DTD's name (the DOCTYPE name).
1450        let dtd_ref = &*dtd;
1451        if !dtd_ref.name.is_null() && string::xml_strcmp((*root).name, dtd_ref.name) != 0 {
1452            let root_str = string::xmlstr_to_string((*root).name);
1453            let dtd_str = string::xmlstr_to_string(dtd_ref.name);
1454            let err_msg = format!(
1455                "Root element '{}' does not match DTD root '{}'\0",
1456                root_str, dtd_str
1457            );
1458            vctxt_error(ctxt, err_msg.as_ptr() as *const c_char);
1459            return 0;
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.
1856const fn 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).
1861const fn 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).
2440const unsafe 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                    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`).
3043///
3044/// # SAFETY
3045///
3046/// - `_ctxt`, `_doc`, `_nota` must be valid pointers (or NULL
3047///   where the upstream C contract allows), obtained from the
3048///   matching constructor/owner and not yet freed; the callee may
3049///   take or keep ownership exactly as the C API specifies.
3050///
3051/// The caller must not race this call with concurrent mutation of the
3052/// same objects from other threads (per-object state is not internally
3053/// synchronized). Violating any of the above is undefined behavior.
3054///
3055/// Exercised by the C-API differential courts
3056/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3057/// courts; those pass byte-for-byte against the upstream oracle.
3058pub const unsafe fn validate_notation_decl(
3059    _ctxt: *mut _xmlValidCtxt,
3060    _doc: *mut _xmlDoc,
3061    _nota: *mut _xmlNotation,
3062) -> c_int {
3063    1
3064}
3065
3066/// Upstream `xmlValidateOneAttribute(ctxt, doc, elem, attr, value)`.
3067///
3068/// Performs [VC: Attribute Value Type], [VC: Fixed Attribute Default],
3069/// [VC: ID], [VC: IDREF], [VC: Notation Attributes], [VC: Enumeration],
3070/// and the ENTITY existence check via xmlValidateAttributeValue2.
3071///
3072/// # SAFETY
3073///
3074/// - `ctxt`/`doc` may be NULL; `elem`/`attr`/`value` valid pointers or NULL.
3075pub unsafe fn validate_one_attribute(
3076    ctxt: *mut _xmlValidCtxt,
3077    doc: *mut _xmlDoc,
3078    elem: *mut _xmlNode,
3079    attr: *mut _xmlAttr,
3080    value: *const xmlChar,
3081) -> c_int {
3082    unsafe {
3083        if doc.is_null() {
3084            return 0;
3085        }
3086        if elem.is_null() || (*elem).name.is_null() {
3087            return 0;
3088        }
3089        if attr.is_null() || (*attr).name.is_null() {
3090            return 0;
3091        }
3092        let mut ret = 1;
3093
3094        let aprefix = if !(*attr).ns.is_null() {
3095            (*(*attr).ns).prefix
3096        } else {
3097            ptr::null()
3098        };
3099
3100        let mut attr_decl = ptr::null_mut();
3101        if !(*elem).ns.is_null() && !(*(*elem).ns).prefix.is_null() {
3102            let fullname =
3103                string::build_qname((*elem).name, (*(*elem).ns).prefix, ptr::null_mut(), 0);
3104            if fullname.is_null() {
3105                vctxt_error(
3106                    ctxt,
3107                    b"Memory allocation failed : xmlValidateOneAttribute\0" as *const u8
3108                        as *const c_char,
3109                );
3110                return 0;
3111            }
3112            attr_decl = get_dtd_qattr_desc((*doc).intSubset, fullname, (*attr).name, aprefix);
3113            if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3114                attr_decl = get_dtd_qattr_desc((*doc).extSubset, fullname, (*attr).name, aprefix);
3115            }
3116            if !std::ptr::eq(fullname, (*elem).name) {
3117                allocator::xmlFreeImpl(fullname as *mut c_void);
3118            }
3119        }
3120        if attr_decl.is_null() {
3121            attr_decl = get_dtd_qattr_desc((*doc).intSubset, (*elem).name, (*attr).name, aprefix);
3122            if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3123                attr_decl =
3124                    get_dtd_qattr_desc((*doc).extSubset, (*elem).name, (*attr).name, aprefix);
3125            }
3126        }
3127
3128        // [VC: Attribute Value Type]
3129        if attr_decl.is_null() {
3130            let msg = format!(
3131                "No declaration for attribute {} of element {}\0",
3132                string::xmlstr_to_string((*attr).name),
3133                string::xmlstr_to_string((*elem).name)
3134            );
3135            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3136            return 0;
3137        }
3138        if !(*attr).id.is_null() {
3139            remove_id(doc, attr);
3140        }
3141        (*attr).atype = (*attr_decl).atype;
3142
3143        // syntax check against the declared type (with OLD10 doc flag)
3144        let val = if (*doc).properties & crate::abi::types::xmlDocProperties::XML_DOC_OLD10 as c_int
3145            != 0
3146        {
3147            // OLD10 name classes are not implemented; the modern classes are
3148            // a superset for ASCII and match for all BMP ranges used here.
3149            match (*attr_decl).atype as u32 {
3150                t if t == XML_ATTRIBUTE_ENTITIES as u32 || t == XML_ATTRIBUTE_IDREFS as u32 => {
3151                    validate_values_internal(value, 0)
3152                }
3153                t if t == XML_ATTRIBUTE_ENTITY as u32
3154                    || t == XML_ATTRIBUTE_IDREF as u32
3155                    || t == XML_ATTRIBUTE_ID as u32
3156                    || t == XML_ATTRIBUTE_NOTATION as u32 =>
3157                {
3158                    validate_value_internal(value, 0)
3159                }
3160                t if t == XML_ATTRIBUTE_NMTOKENS as u32
3161                    || t == XML_ATTRIBUTE_ENUMERATION as u32 =>
3162                {
3163                    validate_values_internal(value, XML_SCAN_NMTOKEN)
3164                }
3165                t if t == XML_ATTRIBUTE_NMTOKEN as u32 => {
3166                    validate_value_internal(value, XML_SCAN_NMTOKEN)
3167                }
3168                _ => 1,
3169            }
3170        } else {
3171            validate_attribute_value((*attr_decl).atype, value)
3172        };
3173        if val == 0 {
3174            let msg = format!(
3175                "Syntax of value for attribute {} of {} is not valid\0",
3176                string::xmlstr_to_string((*attr).name),
3177                string::xmlstr_to_string((*elem).name)
3178            );
3179            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3180            ret = 0;
3181        }
3182
3183        // [VC: Fixed Attribute Default]
3184        if (*attr_decl).def == XML_ATTRIBUTE_FIXED as c_int
3185            && string::xml_strcmp(value, (*attr_decl).defaultValue) != 0
3186        {
3187            let _msg = format!(
3188                "Value for attribute {} of {} is different from default \"{}\n\0",
3189                string::xmlstr_to_string((*attr).name),
3190                string::xmlstr_to_string((*elem).name),
3191                string::xmlstr_to_string((*attr_decl).defaultValue)
3192            );
3193            // upstream format: "Value for attribute %s of %s is different from default \"%s\"\n"
3194            let msg = format!(
3195                "Value for attribute {} of {} is different from default \"{}\"\0",
3196                string::xmlstr_to_string((*attr).name),
3197                string::xmlstr_to_string((*elem).name),
3198                string::xmlstr_to_string((*attr_decl).defaultValue)
3199            );
3200            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3201            ret = 0;
3202        }
3203
3204        // [VC: ID] uniqueness (skipped inside entities)
3205        const XML_VCTXT_IN_ENTITY: c_uint = 4; // upstream valid.h
3206        if (*attr_decl).atype == XML_ATTRIBUTE_ID as c_int
3207            && (ctxt.is_null() || (*ctxt).flags & XML_VCTXT_IN_ENTITY == 0)
3208            && add_id(ctxt, doc, value, attr).is_null()
3209        {
3210            ret = 0;
3211        }
3212        if ((*attr_decl).atype == XML_ATTRIBUTE_IDREF as c_int
3213            || (*attr_decl).atype == XML_ATTRIBUTE_IDREFS as c_int)
3214            && add_ref(ctxt, doc, value, attr).is_null()
3215        {
3216            ret = 0;
3217        }
3218
3219        // [VC: Notation Attributes]
3220        if (*attr_decl).atype == XML_ATTRIBUTE_NOTATION as c_int {
3221            let mut nota = get_dtd_notation_desc((*doc).intSubset, value);
3222            if nota.is_null() {
3223                nota = get_dtd_notation_desc((*doc).extSubset, value);
3224            }
3225            if nota.is_null() {
3226                let msg = format!(
3227                    "Value \"{}\" for attribute {} of {} is not a declared Notation\0",
3228                    string::xmlstr_to_string(value),
3229                    string::xmlstr_to_string((*attr).name),
3230                    string::xmlstr_to_string((*elem).name)
3231                );
3232                vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3233                ret = 0;
3234            }
3235            let mut tree = (*attr_decl).tree;
3236            while !tree.is_null() {
3237                if string::xml_strcmp((*tree).name, value) == 0 {
3238                    break;
3239                }
3240                tree = (*tree).next;
3241            }
3242            if tree.is_null() {
3243                let msg = format!(
3244                    "Value \"{}\" for attribute {} of {} is not among the enumerated notations\0",
3245                    string::xmlstr_to_string(value),
3246                    string::xmlstr_to_string((*attr).name),
3247                    string::xmlstr_to_string((*elem).name)
3248                );
3249                vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3250                ret = 0;
3251            }
3252        }
3253
3254        // [VC: Enumeration]
3255        if (*attr_decl).atype == XML_ATTRIBUTE_ENUMERATION as c_int {
3256            let mut tree = (*attr_decl).tree;
3257            while !tree.is_null() {
3258                if string::xml_strcmp((*tree).name, value) == 0 {
3259                    break;
3260                }
3261                tree = (*tree).next;
3262            }
3263            if tree.is_null() {
3264                let msg = format!(
3265                    "Value \"{}\" for attribute {} of {} is not among the enumerated set\0",
3266                    string::xmlstr_to_string(value),
3267                    string::xmlstr_to_string((*attr).name),
3268                    string::xmlstr_to_string((*elem).name)
3269                );
3270                vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3271                ret = 0;
3272            }
3273        }
3274
3275        // Fixed Attribute Default (second occurrence, upstream)
3276        if (*attr_decl).def == XML_ATTRIBUTE_FIXED as c_int
3277            && string::xml_strcmp((*attr_decl).defaultValue, value) != 0
3278        {
3279            let msg = format!(
3280                "Value for attribute {} of {} must be \"{}\"\0",
3281                string::xmlstr_to_string((*attr).name),
3282                string::xmlstr_to_string((*elem).name),
3283                string::xmlstr_to_string((*attr_decl).defaultValue)
3284            );
3285            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3286            ret = 0;
3287        }
3288
3289        // [VC: Entity Name] — ENTITY must name a declared unparsed entity
3290        if (*attr_decl).atype == XML_ATTRIBUTE_ENTITY as c_int {
3291            let ent = tree::get_doc_entity(doc, value);
3292            if ent.is_null() {
3293                let msg = format!(
3294                    "ENTITY attribute {} reference an unknown entity \"{}\"\0",
3295                    string::xmlstr_to_string((*attr).name),
3296                    string::xmlstr_to_string(value)
3297                );
3298                vctxt_error_node(ctxt, doc as *mut _xmlNode, msg.as_ptr() as *const c_char);
3299                ret = 0;
3300            } else if (*ent).etype != XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int {
3301                let msg = format!(
3302                    "ENTITY attribute {} reference an entity \"{}\" of wrong type\0",
3303                    string::xmlstr_to_string((*attr).name),
3304                    string::xmlstr_to_string(value)
3305                );
3306                vctxt_error_node(ctxt, doc as *mut _xmlNode, msg.as_ptr() as *const c_char);
3307                ret = 0;
3308            }
3309        }
3310        ret
3311    }
3312}
3313
3314/// Upstream `xmlValidateOneNamespace(ctxt, doc, elem, prefix, ns, value)` —
3315/// namespace-declaration attribute validation.
3316///
3317/// # SAFETY
3318///
3319/// - `ctxt` may be NULL; `doc`/`elem`/`ns` valid pointers or NULL.
3320pub unsafe fn validate_one_namespace(
3321    ctxt: *mut _xmlValidCtxt,
3322    doc: *mut _xmlDoc,
3323    elem: *mut _xmlNode,
3324    prefix: *const xmlChar,
3325    ns: *mut _xmlNs,
3326    value: *const xmlChar,
3327) -> c_int {
3328    unsafe {
3329        if doc.is_null() {
3330            return 0;
3331        }
3332        if elem.is_null() || (*elem).name.is_null() {
3333            return 0;
3334        }
3335        if ns.is_null() || (*ns).href.is_null() {
3336            return 0;
3337        }
3338        let mut ret = 1;
3339
3340        let mut attr_decl = ptr::null_mut();
3341        if !prefix.is_null() {
3342            let fullname = string::build_qname((*elem).name, prefix, ptr::null_mut(), 0);
3343            if fullname.is_null() {
3344                vctxt_error(
3345                    ctxt,
3346                    b"Memory allocation failed : xmlValidateOneNamespace\0" as *const u8
3347                        as *const c_char,
3348                );
3349                return 0;
3350            }
3351            if !(*ns).prefix.is_null() {
3352                attr_decl = get_dtd_qattr_desc(
3353                    (*doc).intSubset,
3354                    fullname,
3355                    (*ns).prefix,
3356                    b"xmlns\0" as *const u8 as *const xmlChar,
3357                );
3358                if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3359                    attr_decl = get_dtd_qattr_desc(
3360                        (*doc).extSubset,
3361                        fullname,
3362                        (*ns).prefix,
3363                        b"xmlns\0" as *const u8 as *const xmlChar,
3364                    );
3365                }
3366            } else {
3367                attr_decl = get_dtd_qattr_desc(
3368                    (*doc).intSubset,
3369                    fullname,
3370                    b"xmlns\0" as *const u8 as *const xmlChar,
3371                    ptr::null(),
3372                );
3373                if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3374                    attr_decl = get_dtd_qattr_desc(
3375                        (*doc).extSubset,
3376                        fullname,
3377                        b"xmlns\0" as *const u8 as *const xmlChar,
3378                        ptr::null(),
3379                    );
3380                }
3381            }
3382            if !std::ptr::eq(fullname, (*elem).name) {
3383                allocator::xmlFreeImpl(fullname as *mut c_void);
3384            }
3385        }
3386        if attr_decl.is_null() {
3387            if !(*ns).prefix.is_null() {
3388                attr_decl = get_dtd_qattr_desc(
3389                    (*doc).intSubset,
3390                    (*elem).name,
3391                    (*ns).prefix,
3392                    b"xmlns\0" as *const u8 as *const xmlChar,
3393                );
3394                if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3395                    attr_decl = get_dtd_qattr_desc(
3396                        (*doc).extSubset,
3397                        (*elem).name,
3398                        (*ns).prefix,
3399                        b"xmlns\0" as *const u8 as *const xmlChar,
3400                    );
3401                }
3402            } else {
3403                attr_decl = get_dtd_qattr_desc(
3404                    (*doc).intSubset,
3405                    (*elem).name,
3406                    b"xmlns\0" as *const u8 as *const xmlChar,
3407                    ptr::null(),
3408                );
3409                if attr_decl.is_null() && !(*doc).extSubset.is_null() {
3410                    attr_decl = get_dtd_qattr_desc(
3411                        (*doc).extSubset,
3412                        (*elem).name,
3413                        b"xmlns\0" as *const u8 as *const xmlChar,
3414                        ptr::null(),
3415                    );
3416                }
3417            }
3418        }
3419
3420        // [VC: Attribute Value Type]
3421        if attr_decl.is_null() {
3422            let msg = if !(*ns).prefix.is_null() {
3423                format!(
3424                    "No declaration for attribute xmlns:{} of element {}\0",
3425                    string::xmlstr_to_string((*ns).prefix),
3426                    string::xmlstr_to_string((*elem).name)
3427                )
3428            } else {
3429                format!(
3430                    "No declaration for attribute xmlns of element {}\0",
3431                    string::xmlstr_to_string((*elem).name)
3432                )
3433            };
3434            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3435            return 0;
3436        }
3437
3438        let val = validate_attribute_value((*attr_decl).atype, value);
3439        if val == 0 {
3440            let msg = if !(*ns).prefix.is_null() {
3441                format!(
3442                    "Syntax of value for attribute xmlns:{} of {} is not valid\0",
3443                    string::xmlstr_to_string((*ns).prefix),
3444                    string::xmlstr_to_string((*elem).name)
3445                )
3446            } else {
3447                format!(
3448                    "Syntax of value for attribute xmlns of {} is not valid\0",
3449                    string::xmlstr_to_string((*elem).name)
3450                )
3451            };
3452            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3453            ret = 0;
3454        }
3455
3456        // [VC: Fixed Attribute Default]
3457        if (*attr_decl).def == XML_ATTRIBUTE_FIXED as c_int
3458            && string::xml_strcmp(value, (*attr_decl).defaultValue) != 0
3459        {
3460            let msg = if !(*ns).prefix.is_null() {
3461                format!(
3462                    "Value for attribute xmlns:{} of {} is different from default \"{}\"\0",
3463                    string::xmlstr_to_string((*ns).prefix),
3464                    string::xmlstr_to_string((*elem).name),
3465                    string::xmlstr_to_string((*attr_decl).defaultValue)
3466                )
3467            } else {
3468                format!(
3469                    "Value for attribute xmlns of {} is different from default \"{}\"\0",
3470                    string::xmlstr_to_string((*elem).name),
3471                    string::xmlstr_to_string((*attr_decl).defaultValue)
3472                )
3473            };
3474            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3475            ret = 0;
3476        }
3477        ret
3478    }
3479}
3480
3481/// Upstream `xmlValidateOneElement(ctxt, doc, elem)` — validates a single
3482/// element against its declaration (content model + attributes), WITHOUT
3483/// recursing into children.
3484///
3485/// # SAFETY
3486///
3487/// - `ctxt` may be NULL; `doc`/`elem` valid pointers or NULL.
3488pub unsafe fn validate_one_element(
3489    ctxt: *mut _xmlValidCtxt,
3490    doc: *mut _xmlDoc,
3491    elem: *mut _xmlNode,
3492) -> c_int {
3493    unsafe {
3494        if doc.is_null() {
3495            return 0;
3496        }
3497        if elem.is_null() {
3498            return 0;
3499        }
3500        match (*elem).type_ {
3501            t if t == XML_TEXT_NODE as c_int
3502                || t == XML_CDATA_SECTION_NODE as c_int
3503                || t == XML_ENTITY_REF_NODE as c_int
3504                || t == XML_PI_NODE as c_int
3505                || t == XML_COMMENT_NODE as c_int
3506                || t == XML_XINCLUDE_START as c_int
3507                || t == XML_XINCLUDE_END as c_int =>
3508            {
3509                return 1;
3510            }
3511            t if t == XML_ELEMENT_NODE as c_int => {}
3512            _ => {
3513                vctxt_error_node(
3514                    ctxt,
3515                    elem,
3516                    b"unexpected element type\0" as *const u8 as *const c_char,
3517                );
3518                return 0;
3519            }
3520        }
3521
3522        let mut ret = 1;
3523        let mut extsubset = 0;
3524        let elem_decl = valid_get_elem_decl(ctxt, doc, elem, &mut extsubset);
3525        if elem_decl.is_null() {
3526            return 0;
3527        }
3528
3529        // Continuous (push) validation already checks the content model via
3530        // the vstate stack; skip the tree walk when active.
3531        if (*ctxt).vstateNr == 0 {
3532            match (*elem_decl).etype as u32 {
3533                t if t == XML_ELEMENT_TYPE_UNDEFINED as u32 => {
3534                    let msg = format!(
3535                        "No declaration for element {}\0",
3536                        string::xmlstr_to_string((*elem).name)
3537                    );
3538                    vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3539                    return 0;
3540                }
3541                t if t == XML_ELEMENT_TYPE_EMPTY as u32 => {
3542                    if !(*elem).children.is_null() {
3543                        let msg = format!(
3544                            "Element {} was declared EMPTY this one has content\0",
3545                            string::xmlstr_to_string((*elem).name)
3546                        );
3547                        vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3548                        ret = 0;
3549                    }
3550                }
3551                t if t == XML_ELEMENT_TYPE_ANY as u32 => {}
3552                t if t == XML_ELEMENT_TYPE_MIXED as u32 => {
3553                    if !(*elem_decl).content.is_null()
3554                        && (*(*elem_decl).content).type_ == XML_ELEMENT_CONTENT_PCDATA as c_int
3555                    {
3556                        // #PCDATA-only: any element child is an error
3557                        let mut child = (*elem).children;
3558                        while !child.is_null() {
3559                            if (*child).type_ == XML_ELEMENT_NODE as c_int {
3560                                let msg = format!(
3561                                    "Element {} was declared #PCDATA but contains non text nodes\0",
3562                                    string::xmlstr_to_string((*elem).name)
3563                                );
3564                                vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3565                                ret = 0;
3566                                break;
3567                            }
3568                            child = (*child).next;
3569                        }
3570                    } else {
3571                        // check each child element against the mixed list
3572                        let mut child = (*elem).children;
3573                        while !child.is_null() {
3574                            if (*child).type_ == XML_ELEMENT_NODE as c_int {
3575                                let mut fullname = (*child).name;
3576                                let mut own = false;
3577                                if !(*child).ns.is_null() && !(*(*child).ns).prefix.is_null() {
3578                                    let fnp = string::build_qname(
3579                                        (*child).name,
3580                                        (*(*child).ns).prefix,
3581                                        ptr::null_mut(),
3582                                        0,
3583                                    );
3584                                    if fnp.is_null() {
3585                                        vctxt_error(
3586                                            ctxt,
3587                                            b"Memory allocation failed : xmlValidateOneElement\0"
3588                                                as *const u8
3589                                                as *const c_char,
3590                                        );
3591                                        return 0;
3592                                    }
3593                                    fullname = fnp;
3594                                    own = true;
3595                                }
3596                                if validate_check_mixed(ctxt, (*elem_decl).content, fullname) != 1 {
3597                                    let msg = format!(
3598                                        "Element {} is not declared in {} list of possible children\0",
3599                                        string::xmlstr_to_string(fullname),
3600                                        string::xmlstr_to_string((*elem).name)
3601                                    );
3602                                    vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3603                                    ret = 0;
3604                                }
3605                                if own {
3606                                    allocator::xmlFreeImpl(fullname as *mut c_void);
3607                                }
3608                            }
3609                            child = (*child).next;
3610                        }
3611                    }
3612                }
3613                t if t == XML_ELEMENT_TYPE_ELEMENT as u32 => {
3614                    // Element-only content: collect child element names and
3615                    // check against the content model.
3616                    let mut names: Vec<*const xmlChar> = Vec::new();
3617                    let mut owned: Vec<*mut xmlChar> = Vec::new();
3618                    let mut child = (*elem).children;
3619                    while !child.is_null() {
3620                        if (*child).type_ == XML_ELEMENT_NODE as c_int {
3621                            let mut fullname = (*child).name;
3622                            if !(*child).ns.is_null() && !(*(*child).ns).prefix.is_null() {
3623                                let fnp = string::build_qname(
3624                                    (*child).name,
3625                                    (*(*child).ns).prefix,
3626                                    ptr::null_mut(),
3627                                    0,
3628                                );
3629                                if !fnp.is_null() {
3630                                    fullname = fnp;
3631                                    owned.push(fnp);
3632                                }
3633                            }
3634                            names.push(fullname);
3635                        }
3636                        child = (*child).next;
3637                    }
3638                    let result = dtd::valid_content_model((*elem_decl).content, &names);
3639                    for n in owned {
3640                        allocator::xmlFreeImpl(n as *mut c_void);
3641                    }
3642                    if result != dtd::ContentModelResult::Valid {
3643                        let msg = format!(
3644                            "Element {} content does not follow the DTD\0",
3645                            string::xmlstr_to_string((*elem).name)
3646                        );
3647                        vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3648                        ret = 0;
3649                    }
3650                }
3651                _ => {}
3652            }
3653
3654            // Required attributes + attribute value checks
3655            let mut attr = (*elem).properties;
3656            while !attr.is_null() {
3657                let aval = if !(*attr).children.is_null() {
3658                    (*(*attr).children).content
3659                } else {
3660                    ptr::null()
3661                };
3662                if validate_one_attribute(ctxt, doc, elem, attr, aval) == 0 {
3663                    ret = 0;
3664                }
3665                attr = (*attr).next;
3666            }
3667        }
3668        ret
3669    }
3670}
3671
3672// ═══════════════════════════════════════════════════════════════════════════════
3673// Streaming (push) validation — upstream valid.c xmlValidatePushElement /
3674// PushCData / PopElement + xmlValidBuildContentModel
3675// ═══════════════════════════════════════════════════════════════════════════════
3676//
3677// Upstream keeps a stack of validation states (one per open element). Each
3678// state holds the element declaration and, for ELEMENT content, a regexp
3679// exec context over the compiled content model. The candidate reproduces
3680// the same observable contract: per-push checks against the current state,
3681// "Misplaced"/"Text not allowed"/"Expecting more children" diagnostics,
3682// and the vstate push/pop stack on the public _xmlValidCtxt layout
3683// (vstate/vstateNr/vstateMax/vstateTab).
3684
3685/// Mirror of upstream `_xmlValidState` (valid.c): one entry per open element.
3686#[repr(C)]
3687struct ValidState {
3688    elem_decl: *mut _xmlElement,
3689    node: *mut _xmlNode,
3690    exec: *mut ContentModelExec,
3691}
3692
3693/// Find the declaration for an element (upstream xmlValidGetElemDecl).
3694/// Reports "No declaration for element %s" when absent.
3695unsafe fn valid_get_elem_decl(
3696    ctxt: *mut _xmlValidCtxt,
3697    doc: *mut _xmlDoc,
3698    elem: *mut _xmlNode,
3699    extsubset: *mut c_int,
3700) -> *mut _xmlElement {
3701    unsafe {
3702        if ctxt.is_null() || doc.is_null() || elem.is_null() || (*elem).name.is_null() {
3703            return ptr::null_mut();
3704        }
3705        if !extsubset.is_null() {
3706            *extsubset = 0;
3707        }
3708        let mut elem_decl = ptr::null_mut();
3709
3710        let prefix = if !(*elem).ns.is_null() && !(*(*elem).ns).prefix.is_null() {
3711            (*(*elem).ns).prefix
3712        } else {
3713            ptr::null()
3714        };
3715        if !prefix.is_null() {
3716            elem_decl = get_dtd_qelement_desc((*doc).intSubset, (*elem).name, prefix);
3717            if elem_decl.is_null() && !(*doc).extSubset.is_null() {
3718                elem_decl = get_dtd_qelement_desc((*doc).extSubset, (*elem).name, prefix);
3719                if !elem_decl.is_null() && !extsubset.is_null() {
3720                    *extsubset = 1;
3721                }
3722            }
3723        }
3724        if elem_decl.is_null() {
3725            // non-strict fallback: plain name against either subset
3726            elem_decl = get_dtd_qelement_desc((*doc).intSubset, (*elem).name, ptr::null());
3727            if elem_decl.is_null() && !(*doc).extSubset.is_null() {
3728                elem_decl = get_dtd_qelement_desc((*doc).extSubset, (*elem).name, ptr::null());
3729                if !elem_decl.is_null() && !extsubset.is_null() {
3730                    *extsubset = 1;
3731                }
3732            }
3733        }
3734        if elem_decl.is_null() {
3735            let msg = format!(
3736                "No declaration for element {}\0",
3737                string::xmlstr_to_string((*elem).name)
3738            );
3739            vctxt_error_node(ctxt, elem, msg.as_ptr() as *const c_char);
3740        }
3741        elem_decl
3742    }
3743}
3744
3745/// Upstream xmlValidateCheckMixed: is `qname` in the MIXED content list?
3746unsafe fn validate_check_mixed(
3747    ctxt: *mut _xmlValidCtxt,
3748    cont: *mut _xmlElementContent,
3749    qname: *const xmlChar,
3750) -> c_int {
3751    unsafe {
3752        let mut plen: c_int = 0;
3753        let has_colon = string::split_qname3(qname, &mut plen) != 0;
3754        // upstream xmlSplitQName3 returns the local-name pointer (NULL when
3755        // the qname has no colon); the candidate's split_qname3 returns the
3756        // prefix length, so the local part is qname + plen + 1.
3757        let local = if has_colon {
3758            qname.add(plen as usize + 1)
3759        } else {
3760            ptr::null()
3761        };
3762        let mut cur = cont;
3763        if local.is_null() {
3764            while !cur.is_null() {
3765                if (*cur).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int {
3766                    if (*cur).prefix.is_null() && string::xml_strcmp((*cur).name, qname) == 0 {
3767                        return 1;
3768                    }
3769                } else if (*cur).type_ == XML_ELEMENT_CONTENT_OR as c_int
3770                    && !(*cur).c1.is_null()
3771                    && (*(*cur).c1).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int
3772                {
3773                    if (*(*cur).c1).prefix.is_null()
3774                        && string::xml_strcmp((*(*cur).c1).name, qname) == 0
3775                    {
3776                        return 1;
3777                    }
3778                } else if (*cur).type_ != XML_ELEMENT_CONTENT_OR as c_int
3779                    || (*cur).c1.is_null()
3780                    || (*(*cur).c1).type_ != XML_ELEMENT_CONTENT_PCDATA as c_int
3781                {
3782                    vctxt_error(
3783                        ctxt,
3784                        b"Internal: MIXED struct corrupted\0" as *const u8 as *const c_char,
3785                    );
3786                    break;
3787                }
3788                cur = (*cur).c2;
3789            }
3790        } else {
3791            while !cur.is_null() {
3792                if (*cur).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int {
3793                    if !(*cur).prefix.is_null()
3794                        && prefix_matches((*cur).prefix, qname, plen)
3795                        && string::xml_strcmp((*cur).name, local) == 0
3796                    {
3797                        return 1;
3798                    }
3799                } else if (*cur).type_ == XML_ELEMENT_CONTENT_OR as c_int
3800                    && !(*cur).c1.is_null()
3801                    && (*(*cur).c1).type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int
3802                {
3803                    if !(*(*cur).c1).prefix.is_null()
3804                        && prefix_matches((*(*cur).c1).prefix, qname, plen)
3805                        && string::xml_strcmp((*(*cur).c1).name, local) == 0
3806                    {
3807                        return 1;
3808                    }
3809                } else if (*cur).type_ != XML_ELEMENT_CONTENT_OR as c_int
3810                    || (*cur).c1.is_null()
3811                    || (*(*cur).c1).type_ != XML_ELEMENT_CONTENT_PCDATA as c_int
3812                {
3813                    vctxt_error(
3814                        ctxt,
3815                        b"Internal: MIXED struct corrupted\0" as *const u8 as *const c_char,
3816                    );
3817                    break;
3818                }
3819                cur = (*cur).c2;
3820            }
3821        }
3822        0
3823    }
3824}
3825
3826/// Does `prefix` equal the first `len` bytes of `qname` (upstream
3827/// xmlStrncmp(prefix, qname, plen))?
3828unsafe fn prefix_matches(prefix: *const xmlChar, qname: *const xmlChar, len: c_int) -> bool {
3829    unsafe {
3830        let p = string::xmlstr_to_bytes(prefix);
3831        let q = string::xmlstr_to_bytes(qname);
3832        p.len() == len as usize && q.len() >= len as usize && p[..len as usize] == q[..len as usize]
3833    }
3834}
3835
3836/// Incremental content-model matcher stored in `_xmlElement.cont_model`.
3837///
3838/// The candidate's regex engine matches character-by-character, which does
3839/// not model upstream's whole-name content-model tokens, so the content
3840/// model is compiled into a dedicated small NFA over full element names.
3841/// Upstream builds the same automaton (xmlValidBuildAContentModel) and then
3842/// converts it with xmlRegFromAutomata; the observable push/pop contract is
3843/// identical (per-push "Misplaced" errors, completion checks on pop).
3844#[derive(Debug)]
3845#[repr(C)]
3846pub struct ContentModelNfa {
3847    /// Flat transition list: (from_state, name, to_state); name NULL = epsilon.
3848    transitions: Vec<(u32, *const xmlChar, u32)>,
3849    /// start state index
3850    start: u32,
3851    /// accepting state indices (match complete)
3852    accept: Vec<u32>,
3853}
3854
3855/// Runtime exec state for one open element's content model.
3856#[derive(Debug)]
3857#[repr(C)]
3858pub struct ContentModelExec {
3859    /// the compiled NFA
3860    nfa: *mut ContentModelNfa,
3861    /// current state set after epsilon closure
3862    current: Vec<u32>,
3863}
3864
3865/// Thompson-style NFA builder over the content tree.
3866struct NfaBuilder {
3867    transitions: Vec<(u32, *const xmlChar, u32)>,
3868    n_states: u32,
3869}
3870
3871impl NfaBuilder {
3872    const fn new() -> Self {
3873        NfaBuilder {
3874            transitions: Vec::new(),
3875            n_states: 0,
3876        }
3877    }
3878    const fn new_state(&mut self) -> u32 {
3879        let s = self.n_states;
3880        self.n_states += 1;
3881        s
3882    }
3883    fn eps(&mut self, from: u32, to: u32) {
3884        self.transitions.push((from, ptr::null(), to));
3885    }
3886    fn name_trans(&mut self, from: u32, name: *const xmlChar, to: u32) {
3887        self.transitions.push((from, name, to));
3888    }
3889}
3890
3891/// Compile one content-model subtree. Returns (in_state, out_states); the
3892/// occurrence quantifier on the node is applied by wrapping the fragment
3893/// with epsilon edges (standard Thompson construction, matching upstream's
3894/// automaton shape for OPT/MULT/PLUS).
3895unsafe fn compile_content_sub(
3896    b: &mut NfaBuilder,
3897    model: *mut _xmlElementContent,
3898) -> (u32, Vec<u32>) {
3899    if model.is_null() {
3900        let s = b.new_state();
3901        return (s, vec![s]);
3902    }
3903    let m = unsafe { &*model };
3904    let (mut in_s, outs) = match m.type_ as u32 {
3905        t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
3906            let s = b.new_state();
3907            let to = b.new_state();
3908            b.name_trans(s, m.name, to);
3909            (s, vec![to])
3910        }
3911        t if t == XML_ELEMENT_CONTENT_SEQ as u32 => {
3912            let (in1, out1) = compile_content_sub(b, m.c1);
3913            let (in2, out2) = compile_content_sub(b, m.c2);
3914            for &o in &out1 {
3915                b.eps(o, in2);
3916            }
3917            (in1, out2)
3918        }
3919        t if t == XML_ELEMENT_CONTENT_OR as u32 => {
3920            let (in1, out1) = compile_content_sub(b, m.c1);
3921            let (in2, out2) = compile_content_sub(b, m.c2);
3922            let s = b.new_state();
3923            b.eps(s, in1);
3924            b.eps(s, in2);
3925            let mut all = out1;
3926            all.extend(out2);
3927            (s, all)
3928        }
3929        // PCDATA cannot appear in an ELEMENT content model; the caller
3930        // rejects it before compiling (upstream xmlValidBuildAContentModel
3931        // emits "Found PCDATA in content model of %s"). A PCDATA node here
3932        // compiles to an empty fragment so a malformed tree cannot crash.
3933        _ => {
3934            let s = b.new_state();
3935            (s, vec![s])
3936        }
3937    };
3938    match m.ocur as u32 {
3939        o if o == XML_ELEMENT_CONTENT_OPT as u32 => {
3940            let s = b.new_state();
3941            b.eps(s, in_s);
3942            for &o2 in &outs {
3943                b.eps(s, o2);
3944            }
3945            in_s = s;
3946        }
3947        o if o == XML_ELEMENT_CONTENT_MULT as u32 => {
3948            let s = b.new_state();
3949            b.eps(s, in_s);
3950            for &o2 in &outs {
3951                b.eps(s, o2);
3952                b.eps(o2, s);
3953            }
3954            in_s = s;
3955        }
3956        o if o == XML_ELEMENT_CONTENT_PLUS as u32 => {
3957            let s = b.new_state();
3958            b.eps(s, in_s);
3959            for &o2 in &outs {
3960                b.eps(o2, s);
3961            }
3962            in_s = s;
3963        }
3964        _ => {}
3965    }
3966    (in_s, outs)
3967}
3968
3969/// Does the content tree contain a PCDATA node (illegal in ELEMENT models)?
3970unsafe fn content_has_pcdata(model: *mut _xmlElementContent) -> bool {
3971    if model.is_null() {
3972        return false;
3973    }
3974    unsafe {
3975        let m = &*model;
3976        if m.type_ == XML_ELEMENT_CONTENT_PCDATA as c_int {
3977            return true;
3978        }
3979        content_has_pcdata(m.c1) || content_has_pcdata(m.c2)
3980    }
3981}
3982
3983/// Compile an element content tree into a ContentModelNfa.
3984///
3985/// # SAFETY
3986///
3987/// - `content` must be a valid content tree or NULL (returns NULL).
3988unsafe fn build_content_nfa(content: *mut _xmlElementContent) -> *mut ContentModelNfa {
3989    unsafe {
3990        if content.is_null() {
3991            return ptr::null_mut();
3992        }
3993        let mut b = NfaBuilder::new();
3994        let (start, outs) = compile_content_sub(&mut b, content);
3995        let nfa = Box::new(ContentModelNfa {
3996            transitions: b.transitions,
3997            start,
3998            accept: outs,
3999        });
4000        Box::into_raw(nfa)
4001    }
4002}
4003
4004/// Free a compiled content-model NFA (called from xmlFreeElement).
4005///
4006/// # SAFETY
4007///
4008/// - `nfa` must be a pointer from build_content_nfa or NULL.
4009pub unsafe fn free_content_model_nfa(nfa: *mut ContentModelNfa) {
4010    if nfa.is_null() {
4011        return;
4012    }
4013    unsafe {
4014        ptr::drop_in_place(nfa);
4015        allocator::xmlFreeImpl(nfa as *mut c_void);
4016    }
4017}
4018
4019/// Epsilon closure of a state set.
4020unsafe fn eps_closure(nfa: &ContentModelNfa, states: &[u32]) -> Vec<u32> {
4021    let mut out = states.to_vec();
4022    let mut stack = states.to_vec();
4023    while let Some(s) = stack.pop() {
4024        for &(from, name, to) in &nfa.transitions {
4025            if from == s && name.is_null() && !out.contains(&to) {
4026                out.push(to);
4027                stack.push(to);
4028            }
4029        }
4030    }
4031    out.sort_unstable();
4032    out.dedup();
4033    out
4034}
4035
4036/// Create an exec context over a compiled content model. Returns NULL on OOM.
4037unsafe fn new_content_exec(nfa: *mut ContentModelNfa) -> *mut ContentModelExec {
4038    unsafe {
4039        let exec = allocator::xmlMallocImpl(size_of::<ContentModelExec>()) as *mut ContentModelExec;
4040        if exec.is_null() {
4041            return ptr::null_mut();
4042        }
4043        let cur = eps_closure(&*nfa, &[(*nfa).start]);
4044        ptr::write(&mut (*exec).nfa, nfa);
4045        ptr::write(&mut (*exec).current, cur);
4046        exec
4047    }
4048}
4049
4050/// Free an exec context.
4051unsafe fn free_content_exec(exec: *mut ContentModelExec) {
4052    if exec.is_null() {
4053        return;
4054    }
4055    unsafe {
4056        ptr::drop_in_place(&mut (*exec).current);
4057        allocator::xmlFreeImpl(exec as *mut c_void);
4058    }
4059}
4060
4061/// Push a full element name (or NULL = end of input) into the exec context.
4062///
4063/// Mirrors upstream xmlRegExecPushString contract: 1 = match complete,
4064/// 0 = more input needed, -1 = cannot continue (Misplaced).
4065unsafe fn content_exec_push(exec: *mut ContentModelExec, value: *const xmlChar) -> c_int {
4066    unsafe {
4067        if exec.is_null() {
4068            return -1;
4069        }
4070        let nfa = &*(*exec).nfa;
4071        if value.is_null() {
4072            let cur = eps_closure(nfa, &(*exec).current);
4073            return if cur.iter().any(|&s| nfa.accept.contains(&s)) {
4074                1
4075            } else {
4076                0
4077            };
4078        }
4079        let mut next: Vec<u32> = Vec::new();
4080        for &s in &(*exec).current {
4081            for &(from, name, to) in &nfa.transitions {
4082                if from == s && !name.is_null() && string::xml_strcmp(name, value) == 0 {
4083                    next.push(to);
4084                }
4085            }
4086        }
4087        next.sort_unstable();
4088        next.dedup();
4089        if next.is_empty() {
4090            return -1;
4091        }
4092        let closed = eps_closure(nfa, &next);
4093        (*exec).current = closed;
4094        if (*exec).current.iter().any(|&s| nfa.accept.contains(&s)) {
4095            1
4096        } else {
4097            0
4098        }
4099    }
4100}
4101
4102/// Upstream vstateVPush: push a validation state for an open element.
4103unsafe fn vstate_vpush(
4104    ctxt: *mut _xmlValidCtxt,
4105    elem_decl: *mut _xmlElement,
4106    node: *mut _xmlNode,
4107) -> c_int {
4108    unsafe {
4109        if (*ctxt).vstateNr >= (*ctxt).vstateMax {
4110            let new_max = if (*ctxt).vstateMax == 0 {
4111                10
4112            } else {
4113                (*ctxt).vstateMax * 2
4114            };
4115            let new_tab = allocator::xmlReallocImpl(
4116                (*ctxt).vstateTab,
4117                (new_max as usize) * size_of::<ValidState>(),
4118            ) as *mut ValidState;
4119            if new_tab.is_null() {
4120                vctxt_error(
4121                    ctxt,
4122                    b"Memory allocation failed : xmlValidCtxt\0" as *const u8 as *const c_char,
4123                );
4124                return -1;
4125            }
4126            (*ctxt).vstateTab = new_tab as *mut c_void;
4127            (*ctxt).vstateMax = new_max;
4128        }
4129        let idx = (*ctxt).vstateNr as usize;
4130        let tab = (*ctxt).vstateTab as *mut ValidState;
4131        (*tab.add(idx)).elem_decl = elem_decl;
4132        (*tab.add(idx)).node = node;
4133        (*tab.add(idx)).exec = ptr::null_mut();
4134        if !elem_decl.is_null() && (*elem_decl).etype == XML_ELEMENT_TYPE_ELEMENT as c_int {
4135            if (*elem_decl).cont_model.is_null() {
4136                validate_build_content_model(ctxt, elem_decl);
4137            }
4138            if !(*elem_decl).cont_model.is_null() {
4139                let exec = new_content_exec((*elem_decl).cont_model as *mut ContentModelNfa);
4140                if exec.is_null() {
4141                    vctxt_error(
4142                        ctxt,
4143                        b"Memory allocation failed : xmlValidCtxt\0" as *const u8 as *const c_char,
4144                    );
4145                    return -1;
4146                }
4147                (*tab.add(idx)).exec = exec;
4148            } else {
4149                let msg = format!(
4150                    "Failed to build content model regexp for {}\0",
4151                    string::xmlstr_to_string((*elem_decl).name)
4152                );
4153                vctxt_error_node(ctxt, node, msg.as_ptr() as *const c_char);
4154            }
4155        }
4156        (*ctxt).vstate = tab.add(idx) as *mut c_void;
4157        (*ctxt).vstateNr += 1;
4158        0
4159    }
4160}
4161
4162/// Upstream vstateVPop: pop the current validation state, freeing its exec.
4163unsafe fn vstate_vpop(ctxt: *mut _xmlValidCtxt) -> c_int {
4164    unsafe {
4165        if (*ctxt).vstateNr < 1 {
4166            return -1;
4167        }
4168        (*ctxt).vstateNr -= 1;
4169        let idx = (*ctxt).vstateNr as usize;
4170        let tab = (*ctxt).vstateTab as *mut ValidState;
4171        let elem_decl = (*tab.add(idx)).elem_decl;
4172        (*tab.add(idx)).elem_decl = ptr::null_mut();
4173        (*tab.add(idx)).node = ptr::null_mut();
4174        if !elem_decl.is_null()
4175            && (*elem_decl).etype == XML_ELEMENT_TYPE_ELEMENT as c_int
4176            && !(*tab.add(idx)).exec.is_null()
4177        {
4178            free_content_exec((*tab.add(idx)).exec);
4179        }
4180        (*tab.add(idx)).exec = ptr::null_mut();
4181        if (*ctxt).vstateNr >= 1 {
4182            (*ctxt).vstate = tab.add((*ctxt).vstateNr as usize - 1) as *mut c_void;
4183        } else {
4184            (*ctxt).vstate = ptr::null_mut();
4185        }
4186        0
4187    }
4188}
4189
4190/// Upstream `xmlValidBuildContentModel(ctxt, elem)`: compile the element's
4191/// content tree into a content-model NFA cached in `elem->contModel`.
4192/// Returns 1 on success, 0 on failure.
4193///
4194/// # SAFETY
4195///
4196/// - `ctxt` may be NULL; `elem` a valid pointer.
4197pub unsafe fn validate_build_content_model(
4198    ctxt: *mut _xmlValidCtxt,
4199    elem: *mut _xmlElement,
4200) -> c_int {
4201    unsafe {
4202        if ctxt.is_null() {
4203            return 0;
4204        }
4205        if (*elem).type_ != XML_ELEMENT_DECL as c_int {
4206            return 0;
4207        }
4208        if (*elem).etype != XML_ELEMENT_TYPE_ELEMENT as c_int {
4209            return 1;
4210        }
4211        if !(*elem).cont_model.is_null() {
4212            return 1;
4213        }
4214        if (*elem).content.is_null() {
4215            return 1;
4216        }
4217        if content_has_pcdata((*elem).content) {
4218            let msg = format!(
4219                "Found PCDATA in content model of {}\0",
4220                string::xmlstr_to_string((*elem).name)
4221            );
4222            vctxt_error_node(ctxt, elem as *mut _xmlNode, msg.as_ptr() as *const c_char);
4223            return 0;
4224        }
4225        let nfa = build_content_nfa((*elem).content);
4226        if nfa.is_null() {
4227            vctxt_error(
4228                ctxt,
4229                b"Memory allocation failed : xmlValidBuildContentModel\0" as *const u8
4230                    as *const c_char,
4231            );
4232            return 0;
4233        }
4234        (*elem).cont_model = nfa as *mut c_void;
4235        1
4236    }
4237}
4238
4239/// Upstream `xmlValidatePushElement(ctxt, doc, elem, qname)`: validate a
4240/// start tag against the parent's content model and push the new element's
4241/// validation state.
4242///
4243/// # SAFETY
4244///
4245/// - `ctxt` may be NULL; `doc`/`elem`/`qname` valid pointers or NULL.
4246pub unsafe fn validate_push_element(
4247    ctxt: *mut _xmlValidCtxt,
4248    doc: *mut _xmlDoc,
4249    elem: *mut _xmlNode,
4250    qname: *const xmlChar,
4251) -> c_int {
4252    unsafe {
4253        let mut ret = 1;
4254        if ctxt.is_null() {
4255            return 0;
4256        }
4257        if (*ctxt).vstateNr > 0 && !(*ctxt).vstate.is_null() {
4258            let state = (*ctxt).vstate as *mut ValidState;
4259            let elem_decl = (*state).elem_decl;
4260            if !elem_decl.is_null() {
4261                match (*elem_decl).etype as u32 {
4262                    t if t == XML_ELEMENT_TYPE_UNDEFINED as u32 => ret = 0,
4263                    t if t == XML_ELEMENT_TYPE_EMPTY as u32 => {
4264                        let msg = format!(
4265                            "Element {} was declared EMPTY this one has content\0",
4266                            string::xmlstr_to_string((*(*state).node).name)
4267                        );
4268                        vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4269                        ret = 0;
4270                    }
4271                    t if t == XML_ELEMENT_TYPE_ANY as u32 => {}
4272                    t if t == XML_ELEMENT_TYPE_MIXED as u32 => {
4273                        if !(*elem_decl).content.is_null()
4274                            && (*(*elem_decl).content).type_ == XML_ELEMENT_CONTENT_PCDATA as c_int
4275                        {
4276                            let msg = format!(
4277                                "Element {} was declared #PCDATA but contains non text nodes\0",
4278                                string::xmlstr_to_string((*(*state).node).name)
4279                            );
4280                            vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4281                            ret = 0;
4282                        } else {
4283                            ret = validate_check_mixed(ctxt, (*elem_decl).content, qname);
4284                            if ret != 1 {
4285                                let msg = format!(
4286                                    "Element {} is not declared in {} list of possible children\0",
4287                                    string::xmlstr_to_string(qname),
4288                                    string::xmlstr_to_string((*(*state).node).name)
4289                                );
4290                                vctxt_error_node(
4291                                    ctxt,
4292                                    (*state).node,
4293                                    msg.as_ptr() as *const c_char,
4294                                );
4295                            }
4296                        }
4297                    }
4298                    t if t == XML_ELEMENT_TYPE_ELEMENT as u32 && !(*state).exec.is_null() => {
4299                        ret = content_exec_push((*state).exec, qname);
4300                        if ret < 0 {
4301                            let msg = format!(
4302                                "Element {} content does not follow the DTD, Misplaced {}\0",
4303                                string::xmlstr_to_string((*(*state).node).name),
4304                                string::xmlstr_to_string(qname)
4305                            );
4306                            vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4307                            ret = 0;
4308                        } else {
4309                            ret = 1;
4310                        }
4311                    }
4312                    _ => {}
4313                }
4314            }
4315        }
4316        let mut extsubset = 0;
4317        let e_decl = valid_get_elem_decl(ctxt, doc, elem, &mut extsubset);
4318        // upstream ignores the vstateVPush return here
4319        let _ = vstate_vpush(ctxt, e_decl, elem);
4320        ret
4321    }
4322}
4323
4324/// Upstream `xmlValidatePushCData(ctxt, data, len)`: character data is only
4325/// legal as whitespace inside ELEMENT content.
4326///
4327/// # SAFETY
4328///
4329/// - `ctxt` may be NULL; `data` a valid buffer of `len` bytes or NULL.
4330pub unsafe fn validate_push_cdata(
4331    ctxt: *mut _xmlValidCtxt,
4332    data: *const xmlChar,
4333    len: c_int,
4334) -> c_int {
4335    unsafe {
4336        let mut ret = 1;
4337        if ctxt.is_null() {
4338            return 0;
4339        }
4340        if len <= 0 {
4341            return 1;
4342        }
4343        if (*ctxt).vstateNr > 0 && !(*ctxt).vstate.is_null() {
4344            let state = (*ctxt).vstate as *mut ValidState;
4345            let elem_decl = (*state).elem_decl;
4346            if !elem_decl.is_null() {
4347                match (*elem_decl).etype as u32 {
4348                    t if t == XML_ELEMENT_TYPE_UNDEFINED as u32 => ret = 0,
4349                    t if t == XML_ELEMENT_TYPE_EMPTY as u32 => {
4350                        let msg = format!(
4351                            "Element {} was declared EMPTY this one has content\0",
4352                            string::xmlstr_to_string((*(*state).node).name)
4353                        );
4354                        vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4355                        ret = 0;
4356                    }
4357                    t if t == XML_ELEMENT_TYPE_ANY as u32 || t == XML_ELEMENT_TYPE_MIXED as u32 => {
4358                    }
4359                    t if t == XML_ELEMENT_TYPE_ELEMENT as u32 => {
4360                        let bytes = core::slice::from_raw_parts(data, len as usize);
4361                        for &b in bytes {
4362                            if !is_blank_byte(b) {
4363                                let msg = format!(
4364                                    "Element {} content does not follow the DTD, Text not allowed\0",
4365                                    string::xmlstr_to_string((*(*state).node).name)
4366                                );
4367                                vctxt_error_node(
4368                                    ctxt,
4369                                    (*state).node,
4370                                    msg.as_ptr() as *const c_char,
4371                                );
4372                                ret = 0;
4373                                break;
4374                            }
4375                        }
4376                    }
4377                    _ => {}
4378                }
4379            }
4380        }
4381        ret
4382    }
4383}
4384
4385/// Upstream `xmlValidatePopElement(ctxt, doc, elem, qname)`: verify the
4386/// parent content model completed and pop the validation state.
4387///
4388/// # SAFETY
4389///
4390/// - `ctxt` may be NULL; `doc`/`elem`/`qname` valid pointers or NULL.
4391pub unsafe fn validate_pop_element(
4392    ctxt: *mut _xmlValidCtxt,
4393    _doc: *mut _xmlDoc,
4394    _elem: *mut _xmlNode,
4395    _qname: *const xmlChar,
4396) -> c_int {
4397    unsafe {
4398        let mut ret = 1;
4399        if ctxt.is_null() {
4400            return 0;
4401        }
4402        if (*ctxt).vstateNr > 0 && !(*ctxt).vstate.is_null() {
4403            let state = (*ctxt).vstate as *mut ValidState;
4404            let elem_decl = (*state).elem_decl;
4405            if !elem_decl.is_null()
4406                && (*elem_decl).etype == XML_ELEMENT_TYPE_ELEMENT as c_int
4407                && !(*state).exec.is_null()
4408            {
4409                ret = content_exec_push((*state).exec, ptr::null());
4410                if ret <= 0 {
4411                    let msg = format!(
4412                        "Element {} content does not follow the DTD, Expecting more children\0",
4413                        string::xmlstr_to_string((*(*state).node).name)
4414                    );
4415                    vctxt_error_node(ctxt, (*state).node, msg.as_ptr() as *const c_char);
4416                    ret = 0;
4417                } else {
4418                    ret = 1;
4419                }
4420            }
4421            let _ = vstate_vpop(ctxt);
4422        }
4423        ret
4424    }
4425}
4426
4427// ═══════════════════════════════════════════════════════════════════════════════
4428// Tests
4429// ═══════════════════════════════════════════════════════════════════════════════
4430
4431#[cfg(test)]
4432mod tests {
4433    use super::*;
4434    use crate::abi::allocator;
4435
4436    use crate::xml::dtd;
4437    use crate::xml::tree;
4438
4439    // ── Helpers ───────────────────────────────────────────────────────────
4440
4441    /// Create a null-terminated xmlChar* from a Rust string.
4442    unsafe fn c_str(s: &str) -> *const xmlChar {
4443        let bytes = s.as_bytes();
4444        let ptr = allocator::xmlMallocImpl(bytes.len() + 1) as *mut xmlChar;
4445        assert!(!ptr.is_null());
4446        std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len());
4447        *ptr.add(bytes.len()) = 0;
4448        ptr
4449    }
4450
4451    /// Create a simple document with a DTD for testing.
4452    unsafe fn make_test_doc() -> (*mut _xmlDoc, *mut _xmlDtd) {
4453        let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
4454        assert!(!doc.is_null());
4455
4456        let name = c_str("root");
4457        let ext_id = c_str("--//Test//DTD//EN");
4458        let sys_id = c_str("test.dtd");
4459        let dtd = dtd::create_int_subset(doc, name, ext_id, sys_id);
4460        assert!(!dtd.is_null());
4461
4462        (doc, dtd)
4463    }
4464
4465    /// Add an element declaration to a DTD.
4466    #[allow(unused)]
4467    unsafe fn add_elem_decl(
4468        dtd: *mut _xmlDtd,
4469        name: *const xmlChar,
4470        elem_type: c_int,
4471        content: *mut _xmlElementContent,
4472    ) -> *mut _xmlElement {
4473        dtd::add_element_decl(dtd, name, elem_type, content)
4474    }
4475
4476    /// Create a root element node.
4477    unsafe fn create_root_elem(doc: *mut _xmlDoc, name: *const xmlChar) -> *mut _xmlNode {
4478        let node = tree::new_node(ptr::null_mut(), name);
4479        assert!(!node.is_null());
4480        tree::add_child(doc as *mut _xmlNode, node);
4481        node
4482    }
4483
4484    /// Create a child element node.
4485    #[allow(unused)]
4486    unsafe fn create_child_elem(parent: *mut _xmlNode, name: *const xmlChar) -> *mut _xmlNode {
4487        let node = tree::new_node(ptr::null_mut(), name);
4488        assert!(!node.is_null());
4489        tree::add_child(parent, node);
4490        node
4491    }
4492
4493    // ── xmlValidateName tests ─────────────────────────────────────────────
4494
4495    #[test]
4496    fn test_validate_name_null() {
4497        unsafe {
4498            assert_eq!(validate_name(ptr::null()), 0);
4499        }
4500    }
4501
4502    #[test]
4503    fn test_validate_name_empty() {
4504        unsafe {
4505            let s = b"\0" as *const u8 as *const xmlChar;
4506            assert_eq!(validate_name(s), 0);
4507        }
4508    }
4509
4510    #[test]
4511    fn test_validate_name_valid() {
4512        unsafe {
4513            let tests = ["foo", "_bar", ":baz", "hello-world", "ns:elem", "a123"];
4514            for t in &tests {
4515                let s = c_str(t);
4516                assert_eq!(validate_name(s), 1, "Expected '{}' to be a valid Name", t);
4517                allocator::xmlFreeImpl(s as *mut c_void);
4518            }
4519        }
4520    }
4521
4522    #[test]
4523    fn test_validate_name_invalid() {
4524        unsafe {
4525            let tests = ["123abc", "-foo", ".bar", "foo bar", "a b"];
4526            for t in &tests {
4527                let s = c_str(t);
4528                assert_eq!(validate_name(s), 0, "Expected '{}' to be invalid", t);
4529                allocator::xmlFreeImpl(s as *mut c_void);
4530            }
4531        }
4532    }
4533
4534    #[test]
4535    fn test_validate_names_valid() {
4536        unsafe {
4537            let s = c_str("foo bar baz");
4538            assert_eq!(validate_names(s), 1);
4539            allocator::xmlFreeImpl(s as *mut c_void);
4540        }
4541    }
4542
4543    #[test]
4544    fn test_validate_names_invalid() {
4545        unsafe {
4546            let s = c_str("foo 123bar baz");
4547            assert_eq!(validate_names(s), 0);
4548            allocator::xmlFreeImpl(s as *mut c_void);
4549        }
4550    }
4551
4552    // ── xmlValidateNmtoken tests ──────────────────────────────────────────
4553
4554    #[test]
4555    fn test_validate_nmtoken_null() {
4556        unsafe {
4557            assert_eq!(validate_nmtoken(ptr::null()), 0);
4558        }
4559    }
4560
4561    #[test]
4562    fn test_validate_nmtoken_valid() {
4563        unsafe {
4564            let tests = ["foo", "123abc", "-foo", ".bar", "_test", ":ns"];
4565            for t in &tests {
4566                let s = c_str(t);
4567                assert_eq!(
4568                    validate_nmtoken(s),
4569                    1,
4570                    "Expected '{}' to be a valid NMTOKEN",
4571                    t
4572                );
4573                allocator::xmlFreeImpl(s as *mut c_void);
4574            }
4575        }
4576    }
4577
4578    #[test]
4579    fn test_validate_nmtoken_invalid() {
4580        unsafe {
4581            let s = c_str("foo bar");
4582            assert_eq!(validate_nmtoken(s), 0);
4583            allocator::xmlFreeImpl(s as *mut c_void);
4584        }
4585    }
4586
4587    #[test]
4588    fn test_validate_nmtokens_valid() {
4589        unsafe {
4590            let s = c_str("foo 123bar -baz");
4591            assert_eq!(validate_nmtokens(s), 1);
4592            allocator::xmlFreeImpl(s as *mut c_void);
4593        }
4594    }
4595
4596    // ── xmlValidateAttributeValue tests ───────────────────────────────────
4597
4598    #[test]
4599    fn test_validate_attribute_value_cdata() {
4600        unsafe {
4601            let s = c_str("anything goes here!@#$%^&*()");
4602            assert_eq!(validate_attribute_value(XML_ATTRIBUTE_CDATA as c_int, s), 1);
4603            allocator::xmlFreeImpl(s as *mut c_void);
4604
4605            // Empty CDATA is valid
4606            let empty = b"\0" as *const u8 as *const xmlChar;
4607            assert_eq!(
4608                validate_attribute_value(XML_ATTRIBUTE_CDATA as c_int, empty),
4609                1
4610            );
4611        }
4612    }
4613
4614    #[test]
4615    fn test_validate_attribute_value_id() {
4616        unsafe {
4617            let valid = c_str("myId");
4618            assert_eq!(
4619                validate_attribute_value(XML_ATTRIBUTE_ID as c_int, valid),
4620                1
4621            );
4622            allocator::xmlFreeImpl(valid as *mut c_void);
4623
4624            let invalid = c_str("123id");
4625            assert_eq!(
4626                validate_attribute_value(XML_ATTRIBUTE_ID as c_int, invalid),
4627                0
4628            );
4629            allocator::xmlFreeImpl(invalid as *mut c_void);
4630        }
4631    }
4632
4633    #[test]
4634    fn test_validate_attribute_value_idref() {
4635        unsafe {
4636            let valid = c_str("someId");
4637            assert_eq!(
4638                validate_attribute_value(XML_ATTRIBUTE_IDREF as c_int, valid),
4639                1
4640            );
4641            allocator::xmlFreeImpl(valid as *mut c_void);
4642        }
4643    }
4644
4645    #[test]
4646    fn test_validate_attribute_value_idrefs() {
4647        unsafe {
4648            let valid = c_str("id1 id2 id3");
4649            assert_eq!(
4650                validate_attribute_value(XML_ATTRIBUTE_IDREFS as c_int, valid),
4651                1
4652            );
4653            allocator::xmlFreeImpl(valid as *mut c_void);
4654
4655            let invalid = c_str("id1 123id");
4656            assert_eq!(
4657                validate_attribute_value(XML_ATTRIBUTE_IDREFS as c_int, invalid),
4658                0
4659            );
4660            allocator::xmlFreeImpl(invalid as *mut c_void);
4661        }
4662    }
4663
4664    #[test]
4665    fn test_validate_attribute_value_entity() {
4666        unsafe {
4667            let valid = c_str("myEntity");
4668            assert_eq!(
4669                validate_attribute_value(XML_ATTRIBUTE_ENTITY as c_int, valid),
4670                1
4671            );
4672            allocator::xmlFreeImpl(valid as *mut c_void);
4673        }
4674    }
4675
4676    #[test]
4677    fn test_validate_attribute_value_nmtoken() {
4678        unsafe {
4679            let valid = c_str("123abc");
4680            assert_eq!(
4681                validate_attribute_value(XML_ATTRIBUTE_NMTOKEN as c_int, valid),
4682                1
4683            );
4684            allocator::xmlFreeImpl(valid as *mut c_void);
4685
4686            let invalid = c_str("foo bar");
4687            assert_eq!(
4688                validate_attribute_value(XML_ATTRIBUTE_NMTOKEN as c_int, invalid),
4689                0
4690            );
4691            allocator::xmlFreeImpl(invalid as *mut c_void);
4692        }
4693    }
4694
4695    #[test]
4696    fn test_validate_attribute_value_null() {
4697        unsafe {
4698            // UPSTREAM-PARITY: xmlValidateAttributeValueInternal's switch
4699            // breaks out of CDATA and returns 1 (valid.c 2.15.0), even for
4700            // a NULL value; unknown types also fall through to 1.
4701            assert_eq!(
4702                validate_attribute_value(XML_ATTRIBUTE_CDATA as c_int, ptr::null()),
4703                1
4704            );
4705            assert_eq!(
4706                validate_attribute_value(XML_ATTRIBUTE_ID as c_int, ptr::null()),
4707                0
4708            );
4709        }
4710    }
4711
4712    // ── xmlValidateEnumeration tests ──────────────────────────────────────
4713
4714    #[test]
4715    fn test_validate_enumeration_valid() {
4716        unsafe {
4717            let ctxt = new_valid_ctxt();
4718            assert!(!ctxt.is_null());
4719
4720            let red = c_str("red");
4721            let green = c_str("green");
4722            let blue = c_str("blue");
4723
4724            let e3 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>()) as *mut _xmlEnumeration;
4725            (*e3).name = string::xml_strdup(blue);
4726            (*e3).next = ptr::null_mut();
4727
4728            let e2 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>()) as *mut _xmlEnumeration;
4729            (*e2).name = string::xml_strdup(green);
4730            (*e2).next = e3;
4731
4732            let e1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>()) as *mut _xmlEnumeration;
4733            (*e1).name = string::xml_strdup(red);
4734            (*e1).next = e2;
4735
4736            let value = c_str("green");
4737            assert_eq!(validate_enumeration(ctxt, value, e1), 1);
4738            assert_eq!((*ctxt).valid, 1);
4739
4740            allocator::xmlFreeImpl(value as *mut c_void);
4741            allocator::xmlFreeImpl(red as *mut c_void);
4742            allocator::xmlFreeImpl(green as *mut c_void);
4743            allocator::xmlFreeImpl(blue as *mut c_void);
4744            free_valid_ctxt(ctxt);
4745        }
4746    }
4747
4748    #[test]
4749    fn test_validate_enumeration_invalid() {
4750        unsafe {
4751            let ctxt = new_valid_ctxt();
4752            assert!(!ctxt.is_null());
4753
4754            let e1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>()) as *mut _xmlEnumeration;
4755            (*e1).name = string::xml_strdup(b"red\0" as *const u8 as *const xmlChar);
4756            (*e1).next = ptr::null_mut();
4757
4758            let value = c_str("yellow");
4759            assert_eq!(validate_enumeration(ctxt, value, e1), 0);
4760
4761            allocator::xmlFreeImpl(value as *mut c_void);
4762            free_valid_ctxt(ctxt);
4763        }
4764    }
4765
4766    // ── xmlValidateNotationUse tests ──────────────────────────────────────
4767
4768    #[test]
4769    fn test_validate_notation_use_valid() {
4770        unsafe {
4771            let (doc, dtd) = make_test_doc();
4772
4773            let notation_name = c_str("GIF");
4774            dtd::add_notation_decl(dtd, notation_name, ptr::null(), ptr::null());
4775
4776            let ctxt = new_valid_ctxt();
4777            assert!(!ctxt.is_null());
4778
4779            assert_eq!(validate_notation_use(ctxt, doc, notation_name), 1);
4780
4781            free_valid_ctxt(ctxt);
4782            tree::free_doc(doc);
4783        }
4784    }
4785
4786    #[test]
4787    fn test_validate_notation_use_invalid() {
4788        unsafe {
4789            let (doc, _dtd) = make_test_doc();
4790
4791            let ctxt = new_valid_ctxt();
4792            assert!(!ctxt.is_null());
4793
4794            let notation_name = c_str("UNDECLARED");
4795            assert_eq!(validate_notation_use(ctxt, doc, notation_name), 0);
4796
4797            free_valid_ctxt(ctxt);
4798            allocator::xmlFreeImpl(notation_name as *mut c_void);
4799            tree::free_doc(doc);
4800        }
4801    }
4802
4803    // ── xmlNewValidCtxt / xmlFreeValidCtxt tests ─────────────────────────
4804
4805    #[test]
4806    fn test_new_free_valid_ctxt() {
4807        unsafe {
4808            let ctxt = new_valid_ctxt();
4809            assert!(!ctxt.is_null());
4810            assert_eq!((*ctxt).valid, 1);
4811            assert!((*ctxt).node.is_null());
4812            free_valid_ctxt(ctxt);
4813        }
4814    }
4815
4816    #[test]
4817    fn test_free_valid_ctxt_null() {
4818        unsafe {
4819            free_valid_ctxt(ptr::null_mut());
4820        }
4821    }
4822
4823    // ── xmlSetValidErrors tests ──────────────────────────────────────────
4824
4825    #[test]
4826    fn test_set_valid_errors_null() {
4827        unsafe {
4828            set_valid_errors(ptr::null_mut(), None, None, ptr::null_mut());
4829        }
4830    }
4831
4832    // ── xmlValidateElement tests ──────────────────────────────────────────
4833
4834    #[test]
4835    fn test_validate_element_no_dtd() {
4836        unsafe {
4837            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
4838            assert!(!doc.is_null());
4839
4840            let root_name = c_str("root");
4841            let root = create_root_elem(doc, root_name);
4842
4843            let ctxt = new_valid_ctxt();
4844            assert!(!ctxt.is_null());
4845
4846            // No DTD — validation passes (returns 1)
4847            assert_eq!(validate_element(ctxt, doc, root), 1);
4848
4849            free_valid_ctxt(ctxt);
4850            tree::free_doc(doc);
4851        }
4852    }
4853
4854    #[test]
4855    fn test_validate_element_empty_valid() {
4856        unsafe {
4857            let (doc, dtd) = make_test_doc();
4858
4859            let root_name = c_str("root");
4860            add_elem_decl(
4861                dtd,
4862                root_name,
4863                XML_ELEMENT_TYPE_EMPTY as c_int,
4864                ptr::null_mut(),
4865            );
4866
4867            let root = create_root_elem(doc, root_name);
4868
4869            let ctxt = new_valid_ctxt();
4870            assert!(!ctxt.is_null());
4871
4872            assert_eq!(validate_element(ctxt, doc, root), 1);
4873
4874            free_valid_ctxt(ctxt);
4875            tree::free_doc(doc);
4876        }
4877    }
4878
4879    #[test]
4880    fn test_validate_element_undeclared() {
4881        unsafe {
4882            let (doc, _dtd) = make_test_doc();
4883
4884            let root_name = c_str("root");
4885            let root = create_root_elem(doc, root_name);
4886
4887            let ctxt = new_valid_ctxt();
4888            assert!(!ctxt.is_null());
4889
4890            // Element not declared — validation fails
4891            assert_eq!(validate_element(ctxt, doc, root), 0);
4892
4893            free_valid_ctxt(ctxt);
4894            tree::free_doc(doc);
4895        }
4896    }
4897
4898    #[test]
4899    fn test_validate_element_with_content() {
4900        unsafe {
4901            let (doc, dtd) = make_test_doc();
4902
4903            // Create element declarations
4904            let root_name = c_str("root");
4905            let child_name = c_str("child");
4906
4907            // Root content model: child+
4908            let child_content =
4909                dtd::create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
4910            assert!(!child_content.is_null());
4911            (*child_content).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
4912
4913            add_elem_decl(
4914                dtd,
4915                root_name,
4916                XML_ELEMENT_TYPE_ELEMENT as c_int,
4917                child_content,
4918            );
4919            add_elem_decl(
4920                dtd,
4921                child_name,
4922                XML_ELEMENT_TYPE_EMPTY as c_int,
4923                ptr::null_mut(),
4924            );
4925
4926            let root = create_root_elem(doc, root_name);
4927            let _child = create_child_elem(root, child_name);
4928
4929            let ctxt = new_valid_ctxt();
4930            assert!(!ctxt.is_null());
4931
4932            assert_eq!(validate_element(ctxt, doc, root), 1);
4933
4934            free_valid_ctxt(ctxt);
4935            tree::free_doc(doc);
4936        }
4937    }
4938
4939    #[test]
4940    fn test_validate_element_invalid_content() {
4941        unsafe {
4942            let (doc, dtd) = make_test_doc();
4943
4944            let root_name = c_str("root");
4945            let child_name = c_str("child");
4946            let wrong_name = c_str("wrong");
4947
4948            // Root content model: child+
4949            let child_content =
4950                dtd::create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
4951            assert!(!child_content.is_null());
4952            (*child_content).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
4953
4954            add_elem_decl(
4955                dtd,
4956                root_name,
4957                XML_ELEMENT_TYPE_ELEMENT as c_int,
4958                child_content,
4959            );
4960            add_elem_decl(
4961                dtd,
4962                child_name,
4963                XML_ELEMENT_TYPE_EMPTY as c_int,
4964                ptr::null_mut(),
4965            );
4966            add_elem_decl(
4967                dtd,
4968                wrong_name,
4969                XML_ELEMENT_TYPE_EMPTY as c_int,
4970                ptr::null_mut(),
4971            );
4972
4973            let root = create_root_elem(doc, root_name);
4974            // Add "wrong" child instead of "child"
4975            create_child_elem(root, wrong_name);
4976
4977            let ctxt = new_valid_ctxt();
4978            assert!(!ctxt.is_null());
4979
4980            assert_eq!(validate_element(ctxt, doc, root), 0);
4981
4982            free_valid_ctxt(ctxt);
4983            tree::free_doc(doc);
4984        }
4985    }
4986
4987    // ── xmlValidateRoot tests ─────────────────────────────────────────────
4988
4989    #[test]
4990    fn test_validate_root_match() {
4991        unsafe {
4992            let (doc, dtd) = make_test_doc();
4993
4994            let root_name = c_str("root");
4995            add_elem_decl(
4996                dtd,
4997                root_name,
4998                XML_ELEMENT_TYPE_EMPTY as c_int,
4999                ptr::null_mut(),
5000            );
5001            create_root_elem(doc, root_name);
5002
5003            let ctxt = new_valid_ctxt();
5004            assert!(!ctxt.is_null());
5005
5006            assert_eq!(validate_root(ctxt, doc), 1);
5007
5008            free_valid_ctxt(ctxt);
5009            tree::free_doc(doc);
5010        }
5011    }
5012
5013    #[test]
5014    fn test_validate_root_no_dtd() {
5015        unsafe {
5016            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5017            assert!(!doc.is_null());
5018
5019            let root_name = c_str("root");
5020            create_root_elem(doc, root_name);
5021
5022            let ctxt = new_valid_ctxt();
5023            assert!(!ctxt.is_null());
5024
5025            // No DTD — passes
5026            assert_eq!(validate_root(ctxt, doc), 1);
5027
5028            free_valid_ctxt(ctxt);
5029            tree::free_doc(doc);
5030        }
5031    }
5032
5033    // ── xmlValidateDocument tests ─────────────────────────────────────────
5034
5035    #[test]
5036    fn test_validate_document_valid() {
5037        unsafe {
5038            let (doc, dtd) = make_test_doc();
5039
5040            let root_name = c_str("root");
5041            add_elem_decl(
5042                dtd,
5043                root_name,
5044                XML_ELEMENT_TYPE_EMPTY as c_int,
5045                ptr::null_mut(),
5046            );
5047            create_root_elem(doc, root_name);
5048
5049            let ctxt = new_valid_ctxt();
5050            assert!(!ctxt.is_null());
5051
5052            assert_eq!(validate_document(ctxt, doc), 1);
5053
5054            free_valid_ctxt(ctxt);
5055            tree::free_doc(doc);
5056        }
5057    }
5058
5059    #[test]
5060    fn test_validate_document_no_root() {
5061        unsafe {
5062            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5063            assert!(!doc.is_null());
5064
5065            let ctxt = new_valid_ctxt();
5066            assert!(!ctxt.is_null());
5067
5068            assert_eq!(validate_document(ctxt, doc), 0);
5069
5070            free_valid_ctxt(ctxt);
5071            tree::free_doc(doc);
5072        }
5073    }
5074
5075    // ── xmlValidateContent tests ──────────────────────────────────────────
5076
5077    #[test]
5078    fn test_validate_content_valid() {
5079        unsafe {
5080            let (doc, dtd) = make_test_doc();
5081
5082            let root_name = c_str("root");
5083            let child_name = c_str("child");
5084
5085            let child_content =
5086                dtd::create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
5087            assert!(!child_content.is_null());
5088
5089            add_elem_decl(
5090                dtd,
5091                root_name,
5092                XML_ELEMENT_TYPE_ELEMENT as c_int,
5093                child_content,
5094            );
5095            add_elem_decl(
5096                dtd,
5097                child_name,
5098                XML_ELEMENT_TYPE_EMPTY as c_int,
5099                ptr::null_mut(),
5100            );
5101
5102            let root = create_root_elem(doc, root_name);
5103            create_child_elem(root, child_name);
5104
5105            let ctxt = new_valid_ctxt();
5106            assert!(!ctxt.is_null());
5107
5108            assert_eq!(validate_content(ctxt, root, doc), 1);
5109
5110            free_valid_ctxt(ctxt);
5111            tree::free_doc(doc);
5112        }
5113    }
5114
5115    // ── xmlIsMixedElement / xmlIsEmptyElement tests ───────────────────────
5116
5117    #[test]
5118    fn test_is_mixed_element() {
5119        unsafe {
5120            let (doc, dtd) = make_test_doc();
5121            let name = c_str("mixedElem");
5122            add_elem_decl(dtd, name, XML_ELEMENT_TYPE_MIXED as c_int, ptr::null_mut());
5123
5124            assert_eq!(is_mixed_element(doc, name), 1);
5125
5126            let other = c_str("other");
5127            assert_eq!(is_mixed_element(doc, other), 0);
5128
5129            allocator::xmlFreeImpl(other as *mut c_void);
5130            tree::free_doc(doc);
5131        }
5132    }
5133
5134    #[test]
5135    fn test_is_empty_element() {
5136        unsafe {
5137            let (doc, dtd) = make_test_doc();
5138            let name = c_str("emptyElem");
5139            add_elem_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
5140
5141            assert_eq!(is_empty_element(doc, name), 1);
5142
5143            let other = c_str("other");
5144            assert_eq!(is_empty_element(doc, other), 0);
5145
5146            allocator::xmlFreeImpl(other as *mut c_void);
5147            tree::free_doc(doc);
5148        }
5149    }
5150
5151    #[test]
5152    fn test_is_mixed_element_no_dtd() {
5153        unsafe {
5154            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5155            assert!(!doc.is_null());
5156
5157            let name = c_str("foo");
5158            assert_eq!(is_mixed_element(doc, name), 0);
5159
5160            allocator::xmlFreeImpl(name as *mut c_void);
5161            tree::free_doc(doc);
5162        }
5163    }
5164
5165    #[test]
5166    fn test_is_empty_element_no_dtd() {
5167        unsafe {
5168            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
5169            assert!(!doc.is_null());
5170
5171            let name = c_str("foo");
5172            assert_eq!(is_empty_element(doc, name), 0);
5173
5174            allocator::xmlFreeImpl(name as *mut c_void);
5175            tree::free_doc(doc);
5176        }
5177    }
5178
5179    // ── xmlValidateDtd tests ──────────────────────────────────────────────
5180
5181    #[test]
5182    fn test_validate_dtd_null() {
5183        unsafe {
5184            let ctxt = new_valid_ctxt();
5185            assert!(!ctxt.is_null());
5186            assert_eq!(validate_dtd(ctxt, ptr::null_mut(), ptr::null_mut()), 0);
5187            free_valid_ctxt(ctxt);
5188        }
5189    }
5190
5191    // ── Additional edge case tests ────────────────────────────────────────
5192
5193    #[test]
5194    fn test_validate_element_null() {
5195        unsafe {
5196            let (doc, _dtd) = make_test_doc();
5197            let ctxt = new_valid_ctxt();
5198            assert!(!ctxt.is_null());
5199
5200            assert_eq!(validate_element(ctxt, doc, ptr::null_mut()), 0);
5201
5202            free_valid_ctxt(ctxt);
5203            tree::free_doc(doc);
5204        }
5205    }
5206
5207    #[test]
5208    fn test_validate_document_null() {
5209        unsafe {
5210            let ctxt = new_valid_ctxt();
5211            assert!(!ctxt.is_null());
5212
5213            assert_eq!(validate_document(ctxt, ptr::null_mut()), 0);
5214            assert_eq!(validate_document(ptr::null_mut(), ptr::null_mut()), 0);
5215
5216            free_valid_ctxt(ctxt);
5217        }
5218    }
5219
5220    #[test]
5221    fn test_validate_document_final_null() {
5222        unsafe {
5223            let ctxt = new_valid_ctxt();
5224            assert!(!ctxt.is_null());
5225
5226            assert_eq!(validate_document_final(ctxt, ptr::null_mut()), 0);
5227            assert_eq!(validate_document_final(ptr::null_mut(), ptr::null_mut()), 0);
5228
5229            free_valid_ctxt(ctxt);
5230        }
5231    }
5232
5233    #[test]
5234    fn test_validate_attribute_decl_null() {
5235        unsafe {
5236            let ctxt = new_valid_ctxt();
5237            assert!(!ctxt.is_null());
5238
5239            assert_eq!(
5240                validate_attribute_decl(ctxt, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()),
5241                0
5242            );
5243
5244            free_valid_ctxt(ctxt);
5245        }
5246    }
5247
5248    #[test]
5249    fn test_validate_content_null() {
5250        unsafe {
5251            let ctxt = new_valid_ctxt();
5252            assert!(!ctxt.is_null());
5253
5254            assert_eq!(validate_content(ctxt, ptr::null_mut(), ptr::null_mut()), 0);
5255
5256            free_valid_ctxt(ctxt);
5257        }
5258    }
5259
5260    #[test]
5261    fn test_validate_root_null() {
5262        unsafe {
5263            assert_eq!(validate_root(ptr::null_mut(), ptr::null_mut()), 0);
5264        }
5265    }
5266
5267    #[test]
5268    fn test_validate_enumeration_null() {
5269        unsafe {
5270            let ctxt = new_valid_ctxt();
5271            assert!(!ctxt.is_null());
5272
5273            assert_eq!(validate_enumeration(ctxt, ptr::null(), ptr::null_mut()), 0);
5274
5275            free_valid_ctxt(ctxt);
5276        }
5277    }
5278
5279    #[test]
5280    fn test_validate_notation_use_null() {
5281        unsafe {
5282            let ctxt = new_valid_ctxt();
5283            assert!(!ctxt.is_null());
5284
5285            assert_eq!(validate_notation_use(ctxt, ptr::null_mut(), ptr::null()), 0);
5286
5287            free_valid_ctxt(ctxt);
5288        }
5289    }
5290
5291    #[test]
5292    fn test_validate_name_start_characters() {
5293        unsafe {
5294            // Test some Unicode name characters
5295            let name = c_str("\u{C0}lph\u{E0}");
5296            assert_eq!(validate_name(name), 1);
5297            allocator::xmlFreeImpl(name as *mut c_void);
5298        }
5299    }
5300
5301    #[test]
5302    fn test_validate_names_single() {
5303        unsafe {
5304            let s = c_str("singleName");
5305            assert_eq!(validate_names(s), 1);
5306            allocator::xmlFreeImpl(s as *mut c_void);
5307        }
5308    }
5309
5310    #[test]
5311    fn test_validate_nmtokens_single() {
5312        unsafe {
5313            let s = c_str("123abc");
5314            assert_eq!(validate_nmtokens(s), 1);
5315            allocator::xmlFreeImpl(s as *mut c_void);
5316        }
5317    }
5318
5319    #[test]
5320    fn test_validate_nmtokens_invalid() {
5321        unsafe {
5322            let s = c_str("foo\tbar"); // tab separated
5323            assert_eq!(validate_nmtokens(s), 1); // tab is whitespace
5324            allocator::xmlFreeImpl(s as *mut c_void);
5325
5326            // An NMTOKEN with invalid characters should fail
5327            let s2 = c_str("foo@bar");
5328            assert_eq!(validate_nmtokens(s2), 0);
5329            allocator::xmlFreeImpl(s2 as *mut c_void);
5330        }
5331    }
5332
5333    #[test]
5334    fn test_validate_attribute_value_empty_non_cdata() {
5335        unsafe {
5336            let empty = b"\0" as *const u8 as *const xmlChar;
5337            assert_eq!(
5338                validate_attribute_value(XML_ATTRIBUTE_ID as c_int, empty),
5339                0
5340            );
5341            assert_eq!(
5342                validate_attribute_value(XML_ATTRIBUTE_IDREF as c_int, empty),
5343                0
5344            );
5345            assert_eq!(
5346                validate_attribute_value(XML_ATTRIBUTE_NMTOKEN as c_int, empty),
5347                0
5348            );
5349        }
5350    }
5351
5352    #[test]
5353    fn test_validate_attribute_value_unknown_type() {
5354        unsafe {
5355            // UPSTREAM-PARITY: unknown attribute types fall through to the
5356            // default return of 1 (valid.c xmlValidateAttributeValueInternal).
5357            let s = c_str("test");
5358            assert_eq!(validate_attribute_value(999, s), 1);
5359            allocator::xmlFreeImpl(s as *mut c_void);
5360        }
5361    }
5362
5363    #[test]
5364    fn test_validate_element_any_content() {
5365        unsafe {
5366            let (doc, dtd) = make_test_doc();
5367
5368            let root_name = c_str("root");
5369            add_elem_decl(
5370                dtd,
5371                root_name,
5372                XML_ELEMENT_TYPE_ANY as c_int,
5373                ptr::null_mut(),
5374            );
5375
5376            let child_name = c_str("child");
5377            add_elem_decl(
5378                dtd,
5379                child_name,
5380                XML_ELEMENT_TYPE_EMPTY as c_int,
5381                ptr::null_mut(),
5382            );
5383
5384            let root = create_root_elem(doc, root_name);
5385            create_child_elem(root, child_name);
5386
5387            let ctxt = new_valid_ctxt();
5388            assert!(!ctxt.is_null());
5389
5390            // ANY content allows any children
5391            assert_eq!(validate_element(ctxt, doc, root), 1);
5392
5393            free_valid_ctxt(ctxt);
5394            tree::free_doc(doc);
5395        }
5396    }
5397
5398    #[test]
5399    fn test_validate_element_empty_with_child() {
5400        unsafe {
5401            let (doc, dtd) = make_test_doc();
5402
5403            let root_name = c_str("root");
5404            add_elem_decl(
5405                dtd,
5406                root_name,
5407                XML_ELEMENT_TYPE_EMPTY as c_int,
5408                ptr::null_mut(),
5409            );
5410
5411            let child_name = c_str("child");
5412            add_elem_decl(
5413                dtd,
5414                child_name,
5415                XML_ELEMENT_TYPE_EMPTY as c_int,
5416                ptr::null_mut(),
5417            );
5418
5419            let root = create_root_elem(doc, root_name);
5420            create_child_elem(root, child_name);
5421
5422            let ctxt = new_valid_ctxt();
5423            assert!(!ctxt.is_null());
5424
5425            // EMPTY element with child — validation fails
5426            assert_eq!(validate_element(ctxt, doc, root), 0);
5427
5428            free_valid_ctxt(ctxt);
5429            tree::free_doc(doc);
5430        }
5431    }
5432
5433    #[test]
5434    fn test_validate_dtd_final_null() {
5435        unsafe {
5436            assert_eq!(validate_dtd_final(ptr::null_mut(), ptr::null_mut()), 0);
5437        }
5438    }
5439}