Skip to main content

libxml_rs/xml/validation/
mod.rs

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