Skip to main content

libxml_rs/xml/dtd/
mod.rs

1//! DTD declarations handling (§24, §85 Phase 6).
2//!
3//! Internal subsets, external subsets, element declarations, attribute
4//! declarations, default attributes, notations, content models.
5//!
6//! This module provides the foundational types and functions for DTD
7//! validation, used by the validation, RELAX NG, XML Schema, and
8//! Schematron modules.
9//!
10//! # Upstream contract
11//!
12//! Mirrors the DTD declaration machinery of upstream valid.c and parser.c
13//! (SRC-LIBXML2-2.15.0, oracle tree `oracle/historical/src/libxml2-2.15.0/`):
14//! xmlCreateIntSubset, xmlNewDtd, xmlAddElementDecl, xmlAddAttributeDecl,
15//! xmlAddEntity, xmlAddNotationDecl, xmlGetDtdElementDesc and the DTD hash
16//! tables. Parity target: the system libxml2 2.15.3 oracle.
17//!
18//! # Conceptual behavior
19//!
20//! Internal subsets, external subsets, element declarations, attribute
21//! declarations, default attributes, notations and content models. The DTD
22//! node joins doc->children before the first element (xmlCreateIntSubset
23//! semantics, R-000164); declaration hash tables are created lazily by the
24//! xmlAdd* functions.
25//!
26//! # Ownership & safety invariants
27//!
28//! Ownership: the DTD is owned by the document; declarations are owned by the
29//! DTD hash tables; xmlFreeDtd frees only non-declaration children and runs
30//! the child walk before the hash-table frees (double-free fix, R-000164).
31//! SAFETY: `_xmlElement` must keep the upstream 104-byte layout (R-000139) or
32//! every xmlMalloc(sizeof(_xmlElement)) under-allocates.
33//!
34//! # Historical quirks & epochs
35//!
36//! R-000139 (Phase 11.1-I) rewrote the Rust `_xmlElement` mirror from 56 to
37//! 104 bytes after the RUST-MIRROR-ABI court caught the drift; R-000164
38//! (11.1-N) fixed the internalSubset path (xmlNewDtd to xmlCreateIntSubset),
39//! the element-decl type_ carrying XML_ELEMENT_DECL, ATTLISTs for undeclared
40//! elements creating UNDEFINED placeholders, and the attribute hash keyed
41//! (name,prefix,elem) as upstream.
42//!
43//! # Deliberate oddities
44//!
45//! Deliberate oddities: the DTD internal subset is written as part of
46//! doc->children (not a side structure); element declarations store the
47//! element type in etype while type_ holds XML_ELEMENT_DECL — matching
48//! upstream field semantics exactly.
49//!
50//! # Proving courts
51//!
52//! DTD, PARSER, RELAXNG and XSD court families; TREE-001 (DTD node chain,
53//! decl trees, hash order), RUST-MIRROR-ABI (struct layout), header-compile
54//! 595/595 and `cargo test --lib`.
55//!
56//! # Tempting simplifications that would break parity
57//!
58//! A tempting simplification is freeing declaration nodes from the DTD child
59//! list in addition to the hash tables — the exact double free R-000164
60//! fixed. Do not eagerly create the hash tables: laziness matches
61//! xmlGetDtdElementDesc semantics and TREE-001 fingerprints. Do not key the
62//! attribute table (elem,name): upstream is (name,prefix,elem).
63
64use core::ffi::c_void;
65use core::ptr;
66use std::os::raw::c_int;
67
68use crate::abi::allocator;
69use crate::abi::structs::*;
70use crate::abi::types::xmlElementContentOccur::*;
71use crate::abi::types::xmlElementContentType::*;
72use crate::abi::types::xmlElementType::*;
73use crate::abi::types::xmlElementTypeVal::*;
74use crate::abi::types::*;
75use crate::xml::hash;
76use crate::xml::string;
77
78#[cfg(test)]
79use crate::abi::types::xmlAttributeDefault::*;
80#[cfg(test)]
81use crate::abi::types::xmlAttributeType::*;
82
83// ═══════════════════════════════════════════════════════════════════════════════
84// DTD Access
85// ═══════════════════════════════════════════════════════════════════════════════
86
87/// Get the internal subset of a document.
88///
89/// # UPSTREAM-PARITY
90///
91/// ```c
92/// xmlDtdPtr xmlGetIntSubset(const xmlDoc *doc);
93/// ```
94///
95/// Returns a pointer to the DTD, or NULL if none.
96///
97/// # Safety
98///
99/// - `doc` must be a valid pointer to an `_xmlDoc`, or NULL.
100pub const fn get_int_subset(doc: *const _xmlDoc) -> *mut _xmlDtd {
101    if doc.is_null() {
102        return ptr::null_mut();
103    }
104    unsafe { (*doc).intSubset }
105}
106
107/// Create an internal subset (DTD) for a document.
108///
109/// # UPSTREAM-PARITY
110///
111/// ```c
112/// xmlDtdPtr xmlCreateIntSubset(xmlDocPtr doc, const xmlChar *name,
113///                              const xmlChar *ExternalID, const xmlChar *SystemID);
114/// ```
115///
116/// Creates a new DTD and attaches it as the document's internal subset.
117/// If the document already has an internal subset, it is replaced.
118///
119/// # SAFETY
120///
121/// - `doc` may be NULL — upstream still allocates and returns an
122///   unattached DTD in that case (HOSTILE-ABI A37); a NULL `doc` skips the
123///   child-chain attachment.
124/// - `name`, `ExternalID`, `SystemID` must be valid null-terminated strings or NULL.
125pub unsafe fn create_int_subset(
126    doc: *mut _xmlDoc,
127    name: *const xmlChar,
128    ExternalID: *const xmlChar,
129    SystemID: *const xmlChar,
130) -> *mut _xmlDtd {
131    if !doc.is_null() {
132        // UPSTREAM-PARITY (tree.c xmlCreateIntSubset): if the document already
133        // has an internal subset, return it — never create a second DTD node.
134        if !(*doc).intSubset.is_null() {
135            return (*doc).intSubset;
136        }
137    }
138
139    // SAFETY: Allocate zero-initialized memory for the DTD.
140    let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
141    if dtd.is_null() {
142        return ptr::null_mut();
143    }
144
145    unsafe {
146        (*dtd).type_ = XML_DTD_NODE as c_int;
147        (*dtd).name = string::xml_strdup(name);
148        (*dtd).ExternalID = string::xml_strdup(ExternalID);
149        (*dtd).SystemID = string::xml_strdup(SystemID);
150        (*dtd).parent = doc;
151        (*dtd).doc = doc;
152
153        // UPSTREAM-PARITY (tree.c xmlCreateIntSubset): the declaration hash
154        // tables are created lazily by the xmlAdd* functions on first use;
155        // an empty DTD exposes NULL table pointers.
156
157        if !doc.is_null() {
158            // Attach to document
159            (*doc).intSubset = dtd;
160
161            // UPSTREAM-PARITY (tree.c xmlCreateIntSubset): the DTD node is
162            // inserted into the document's child chain immediately before the
163            // first element node (comments/PIs in the prolog stay ahead of it).
164            let doc_children = (*doc).children;
165            if doc_children.is_null() {
166                (*doc).children = dtd as *mut _xmlNode;
167                (*doc).last = dtd as *mut _xmlNode;
168            } else {
169                let mut next = doc_children;
170                while !next.is_null() && (*next).type_ != XML_ELEMENT_NODE as c_int {
171                    next = (*next).next;
172                }
173                if next.is_null() {
174                    (*dtd).prev = (*doc).last;
175                    (*(*doc).last).next = dtd as *mut _xmlNode;
176                    (*doc).last = dtd as *mut _xmlNode;
177                } else {
178                    (*dtd).next = next;
179                    (*dtd).prev = (*next).prev;
180                    if (*dtd).prev.is_null() {
181                        (*doc).children = dtd as *mut _xmlNode;
182                    } else {
183                        (*(*dtd).prev).next = dtd as *mut _xmlNode;
184                    }
185                    (*next).prev = dtd as *mut _xmlNode;
186                }
187            }
188        }
189    }
190
191    dtd
192}
193
194/// Create a new DTD.
195///
196/// # UPSTREAM-PARITY
197///
198/// ```c
199/// xmlDtdPtr xmlNewDtd(xmlDocPtr doc, const xmlChar *name,
200///                     const xmlChar *ExternalID, const xmlChar *SystemID);
201/// ```
202///
203/// Creates a new DTD and if `doc` is non-NULL and has no internal subset,
204/// attaches it as the document's internal subset.
205///
206/// # SAFETY
207///
208/// - `doc` may be NULL.
209/// - `name`, `ExternalID`, `SystemID` must be valid null-terminated strings or NULL.
210pub unsafe fn new_dtd(
211    doc: *mut _xmlDoc,
212    name: *const xmlChar,
213    ExternalID: *const xmlChar,
214    SystemID: *const xmlChar,
215) -> *mut _xmlDtd {
216    // SAFETY: Allocate zero-initialized memory for the DTD.
217    let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
218    if dtd.is_null() {
219        return ptr::null_mut();
220    }
221
222    unsafe {
223        (*dtd).type_ = XML_DTD_NODE as c_int;
224        (*dtd).name = string::xml_strdup(name);
225        (*dtd).ExternalID = string::xml_strdup(ExternalID);
226        (*dtd).SystemID = string::xml_strdup(SystemID);
227        (*dtd).parent = doc;
228        (*dtd).doc = doc;
229
230        // UPSTREAM-PARITY (tree.c xmlNewDtd): the declaration hash tables are
231        // created lazily by the xmlAdd* functions on first use.
232
233        // Attach to document if it has no internal subset yet
234        if !doc.is_null() && (*doc).intSubset.is_null() {
235            (*doc).intSubset = dtd;
236        }
237    }
238
239    dtd
240}
241
242/// extern "C" copier shim for `hash::hash_copy`: element declarations.
243///
244/// # Safety
245///
246/// - `payload` must be NULL or a valid pointer to an `_xmlElement` owned by
247///   the hash table being copied; it is forwarded to `copy_element`.
248/// - `_name` is unused.
249unsafe extern "C" fn copy_elem_cb(payload: *mut c_void, _name: *const xmlChar) -> *mut c_void {
250    unsafe { copy_element(payload as *mut _xmlElement) as *mut c_void }
251}
252
253/// extern "C" copier shim for `hash::hash_copy`: attribute declarations.
254///
255/// # Safety
256///
257/// - `payload` must be NULL or a valid pointer to an `_xmlAttribute` owned by
258///   the hash table being copied; it is forwarded to `copy_attribute_decl`.
259/// - `_name` is unused.
260unsafe extern "C" fn copy_attr_cb(payload: *mut c_void, _name: *const xmlChar) -> *mut c_void {
261    unsafe { copy_attribute_decl(payload as *mut _xmlAttribute) as *mut c_void }
262}
263
264/// extern "C" copier shim for `hash::hash_copy`: entity declarations.
265///
266/// # Safety
267///
268/// - `payload` must be NULL or a valid pointer to an `_xmlEntity` owned by
269///   the hash table being copied; it is forwarded to `copy_entity`.
270/// - `_name` is unused.
271unsafe extern "C" fn copy_ent_cb(payload: *mut c_void, _name: *const xmlChar) -> *mut c_void {
272    unsafe { crate::xml::entities::copy_entity(payload as *mut _xmlEntity) as *mut c_void }
273}
274
275/// extern "C" copier shim for `hash::hash_copy`: notation declarations.
276///
277/// # Safety
278///
279/// - `payload` must be NULL or a valid pointer to an `_xmlNotation` owned by
280///   the hash table being copied; it is forwarded to `copy_notation`.
281/// - `_name` is unused.
282unsafe extern "C" fn copy_notation_cb(payload: *mut c_void, _name: *const xmlChar) -> *mut c_void {
283    unsafe { copy_notation(payload as *mut _xmlNotation) as *mut c_void }
284}
285
286/// Deep-copy a DTD: name, external identifiers, and all declaration tables.
287///
288/// UPSTREAM-PARITY: the DTD portion of `xmlCopyDoc` / `xmlCopyDtd`.
289///
290/// Returns the new DTD, or NULL on failure.
291///
292/// # SAFETY
293///
294/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
295pub unsafe fn copy_dtd(dtd: *const _xmlDtd) -> *mut _xmlDtd {
296    if dtd.is_null() {
297        return ptr::null_mut();
298    }
299    let d = unsafe { &*dtd };
300
301    let copy = unsafe { allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd };
302    if copy.is_null() {
303        return ptr::null_mut();
304    }
305
306    unsafe {
307        (*copy).type_ = d.type_;
308        (*copy).name = string::xml_strdup(d.name);
309        (*copy).ExternalID = string::xml_strdup(d.ExternalID);
310        (*copy).SystemID = string::xml_strdup(d.SystemID);
311        (*copy).notations =
312            hash::hash_copy(d.notations as *mut hash::HashTable, Some(copy_notation_cb))
313                as *mut c_void;
314        (*copy).elements =
315            hash::hash_copy(d.elements as *mut hash::HashTable, Some(copy_elem_cb)) as *mut c_void;
316        (*copy).attributes =
317            hash::hash_copy(d.attributes as *mut hash::HashTable, Some(copy_attr_cb))
318                as *mut c_void;
319        (*copy).entities =
320            hash::hash_copy(d.entities as *mut hash::HashTable, Some(copy_ent_cb)) as *mut c_void;
321        (*copy).pentities =
322            hash::hash_copy(d.pentities as *mut hash::HashTable, Some(copy_ent_cb)) as *mut c_void;
323    }
324
325    copy
326}
327
328/// Free a DTD and all its declarations.
329///
330/// # UPSTREAM-PARITY
331///
332/// ```c
333/// void xmlFreeDtd(xmlDtdPtr dtd);
334/// ```
335///
336/// # SAFETY
337///
338/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
339pub unsafe fn free_dtd(dtd: *mut _xmlDtd) {
340    if dtd.is_null() {
341        return;
342    }
343
344    unsafe {
345        let d = &mut *dtd;
346
347        // UPSTREAM-PARITY (tree.c xmlFreeDtd): element/attribute/entity
348        // declaration nodes in the child list are owned by the hash tables
349        // and are freed by the deallocators below; only non-declaration
350        // children (comments, PIs) are unlinked and freed from the list here.
351        // This must run BEFORE the hash tables are freed so the decl nodes
352        // are still alive when their type is inspected.
353        if !d.children.is_null() {
354            let mut c = d.children;
355            while !c.is_null() {
356                let next = (*c).next;
357                let t = (*c).type_;
358                if t != XML_ELEMENT_DECL as c_int
359                    && t != XML_ATTRIBUTE_DECL as c_int
360                    && t != XML_ENTITY_DECL as c_int
361                {
362                    crate::xml::tree::free_node(c);
363                }
364                c = next;
365            }
366        }
367
368        // Free hash tables with their deallocators
369        if !d.notations.is_null() {
370            hash::hash_free(
371                d.notations as *mut hash::HashTable,
372                Some(notation_deallocator),
373            );
374            d.notations = ptr::null_mut();
375        }
376        if !d.elements.is_null() {
377            hash::hash_free(
378                d.elements as *mut hash::HashTable,
379                Some(element_deallocator),
380            );
381            d.elements = ptr::null_mut();
382        }
383        if !d.attributes.is_null() {
384            hash::hash_free(
385                d.attributes as *mut hash::HashTable,
386                Some(attribute_deallocator),
387            );
388            d.attributes = ptr::null_mut();
389        }
390        if !d.entities.is_null() {
391            hash::hash_free(d.entities as *mut hash::HashTable, Some(entity_deallocator));
392            d.entities = ptr::null_mut();
393        }
394        if !d.pentities.is_null() {
395            hash::hash_free(
396                d.pentities as *mut hash::HashTable,
397                Some(entity_deallocator),
398            );
399            d.pentities = ptr::null_mut();
400        }
401
402        // Free strings
403        if !d.name.is_null() {
404            allocator::xmlFreeImpl(d.name as *mut c_void);
405        }
406        if !d.ExternalID.is_null() {
407            allocator::xmlFreeImpl(d.ExternalID as *mut c_void);
408        }
409        if !d.SystemID.is_null() {
410            allocator::xmlFreeImpl(d.SystemID as *mut c_void);
411        }
412
413        allocator::xmlFreeImpl(dtd as *mut c_void);
414    }
415}
416
417// ═══════════════════════════════════════════════════════════════════════════════
418// Hash Deallocators
419// ═══════════════════════════════════════════════════════════════════════════════
420
421/// Hash-table deallocator for notation declarations.
422///
423/// # Safety
424///
425/// - `payload` must be NULL or a valid pointer to an `_xmlNotation` owned
426///   exclusively by the hash table being freed; it is freed with
427///   `free_notation`.
428/// - `_name` is unused.
429unsafe extern "C" fn notation_deallocator(payload: *mut c_void, _name: *mut u8) {
430    if !payload.is_null() {
431        free_notation(payload as *mut _xmlNotation);
432    }
433}
434
435/// Hash-table deallocator for element declarations.
436///
437/// # Safety
438///
439/// - `payload` must be NULL or a valid pointer to an `_xmlElement` owned
440///   exclusively by the hash table being freed; it is freed with
441///   `free_element`.
442/// - `_name` is unused.
443unsafe extern "C" fn element_deallocator(payload: *mut c_void, _name: *mut u8) {
444    if !payload.is_null() {
445        free_element(payload as *mut _xmlElement);
446    }
447}
448
449/// Hash-table deallocator for attribute declarations.
450///
451/// # Safety
452///
453/// - `payload` must be NULL or a valid pointer to an `_xmlAttribute` owned
454///   exclusively by the hash table being freed; it is freed with
455///   `free_attribute`.
456/// - `_name` is unused.
457unsafe extern "C" fn attribute_deallocator(payload: *mut c_void, _name: *mut u8) {
458    if !payload.is_null() {
459        free_attribute(payload as *mut _xmlAttribute);
460    }
461}
462
463/// Hash-table deallocator for entity declarations.
464///
465/// # Safety
466///
467/// - `payload` must be NULL or a valid pointer to an `_xmlEntity` owned
468///   exclusively by the hash table being freed; it is freed with
469///   `free_entity`.
470/// - `_name` is unused.
471unsafe extern "C" fn entity_deallocator(payload: *mut c_void, _name: *mut u8) {
472    if !payload.is_null() {
473        crate::xml::entities::free_entity(payload as *mut _xmlEntity);
474    }
475}
476
477// ═══════════════════════════════════════════════════════════════════════════════
478// Notation Declarations
479// ═══════════════════════════════════════════════════════════════════════════════
480
481/// Add a notation declaration to a DTD.
482///
483/// # UPSTREAM-PARITY
484///
485/// ```c
486/// xmlNotationPtr xmlAddNotationDecl(xmlDtdPtr dtd, const xmlChar *name,
487///                                   const xmlChar *PublicID,
488///                                   const xmlChar *SystemID);
489/// ```
490///
491/// # SAFETY
492///
493/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
494/// - `name` must be a valid null-terminated string.
495/// - `PublicID`, `SystemID` may be NULL.
496pub unsafe fn add_notation_decl(
497    dtd: *mut _xmlDtd,
498    name: *const xmlChar,
499    PublicID: *const xmlChar,
500    SystemID: *const xmlChar,
501) -> *mut _xmlNotation {
502    if dtd.is_null() || name.is_null() {
503        return ptr::null_mut();
504    }
505
506    unsafe {
507        // UPSTREAM-PARITY (valid.c xmlAddNotationDecl): the table is
508        // created lazily on first use.
509        if (*dtd).notations.is_null() {
510            (*dtd).notations = hash::hash_create(8) as *mut c_void;
511        }
512        let d = &*dtd;
513
514        // Check if notation already exists
515        let existing = hash::hash_lookup(d.notations as *mut hash::HashTable, name);
516        if !existing.is_null() {
517            return existing as *mut _xmlNotation;
518        }
519
520        // SAFETY: Allocate zero-initialized memory for the notation.
521        let not = allocator::xmlMallocZero(size_of::<_xmlNotation>() as usize) as *mut _xmlNotation;
522        if not.is_null() {
523            return ptr::null_mut();
524        }
525
526        (*not).name = string::xml_strdup(name);
527        (*not).PublicID = string::xml_strdup(PublicID);
528        (*not).SystemID = string::xml_strdup(SystemID);
529
530        // Add to hash table
531        let ret = hash::hash_add_entry(
532            d.notations as *mut hash::HashTable,
533            name,
534            not as *mut c_void,
535        );
536        if ret != 0 {
537            // Failed to add (shouldn't happen since we checked)
538            free_notation(not);
539            return ptr::null_mut();
540        }
541
542        not
543    }
544}
545
546/// Look up a notation declaration by name.
547///
548/// # UPSTREAM-PARITY
549///
550/// ```c
551/// xmlNotationPtr xmlGetNotationDecl(xmlDtdPtr dtd, const xmlChar *name);
552/// ```
553///
554/// # SAFETY
555///
556/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
557/// - `name` must be a valid null-terminated string.
558pub unsafe fn get_notation_decl(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlNotation {
559    if dtd.is_null() || name.is_null() {
560        return ptr::null_mut();
561    }
562
563    unsafe {
564        let d = &*dtd;
565        let payload = hash::hash_lookup(d.notations as *mut hash::HashTable, name);
566        payload as *mut _xmlNotation
567    }
568}
569
570/// Deep copy a notation declaration.
571///
572/// # UPSTREAM-PARITY
573///
574/// ```c
575/// xmlNotationPtr xmlCopyNotation(xmlNotationPtr notation);
576/// ```
577///
578/// # SAFETY
579///
580/// - `notation` must be a valid pointer to an _xmlNotation, or NULL.
581pub unsafe fn copy_notation(notation: *mut _xmlNotation) -> *mut _xmlNotation {
582    if notation.is_null() {
583        return ptr::null_mut();
584    }
585
586    unsafe {
587        let n = &*notation;
588        // SAFETY: Allocate zero-initialized memory for the copy.
589        let copy =
590            allocator::xmlMallocZero(size_of::<_xmlNotation>() as usize) as *mut _xmlNotation;
591        if copy.is_null() {
592            return ptr::null_mut();
593        }
594
595        (*copy).name = string::xml_strdup(n.name);
596        (*copy).PublicID = string::xml_strdup(n.PublicID);
597        (*copy).SystemID = string::xml_strdup(n.SystemID);
598
599        copy
600    }
601}
602
603/// Free a notation declaration.
604///
605/// # UPSTREAM-PARITY
606///
607/// ```c
608/// void xmlFreeNotation(xmlNotationPtr notation);
609/// ```
610///
611/// # SAFETY
612///
613/// - `notation` must be a valid pointer to an _xmlNotation, or NULL.
614pub unsafe fn free_notation(notation: *mut _xmlNotation) {
615    if notation.is_null() {
616        return;
617    }
618
619    unsafe {
620        let n = &*notation;
621        if !n.name.is_null() {
622            allocator::xmlFreeImpl(n.name as *mut c_void);
623        }
624        if !n.PublicID.is_null() {
625            allocator::xmlFreeImpl(n.PublicID as *mut c_void);
626        }
627        if !n.SystemID.is_null() {
628            allocator::xmlFreeImpl(n.SystemID as *mut c_void);
629        }
630        allocator::xmlFreeImpl(notation as *mut c_void);
631    }
632}
633
634// ═══════════════════════════════════════════════════════════════════════════════
635// Element Content Model Functions
636// ═══════════════════════════════════════════════════════════════════════════════
637
638/// Create a new element content model node.
639///
640/// # UPSTREAM-PARITY
641///
642/// ```c
643/// xmlElementContentPtr xmlNewElementContent(const xmlChar *name, int type);
644/// ```
645///
646/// Creates a content model node with the given name and type.
647/// The `ocur` field is set to XML_ELEMENT_CONTENT_ONCE by default.
648///
649/// # SAFETY
650///
651/// - `name` may be NULL (for PCDATA and connector types).
652pub unsafe fn create_content_model(name: *const xmlChar, type_: c_int) -> *mut _xmlElementContent {
653    // SAFETY: Allocate zero-initialized memory for the content model.
654    let content = allocator::xmlMallocZero(size_of::<_xmlElementContent>() as usize)
655        as *mut _xmlElementContent;
656    if content.is_null() {
657        return ptr::null_mut();
658    }
659
660    unsafe {
661        (*content).type_ = type_;
662        (*content).ocur = XML_ELEMENT_CONTENT_ONCE as c_int;
663        (*content).name = string::xml_strdup(name);
664        (*content).c1 = ptr::null_mut();
665        (*content).c2 = ptr::null_mut();
666        (*content).parent = ptr::null_mut();
667        (*content).prefix = ptr::null_mut();
668    }
669
670    content
671}
672
673/// Free an element content model tree.
674///
675/// # UPSTREAM-PARITY
676///
677/// ```c
678/// void xmlFreeElementContent(xmlElementContentPtr cur);
679/// ```
680///
681/// Recursively frees the entire content model tree.
682///
683/// # SAFETY
684///
685/// - `cur` must be a valid pointer to an _xmlElementContent, or NULL.
686pub unsafe fn free_content_model(cur: *mut _xmlElementContent) {
687    if cur.is_null() {
688        return;
689    }
690
691    unsafe {
692        let c = &*cur;
693
694        // Recursively free children
695        if !c.c1.is_null() {
696            free_content_model(c.c1);
697        }
698        if !c.c2.is_null() {
699            free_content_model(c.c2);
700        }
701
702        // Free name and prefix
703        if !c.name.is_null() {
704            allocator::xmlFreeImpl(c.name as *mut c_void);
705        }
706        if !c.prefix.is_null() {
707            allocator::xmlFreeImpl(c.prefix as *mut c_void);
708        }
709
710        allocator::xmlFreeImpl(cur as *mut c_void);
711    }
712}
713
714/// Deep copy an element content model tree.
715///
716/// # UPSTREAM-PARITY
717///
718/// ```c
719/// xmlElementContentPtr xmlCopyElementContent(xmlElementContentPtr content);
720/// ```
721///
722/// Recursively copies the entire content model tree.
723///
724/// # SAFETY
725///
726/// - `content` must be a valid pointer to an _xmlElementContent, or NULL.
727pub unsafe fn copy_content_model(content: *mut _xmlElementContent) -> *mut _xmlElementContent {
728    if content.is_null() {
729        return ptr::null_mut();
730    }
731
732    unsafe {
733        let c = &*content;
734
735        // SAFETY: Allocate zero-initialized memory for the copy.
736        let copy = allocator::xmlMallocZero(size_of::<_xmlElementContent>() as usize)
737            as *mut _xmlElementContent;
738        if copy.is_null() {
739            return ptr::null_mut();
740        }
741
742        (*copy).type_ = c.type_;
743        (*copy).ocur = c.ocur;
744        (*copy).name = string::xml_strdup(c.name);
745        (*copy).prefix = string::xml_strdup(c.prefix);
746
747        // Recursively copy children
748        if !c.c1.is_null() {
749            (*copy).c1 = copy_content_model(c.c1);
750            if !(*copy).c1.is_null() {
751                (*(*copy).c1).parent = copy;
752            }
753        }
754        if !c.c2.is_null() {
755            (*copy).c2 = copy_content_model(c.c2);
756            if !(*copy).c2.is_null() {
757                (*(*copy).c2).parent = copy;
758            }
759        }
760
761        copy
762    }
763}
764
765// ═══════════════════════════════════════════════════════════════════════════════
766// Element Declarations
767// ═══════════════════════════════════════════════════════════════════════════════
768
769/// Add an element declaration to a DTD.
770///
771/// # UPSTREAM-PARITY
772///
773/// ```c
774/// xmlElementPtr xmlAddElementDecl(xmlDtdPtr dtd, const xmlChar *name, int type,
775///                                 xmlElementContentPtr content);
776/// ```
777///
778/// If an element with the same name already exists, the existing one is returned
779/// and no new declaration is created.
780///
781/// # SAFETY
782///
783/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
784/// - `name` must be a valid null-terminated string.
785pub unsafe fn add_element_decl(
786    dtd: *mut _xmlDtd,
787    name: *const xmlChar,
788    type_: c_int,
789    content: *mut _xmlElementContent,
790) -> *mut _xmlElement {
791    if dtd.is_null() || name.is_null() {
792        return ptr::null_mut();
793    }
794
795    unsafe {
796        // UPSTREAM-PARITY (valid.c xmlAddElementDecl): the table is created
797        // lazily on first use.
798        if (*dtd).elements.is_null() {
799            (*dtd).elements = hash::hash_create(8) as *mut c_void;
800        }
801        let d = &*dtd;
802
803        // Check if element already exists
804        let existing = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
805        if !existing.is_null() {
806            return existing as *mut _xmlElement;
807        }
808
809        // SAFETY: Allocate zero-initialized memory for the element.
810        let elem = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
811        if elem.is_null() {
812            return ptr::null_mut();
813        }
814
815        // UPSTREAM-PARITY (valid.c xmlAddElementDecl): the node type is
816        // XML_ELEMENT_DECL; the element type (EMPTY/ANY/MIXED/ELEMENT) is
817        // stored in `etype` only. The caller passes the element type as
818        // `type_` (matching the upstream parameter list).
819        (*elem).name = string::xml_strdup(name);
820        (*elem).type_ = XML_ELEMENT_DECL as c_int;
821        (*elem).etype = type_; // xmlElementTypeVal mirrors xmlElementType here
822        (*elem).content = content; // Takes ownership of the content model
823        (*elem).attributes = ptr::null_mut();
824        (*elem).prefix = ptr::null_mut();
825        (*elem).children = ptr::null_mut();
826        (*elem).last = ptr::null_mut();
827        (*elem).parent = dtd;
828        (*elem).next = ptr::null_mut();
829        (*elem).prev = ptr::null_mut();
830        (*elem).doc = (*dtd).doc;
831        (*elem).cont_model = ptr::null_mut();
832
833        // Add to hash table
834        let ret = hash::hash_add_entry(
835            d.elements as *mut hash::HashTable,
836            name,
837            elem as *mut c_void,
838        );
839        if ret != 0 {
840            // Failed to add
841            free_element(elem);
842            return ptr::null_mut();
843        }
844
845        // UPSTREAM-PARITY (valid.c xmlAddElementDecl "Link it to the DTD"):
846        // the element decl is a child node of the DTD.
847        if (*dtd).last.is_null() {
848            (*dtd).children = elem as *mut _xmlNode;
849            (*dtd).last = elem as *mut _xmlNode;
850        } else {
851            (*(*dtd).last).next = elem as *mut _xmlNode;
852            (*elem).prev = (*dtd).last;
853            (*dtd).last = elem as *mut _xmlNode;
854        }
855
856        elem
857    }
858}
859
860/// Look up an element declaration by name.
861///
862/// # UPSTREAM-PARITY
863///
864/// ```c
865/// xmlElementPtr xmlGetElementDecl(xmlDtdPtr dtd, const xmlChar *name);
866/// ```
867///
868/// # SAFETY
869///
870/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
871/// - `name` must be a valid null-terminated string.
872pub unsafe fn get_element_decl(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlElement {
873    if dtd.is_null() || name.is_null() {
874        return ptr::null_mut();
875    }
876
877    unsafe {
878        let d = &*dtd;
879        if d.elements.is_null() {
880            return ptr::null_mut();
881        }
882        let payload = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
883        payload as *mut _xmlElement
884    }
885}
886
887/// UPSTREAM-PARITY (valid.c xmlGetDtdElementDesc2): lookup an element
888/// declaration, creating an UNDEFINED placeholder when missing. The
889/// placeholder is registered in the elements table but NOT linked into the
890/// DTD's child list (the attribute-decl path that triggers this leaves the
891/// element undeclared).
892///
893/// # SAFETY
894///
895/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
896/// - `name` must be a valid null-terminated string.
897pub unsafe fn get_element_decl_created(
898    dtd: *mut _xmlDtd,
899    name: *const xmlChar,
900) -> *mut _xmlElement {
901    if dtd.is_null() || name.is_null() {
902        return ptr::null_mut();
903    }
904
905    unsafe {
906        if (*dtd).elements.is_null() {
907            (*dtd).elements = hash::hash_create(8) as *mut c_void;
908        }
909        let existing = hash::hash_lookup((*dtd).elements as *mut hash::HashTable, name);
910        if !existing.is_null() {
911            return existing as *mut _xmlElement;
912        }
913
914        let elem = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
915        if elem.is_null() {
916            return ptr::null_mut();
917        }
918        (*elem).type_ = XML_ELEMENT_DECL as c_int;
919        (*elem).name = string::xml_strdup(name);
920        (*elem).etype = XML_ELEMENT_TYPE_UNDEFINED as c_int;
921        (*elem).doc = (*dtd).doc;
922        (*elem).parent = dtd;
923        if hash::hash_add_entry(
924            (*dtd).elements as *mut hash::HashTable,
925            name,
926            elem as *mut c_void,
927        ) != 0
928        {
929            free_element(elem);
930            return ptr::null_mut();
931        }
932        elem
933    }
934}
935
936/// Deep copy an element declaration.
937///
938/// # UPSTREAM-PARITY
939///
940/// ```c
941/// xmlElementPtr xmlCopyElement(xmlElementPtr elem);
942/// ```
943///
944/// # SAFETY
945///
946/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
947pub unsafe fn copy_element(elem: *mut _xmlElement) -> *mut _xmlElement {
948    if elem.is_null() {
949        return ptr::null_mut();
950    }
951
952    unsafe {
953        let e = &*elem;
954
955        // SAFETY: Allocate zero-initialized memory for the copy.
956        let copy = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
957        if copy.is_null() {
958            return ptr::null_mut();
959        }
960
961        (*copy).name = string::xml_strdup(e.name);
962        (*copy).type_ = e.type_;
963        (*copy).etype = e.etype;
964        (*copy).content = copy_content_model(e.content);
965        (*copy).prefix = string::xml_strdup(e.prefix);
966        (*copy)._private = e._private;
967        (*copy).parent = e.parent;
968        (*copy).doc = e.doc;
969
970        // Copy attribute declarations (linked list)
971        if !e.attributes.is_null() {
972            // UPSTREAM-PARITY: We copy the attribute linked list by
973            // iterating and copying each attribute.
974            let mut src_attr = e.attributes;
975            let mut prev_copy: *mut _xmlAttribute = ptr::null_mut();
976            let mut first_copy: *mut _xmlAttribute = ptr::null_mut();
977
978            while !src_attr.is_null() {
979                let attr_copy = copy_attribute_decl(src_attr);
980                if attr_copy.is_null() {
981                    // Free what we've copied so far
982                    let mut to_free = first_copy;
983                    while !to_free.is_null() {
984                        let next = (*to_free).nexth;
985                        free_attribute(to_free);
986                        to_free = next;
987                    }
988                    allocator::xmlFreeImpl(copy as *mut c_void);
989                    return ptr::null_mut();
990                }
991
992                if prev_copy.is_null() {
993                    first_copy = attr_copy;
994                } else {
995                    (*prev_copy).nexth = attr_copy;
996                }
997                prev_copy = attr_copy;
998                src_attr = (*src_attr).nexth;
999            }
1000
1001            (*copy).attributes = first_copy;
1002        }
1003
1004        copy
1005    }
1006}
1007
1008/// Free an element declaration and its content model.
1009///
1010/// # UPSTREAM-PARITY
1011///
1012/// ```c
1013/// void xmlFreeElement(xmlElementPtr elem);
1014/// ```
1015///
1016/// Frees the element declaration and its content model, but NOT the
1017/// attribute declarations (which are owned by the DTD's attribute hash).
1018///
1019/// # SAFETY
1020///
1021/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
1022pub unsafe fn free_element(elem: *mut _xmlElement) {
1023    if elem.is_null() {
1024        return;
1025    }
1026
1027    unsafe {
1028        // Free name
1029        if !(*elem).name.is_null() {
1030            allocator::xmlFreeImpl((*elem).name as *mut c_void);
1031        }
1032
1033        // Free prefix
1034        if !(*elem).prefix.is_null() {
1035            allocator::xmlFreeImpl((*elem).prefix as *mut c_void);
1036        }
1037
1038        // Free content model
1039        if !(*elem).content.is_null() {
1040            free_content_model((*elem).content);
1041        }
1042
1043        // Free the compiled content-model NFA (xmlValidBuildContentModel)
1044        // UPSTREAM-PARITY: xmlFreeElement releases contModel via xmlRegFreeRegexp.
1045        if !(*elem).cont_model.is_null() {
1046            crate::xml::validation::free_content_model_nfa(
1047                (*elem).cont_model as *mut crate::xml::validation::ContentModelNfa,
1048            );
1049        }
1050
1051        // UPSTREAM-PARITY: The attributes linked list on the element
1052        // declaration is NOT owned by the element. The DTD's attribute
1053        // hash table is the sole owner. When the DTD is freed, the
1054        // hash table's deallocator frees all attributes.
1055        // Therefore, we do NOT free the attributes list here.
1056        (*elem).attributes = ptr::null_mut();
1057
1058        allocator::xmlFreeImpl(elem as *mut c_void);
1059    }
1060}
1061
1062// ═══════════════════════════════════════════════════════════════════════════════
1063// Attribute Declarations
1064// ═══════════════════════════════════════════════════════════════════════════════
1065
1066/// Free an enumeration value tree.
1067///
1068/// # SAFETY
1069///
1070/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
1071unsafe fn free_enumeration(tree: *mut _xmlEnumeration) {
1072    if tree.is_null() {
1073        return;
1074    }
1075
1076    unsafe {
1077        let mut cur = tree;
1078        while !cur.is_null() {
1079            let next = (*cur).next;
1080            if !(*cur).name.is_null() {
1081                allocator::xmlFreeImpl((*cur).name as *mut c_void);
1082            }
1083            allocator::xmlFreeImpl(cur as *mut c_void);
1084            cur = next;
1085        }
1086    }
1087}
1088
1089/// Deep copy an enumeration value tree.
1090///
1091/// # SAFETY
1092///
1093/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
1094unsafe fn copy_enumeration(tree: *mut _xmlEnumeration) -> *mut _xmlEnumeration {
1095    if tree.is_null() {
1096        return ptr::null_mut();
1097    }
1098
1099    unsafe {
1100        let mut src = tree;
1101        let mut first_copy: *mut _xmlEnumeration = ptr::null_mut();
1102        let mut prev_copy: *mut _xmlEnumeration = ptr::null_mut();
1103
1104        while !src.is_null() {
1105            let copy = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1106                as *mut _xmlEnumeration;
1107            if copy.is_null() {
1108                // Free what we've allocated so far
1109                let mut to_free = first_copy;
1110                while !to_free.is_null() {
1111                    let next = (*to_free).next;
1112                    if !(*to_free).name.is_null() {
1113                        allocator::xmlFreeImpl((*to_free).name as *mut c_void);
1114                    }
1115                    allocator::xmlFreeImpl(to_free as *mut c_void);
1116                    to_free = next;
1117                }
1118                return ptr::null_mut();
1119            }
1120
1121            (*copy).name = string::xml_strdup((*src).name);
1122            (*copy).next = ptr::null_mut();
1123
1124            if prev_copy.is_null() {
1125                first_copy = copy;
1126            } else {
1127                (*prev_copy).next = copy;
1128            }
1129            prev_copy = copy;
1130            src = (*src).next;
1131        }
1132
1133        first_copy
1134    }
1135}
1136
1137/// Add an attribute declaration to a DTD.
1138///
1139/// # UPSTREAM-PARITY
1140///
1141/// ```c
1142/// xmlAttributePtr xmlAddAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
1143///                                     const xmlChar *name, int type, int def,
1144///                                     const xmlChar *defaultValue,
1145///                                     xmlEnumerationPtr tree);
1146/// ```
1147///
1148/// Adds an attribute declaration to both the DTD's attribute hash table
1149/// (keyed by element name + attribute name) and the element's linked list.
1150/// If an attribute with the same name already exists for this element,
1151/// the existing declaration is returned.
1152///
1153/// # SAFETY
1154///
1155/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1156/// - `name` must be a valid null-terminated string.
1157/// - `elem`, `defaultValue`, `tree` may be NULL.
1158#[allow(clippy::too_many_arguments)]
1159pub unsafe fn add_attribute_decl(
1160    dtd: *mut _xmlDtd,
1161    elem: *mut _xmlElement,
1162    name: *const xmlChar,
1163    ns: *const xmlChar,
1164    type_: c_int,
1165    def: c_int,
1166    defaultValue: *const xmlChar,
1167    tree: *mut _xmlEnumeration,
1168) -> *mut _xmlAttribute {
1169    if dtd.is_null() || name.is_null() {
1170        return ptr::null_mut();
1171    }
1172
1173    unsafe {
1174        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): the table is
1175        // created lazily on first use.
1176        if (*dtd).attributes.is_null() {
1177            (*dtd).attributes = hash::hash_create(8) as *mut c_void;
1178        }
1179        let d = &*dtd;
1180        let elem_name = if elem.is_null() {
1181            ptr::null()
1182        } else {
1183            (*elem).name
1184        };
1185
1186        // Check if attribute already exists for this element
1187        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): the attribute table
1188        // is keyed by (name, ns, elem) — xmlHashAdd3/xmlHashLookup3 with the
1189        // namespace as the middle key (R-000176: the pre-2.10 candidate
1190        // signature dropped the ns key).
1191        let existing =
1192            hash::hash_lookup3(d.attributes as *mut hash::HashTable, name, ns, elem_name);
1193        if !existing.is_null() {
1194            return existing as *mut _xmlAttribute;
1195        }
1196
1197        // SAFETY: Allocate zero-initialized memory for the attribute.
1198        let attr =
1199            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1200        if attr.is_null() {
1201            return ptr::null_mut();
1202        }
1203
1204        (*attr).type_ = XML_ATTRIBUTE_DECL as c_int;
1205        (*attr).name = string::xml_strdup(name);
1206        (*attr).parent = dtd;
1207        (*attr).doc = d.doc;
1208        (*attr).nexth = ptr::null_mut();
1209        (*attr).atype = type_;
1210        (*attr).def = def;
1211        (*attr).defaultValue = string::xml_strdup(defaultValue);
1212        (*attr).tree = tree; // Takes ownership of the enumeration tree
1213                             // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): `prefix` mirrors the
1214                             // ns argument; NULL ns leaves it NULL.
1215        (*attr).prefix = if ns.is_null() {
1216            ptr::null_mut()
1217        } else {
1218            string::xml_strdup(ns)
1219        };
1220        (*attr).elem = string::xml_strdup(elem_name);
1221
1222        // Add to DTD's attribute hash table (keyed by attribute name,
1223        // namespace, element name — upstream xmlHashAdd3).
1224        let ret = hash::hash_add_entry3(
1225            d.attributes as *mut hash::HashTable,
1226            name,
1227            ns,
1228            elem_name,
1229            attr as *mut c_void,
1230        );
1231        if ret != 0 {
1232            // Failed to add
1233            if !(*attr).defaultValue.is_null() {
1234                allocator::xmlFreeImpl((*attr).defaultValue as *mut c_void);
1235            }
1236            if !(*attr).name.is_null() {
1237                allocator::xmlFreeImpl((*attr).name as *mut c_void);
1238            }
1239            if !(*attr).elem.is_null() {
1240                allocator::xmlFreeImpl((*attr).elem as *mut c_void);
1241            }
1242            if !(*attr).prefix.is_null() {
1243                allocator::xmlFreeImpl((*attr).prefix as *mut c_void);
1244            }
1245            allocator::xmlFreeImpl(attr as *mut c_void);
1246            // Don't free tree - caller still owns it on failure
1247            return ptr::null_mut();
1248        }
1249
1250        // Add to element's linked list
1251        if !elem.is_null() {
1252            (*attr).nexth = (*elem).attributes;
1253            (*elem).attributes = attr;
1254        }
1255
1256        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl "Link it to the
1257        // DTD"): the attribute decl is a child node of the DTD.
1258        if (*dtd).last.is_null() {
1259            (*dtd).children = attr as *mut _xmlNode;
1260            (*dtd).last = attr as *mut _xmlNode;
1261        } else {
1262            (*(*dtd).last).next = attr as *mut _xmlNode;
1263            (*attr).prev = (*dtd).last;
1264            (*dtd).last = attr as *mut _xmlNode;
1265        }
1266
1267        attr
1268    }
1269}
1270
1271/// Look up an attribute declaration by element name and attribute name.
1272///
1273/// # UPSTREAM-PARITY
1274///
1275/// ```c
1276/// xmlAttributePtr xmlGetAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
1277///                                     const xmlChar *name, int namePrefix);
1278/// ```
1279///
1280/// The `namePrefix` parameter is ignored in this implementation
1281/// (it's a legacy parameter in libxml2).
1282///
1283/// # SAFETY
1284///
1285/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1286/// - `name` must be a valid null-terminated string.
1287/// - `elem` may be NULL.
1288pub unsafe fn get_attribute_decl(
1289    dtd: *mut _xmlDtd,
1290    elem: *mut _xmlElement,
1291    name: *const xmlChar,
1292    _namePrefix: c_int,
1293) -> *mut _xmlAttribute {
1294    if dtd.is_null() || name.is_null() {
1295        return ptr::null_mut();
1296    }
1297
1298    unsafe {
1299        let d = &*dtd;
1300        let elem_name = if elem.is_null() {
1301            ptr::null()
1302        } else {
1303            (*elem).name
1304        };
1305
1306        // UPSTREAM-PARITY (valid.c xmlGetDtdQAttrDesc): keyed by
1307        // (name, prefix, elem).
1308        let payload = hash::hash_lookup3(
1309            d.attributes as *mut hash::HashTable,
1310            name,
1311            ptr::null(),
1312            elem_name,
1313        );
1314        payload as *mut _xmlAttribute
1315    }
1316}
1317
1318/// Deep copy an attribute declaration.
1319///
1320/// # UPSTREAM-PARITY
1321///
1322/// ```c
1323/// xmlAttributePtr xmlCopyAttribute(xmlAttributePtr attr);
1324/// ```
1325///
1326/// # SAFETY
1327///
1328/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1329pub unsafe fn copy_attribute_decl(attr: *mut _xmlAttribute) -> *mut _xmlAttribute {
1330    if attr.is_null() {
1331        return ptr::null_mut();
1332    }
1333
1334    unsafe {
1335        let a = &*attr;
1336
1337        // SAFETY: Allocate zero-initialized memory for the copy.
1338        let copy =
1339            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1340        if copy.is_null() {
1341            return ptr::null_mut();
1342        }
1343
1344        (*copy).type_ = a.type_;
1345        (*copy).name = string::xml_strdup(a.name);
1346        (*copy).parent = a.parent;
1347        (*copy).doc = a.doc;
1348        (*copy).nexth = ptr::null_mut();
1349        (*copy).atype = a.atype;
1350        (*copy).def = a.def;
1351        (*copy).defaultValue = string::xml_strdup(a.defaultValue);
1352        (*copy).tree = copy_enumeration(a.tree);
1353        (*copy).prefix = string::xml_strdup(a.prefix);
1354        (*copy).elem = string::xml_strdup(a.elem);
1355
1356        copy
1357    }
1358}
1359
1360/// Free an attribute declaration.
1361///
1362/// # UPSTREAM-PARITY
1363///
1364/// ```c
1365/// void xmlFreeAttribute(xmlAttributePtr attr);
1366/// ```
1367///
1368/// # SAFETY
1369///
1370/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1371pub unsafe fn free_attribute(attr: *mut _xmlAttribute) {
1372    if attr.is_null() {
1373        return;
1374    }
1375
1376    unsafe {
1377        let a = &*attr;
1378
1379        if !a.name.is_null() {
1380            allocator::xmlFreeImpl(a.name as *mut c_void);
1381        }
1382        if !a.defaultValue.is_null() {
1383            allocator::xmlFreeImpl(a.defaultValue as *mut c_void);
1384        }
1385        if !a.prefix.is_null() {
1386            allocator::xmlFreeImpl(a.prefix as *mut c_void);
1387        }
1388        if !a.elem.is_null() {
1389            allocator::xmlFreeImpl(a.elem as *mut c_void);
1390        }
1391        if !a.tree.is_null() {
1392            free_enumeration(a.tree);
1393        }
1394
1395        allocator::xmlFreeImpl(attr as *mut c_void);
1396    }
1397}
1398
1399// ═══════════════════════════════════════════════════════════════════════════════
1400// Content Model Validation (Automata-based)
1401// ═══════════════════════════════════════════════════════════════════════════════
1402
1403/// Result of content model validation.
1404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1405pub enum ContentModelResult {
1406    /// Content is valid.
1407    Valid,
1408    /// Content is invalid.
1409    Invalid,
1410    /// Content model is indeterminate (mixed content with PCDATA).
1411    Indeterminate,
1412}
1413
1414/// Validate content (a list of element names) against a content model,
1415/// taking occurrence indicators into account.
1416///
1417/// # UPSTREAM-PARITY
1418///
1419/// ```c
1420/// int xmlValidContentModel(xmlElementContentPtr model, ...)
1421/// ```
1422///
1423/// This implements a simple recursive descent validator for content models.
1424/// For simple content models (EMPTY, ANY, PCDATA), the check is direct.
1425/// For sequence/choice models, it recursively validates.
1426///
1427/// Returns `ContentModelResult::Valid` if the content matches the model,
1428/// `ContentModelResult::Invalid` otherwise.
1429///
1430/// # SAFETY
1431///
1432/// - `model` must be a valid pointer to an _xmlElementContent, or NULL.
1433/// - `names` must be a slice of element names (null-terminated xmlChar strings).
1434pub unsafe fn valid_content_model(
1435    model: *mut _xmlElementContent,
1436    names: &[*const xmlChar],
1437) -> ContentModelResult {
1438    if model.is_null() {
1439        return ContentModelResult::Invalid;
1440    }
1441
1442    unsafe {
1443        let m = &*model;
1444
1445        // Handle occurrence indicators at this level first
1446        match m.ocur as u32 {
1447            o if o == XML_ELEMENT_CONTENT_OPT as u32 => {
1448                // Optional: zero or one occurrence
1449                if names.is_empty() {
1450                    return ContentModelResult::Valid;
1451                }
1452                return valid_content_model_inner(model, names);
1453            }
1454            o if o == XML_ELEMENT_CONTENT_MULT as u32 => {
1455                // Zero or more
1456                if names.is_empty() {
1457                    return ContentModelResult::Valid;
1458                }
1459                return valid_content_model_zero_or_more(model, names);
1460            }
1461            o if o == XML_ELEMENT_CONTENT_PLUS as u32 => {
1462                // One or more
1463                if names.is_empty() {
1464                    return ContentModelResult::Invalid;
1465                }
1466                return valid_content_model_one_or_more(model, names);
1467            }
1468            _ => {}
1469        }
1470
1471        valid_content_model_inner(model, names)
1472    }
1473}
1474
1475/// Validate content against a content model without considering occurrence.
1476///
1477/// # Safety
1478///
1479/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1480///   dereferenced to read `type_` and `name`, and its `c1`/`c2` children are
1481///   followed when non-NULL.
1482/// - Each entry in `names` must be NULL or a valid pointer to a
1483///   NUL-terminated `xmlChar` element name; non-NULL entries are compared
1484///   with `xml_strcmp`.
1485unsafe fn valid_content_model_inner(
1486    model: *mut _xmlElementContent,
1487    names: &[*const xmlChar],
1488) -> ContentModelResult {
1489    unsafe {
1490        let m = &*model;
1491
1492        match m.type_ as u32 {
1493            t if t == XML_ELEMENT_CONTENT_PCDATA as u32 => {
1494                // PCDATA: content must be empty (just text)
1495                if names.is_empty() {
1496                    ContentModelResult::Valid
1497                } else {
1498                    ContentModelResult::Invalid
1499                }
1500            }
1501            t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
1502                // Single element: must match exactly one element
1503                if names.len() != 1 {
1504                    return ContentModelResult::Invalid;
1505                }
1506                if names[0].is_null() {
1507                    return ContentModelResult::Invalid;
1508                }
1509                // Compare with model name
1510                if string::xml_strcmp(names[0], m.name) != 0 {
1511                    return ContentModelResult::Invalid;
1512                }
1513                ContentModelResult::Valid
1514            }
1515            t if t == XML_ELEMENT_CONTENT_SEQ as u32 => {
1516                // Sequence: validate children in order
1517                valid_content_model_seq(m, names)
1518            }
1519            t if t == XML_ELEMENT_CONTENT_OR as u32 => {
1520                // Choice: one of the alternatives must match all names
1521                valid_content_model_or(m, names)
1522            }
1523            _ => ContentModelResult::Invalid,
1524        }
1525    }
1526}
1527
1528/// Validate content for zero-or-more occurrence.
1529///
1530/// # Safety
1531///
1532/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1533///   forwarded to `valid_content_model_inner`, which dereferences it.
1534/// - Each entry in `names` must be NULL or a valid pointer to a
1535///   NUL-terminated `xmlChar` element name.
1536unsafe fn valid_content_model_zero_or_more(
1537    model: *mut _xmlElementContent,
1538    names: &[*const xmlChar],
1539) -> ContentModelResult {
1540    // Zero or more: try each possible split
1541    let mut i = 0;
1542    while i <= names.len() {
1543        let consumed = &names[..i];
1544        let remaining = &names[i..];
1545
1546        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1547        if consumed_valid == ContentModelResult::Valid {
1548            if remaining.is_empty() {
1549                return ContentModelResult::Valid;
1550            }
1551            // Try to match remaining with same model
1552            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1553            if remaining_valid == ContentModelResult::Valid {
1554                return ContentModelResult::Valid;
1555            }
1556        }
1557
1558        i += 1;
1559    }
1560    ContentModelResult::Invalid
1561}
1562
1563/// Validate content for one-or-more occurrence.
1564///
1565/// # Safety
1566///
1567/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1568///   forwarded to `valid_content_model_inner`, which dereferences it.
1569/// - Each entry in `names` must be NULL or a valid pointer to a
1570///   NUL-terminated `xmlChar` element name.
1571unsafe fn valid_content_model_one_or_more(
1572    model: *mut _xmlElementContent,
1573    names: &[*const xmlChar],
1574) -> ContentModelResult {
1575    // One or more: must match at least once
1576    let mut i = 1;
1577    while i <= names.len() {
1578        let consumed = &names[..i];
1579        let remaining = &names[i..];
1580
1581        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1582        if consumed_valid == ContentModelResult::Valid {
1583            if remaining.is_empty() {
1584                return ContentModelResult::Valid;
1585            }
1586            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1587            if remaining_valid == ContentModelResult::Valid {
1588                return ContentModelResult::Valid;
1589            }
1590        }
1591
1592        i += 1;
1593    }
1594    ContentModelResult::Invalid
1595}
1596
1597/// Validate content against a sequence content model.
1598///
1599/// # Safety
1600///
1601/// - The `model` reference must point to a live `_xmlElementContent`.
1602/// - Non-NULL `c1`/`c2` children must be valid pointers to
1603///   `_xmlElementContent`; they are passed to `valid_content_model`.
1604/// - Each entry in `names` must be NULL or a valid pointer to a
1605///   NUL-terminated `xmlChar` element name.
1606unsafe fn valid_content_model_seq(
1607    model: &_xmlElementContent,
1608    names: &[*const xmlChar],
1609) -> ContentModelResult {
1610    // For a sequence, we need to split the names between c1 and c2
1611    // This is a simplified validation - full automata-based validation
1612    // would be more complex.
1613
1614    let c1 = model.c1;
1615    let c2 = model.c2;
1616
1617    if c1.is_null() && c2.is_null() {
1618        return ContentModelResult::Valid;
1619    }
1620
1621    if c1.is_null() {
1622        return unsafe { valid_content_model(c2, names) };
1623    }
1624
1625    if c2.is_null() {
1626        return unsafe { valid_content_model(c1, names) };
1627    }
1628
1629    // Try to split the names at each possible position
1630    // This implements a simple backtracking validator
1631    for split in 0..=names.len() {
1632        let left = &names[..split];
1633        let right = &names[split..];
1634
1635        let left_valid = unsafe { valid_content_model(c1, left) };
1636        if left_valid != ContentModelResult::Valid {
1637            continue;
1638        }
1639
1640        let right_valid = unsafe { valid_content_model(c2, right) };
1641        if right_valid == ContentModelResult::Valid {
1642            return ContentModelResult::Valid;
1643        }
1644    }
1645
1646    ContentModelResult::Invalid
1647}
1648
1649/// Validate content against a choice content model.
1650///
1651/// # Safety
1652///
1653/// - The `model` reference must point to a live `_xmlElementContent`.
1654/// - Non-NULL `c1`/`c2` children must be valid pointers to
1655///   `_xmlElementContent`; they are passed to `valid_content_model`.
1656/// - Each entry in `names` must be NULL or a valid pointer to a
1657///   NUL-terminated `xmlChar` element name.
1658unsafe fn valid_content_model_or(
1659    model: &_xmlElementContent,
1660    names: &[*const xmlChar],
1661) -> ContentModelResult {
1662    let c1 = model.c1;
1663    let c2 = model.c2;
1664
1665    if c1.is_null() && c2.is_null() {
1666        return ContentModelResult::Invalid;
1667    }
1668
1669    if !c1.is_null() {
1670        let result = unsafe { valid_content_model(c1, names) };
1671        if result == ContentModelResult::Valid {
1672            return ContentModelResult::Valid;
1673        }
1674    }
1675
1676    if !c2.is_null() {
1677        let result = unsafe { valid_content_model(c2, names) };
1678        if result == ContentModelResult::Valid {
1679            return ContentModelResult::Valid;
1680        }
1681    }
1682
1683    ContentModelResult::Invalid
1684}
1685
1686// ═══════════════════════════════════════════════════════════════════════════════
1687// Tests
1688// ═══════════════════════════════════════════════════════════════════════════════
1689
1690#[cfg(test)]
1691mod tests {
1692    use super::*;
1693
1694    use core::ffi::c_void;
1695    use core::ptr;
1696
1697    // ── Helpers ──────────────────────────────────────────────────────────
1698
1699    unsafe fn c_str(s: &[u8]) -> *const xmlChar {
1700        // Create a null-terminated xmlChar string
1701        let len = s.len();
1702        let buf = allocator::xmlMallocImpl(len + 1) as *mut xmlChar;
1703        assert!(!buf.is_null());
1704        ptr::copy_nonoverlapping(s.as_ptr(), buf, len);
1705        *buf.add(len) = 0;
1706        buf as *const xmlChar
1707    }
1708
1709    unsafe fn make_doc_and_dtd() -> (*mut _xmlDoc, *mut _xmlDtd) {
1710        let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1711        assert!(!doc.is_null());
1712        (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1713        (*doc).doc = doc;
1714        let dtd = create_int_subset(doc, c_str(b"root"), ptr::null(), ptr::null());
1715        assert!(!dtd.is_null());
1716        (doc, dtd)
1717    }
1718
1719    // ── DTD Access Tests ────────────────────────────────────────────────
1720
1721    /// Verify that `get_int_subset` accepts a NULL document.
1722    ///
1723    /// # Safety
1724    ///
1725    /// - NULL is allowed and is never dereferenced.
1726    #[test]
1727    fn test_get_int_subset_null() {
1728        {
1729            assert!(get_int_subset(ptr::null()).is_null());
1730        }
1731    }
1732
1733    /// Verify that `create_int_subset` attaches a DTD to a document.
1734    ///
1735    /// # Safety
1736    ///
1737    /// - `doc` and `dtd` from `make_doc_and_dtd` are heap-allocated structs
1738    ///   that must be valid while their fields are read; the DTD is freed with
1739    ///   `free_dtd` and the doc with `xmlFreeImpl`.
1740    #[test]
1741    fn test_create_int_subset() {
1742        unsafe {
1743            let (doc, dtd) = make_doc_and_dtd();
1744            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1745            assert!(!(*dtd).name.is_null());
1746            assert_eq!((*doc).intSubset, dtd);
1747
1748            // Cleanup
1749            free_dtd(dtd);
1750            allocator::xmlFreeImpl(doc as *mut c_void);
1751        }
1752    }
1753
1754    /// Verify that `create_int_subset` with a NULL doc still allocates and
1755    /// returns an unattached DTD (upstream tree.c `xmlCreateIntSubset` —
1756    /// HOSTILE-ABI A37).
1757    ///
1758    /// # Safety
1759    ///
1760    /// - A NULL `doc` is allowed and is not dereferenced; the returned DTD
1761    ///   is freed with `free_dtd`.
1762    #[test]
1763    fn test_create_int_subset_null_doc() {
1764        unsafe {
1765            let dtd = create_int_subset(ptr::null_mut(), c_str(b"root"), ptr::null(), ptr::null());
1766            assert!(!dtd.is_null());
1767            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1768            assert!((*dtd).parent.is_null());
1769            assert!((*dtd).doc.is_null());
1770            free_dtd(dtd);
1771        }
1772    }
1773
1774    /// Verify that `new_dtd` creates a DTD node and attaches it to the doc.
1775    ///
1776    /// # Safety
1777    ///
1778    /// - `doc` is a heap-allocated `_xmlDoc` and the strings passed to
1779    ///   `new_dtd` are NUL-terminated allocations that stay alive for the call.
1780    /// - `dtd` must be freed with `free_dtd` and the doc with `xmlFreeImpl`.
1781    #[test]
1782    fn test_new_dtd() {
1783        unsafe {
1784            let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1785            assert!(!doc.is_null());
1786            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1787            (*doc).doc = doc;
1788
1789            let dtd = new_dtd(doc, c_str(b"test"), c_str(b"-//TEST//"), c_str(b"test.dtd"));
1790            assert!(!dtd.is_null());
1791            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1792            assert_eq!((*doc).intSubset, dtd);
1793
1794            free_dtd(dtd);
1795            allocator::xmlFreeImpl(doc as *mut c_void);
1796        }
1797    }
1798
1799    /// Verify that `new_dtd` works without a document.
1800    ///
1801    /// # Safety
1802    ///
1803    /// - The strings passed to `new_dtd` must be NUL-terminated and alive for
1804    ///   the call; the returned DTD is freed with `free_dtd`.
1805    #[test]
1806    fn test_new_dtd_no_doc() {
1807        unsafe {
1808            let dtd = new_dtd(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1809            assert!(!dtd.is_null());
1810            free_dtd(dtd);
1811        }
1812    }
1813
1814    // ── Notation Tests ──────────────────────────────────────────────────
1815
1816    /// Verify adding and looking up a notation declaration.
1817    ///
1818    /// # Safety
1819    ///
1820    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
1821    ///   must be valid and alive for the calls; the DTD is freed with
1822    ///   `free_dtd` and the doc with `xmlFreeImpl`.
1823    #[test]
1824    fn test_add_get_notation() {
1825        unsafe {
1826            let (doc, dtd) = make_doc_and_dtd();
1827            let name = c_str(b"note");
1828            let pubid = c_str(b"-//TEST//NOTATION");
1829            let sysid = c_str(b"note.ent");
1830
1831            let n = add_notation_decl(dtd, name, pubid, sysid);
1832            assert!(!n.is_null());
1833            assert_eq!(string::xml_strcmp((*n).name, name), 0);
1834
1835            // Lookup
1836            let found = get_notation_decl(dtd, name);
1837            assert_eq!(found, n);
1838
1839            // Lookup non-existent
1840            let not_found = get_notation_decl(dtd, c_str(b"nonexistent"));
1841            assert!(not_found.is_null());
1842
1843            free_dtd(dtd);
1844            allocator::xmlFreeImpl(doc as *mut c_void);
1845        }
1846    }
1847
1848    /// Verify that adding a notation to a NULL DTD returns NULL.
1849    ///
1850    /// # Safety
1851    ///
1852    /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
1853    ///   buffer must be NUL-terminated.
1854    #[test]
1855    fn test_add_notation_null_dtd() {
1856        unsafe {
1857            let n = add_notation_decl(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1858            assert!(n.is_null());
1859        }
1860    }
1861
1862    /// Verify `copy_notation` deep-copies a notation declaration.
1863    ///
1864    /// # Safety
1865    ///
1866    /// - `n` from `add_notation_decl` must be a valid `_xmlNotation` while
1867    ///   copied; the copy is freed with `free_notation`, the DTD with
1868    ///   `free_dtd`, and the doc with `xmlFreeImpl`.
1869    #[test]
1870    fn test_copy_notation() {
1871        unsafe {
1872            let (doc, dtd) = make_doc_and_dtd();
1873            let name = c_str(b"note1");
1874            let pubid = c_str(b"public");
1875            let sysid = c_str(b"system");
1876
1877            let n = add_notation_decl(dtd, name, pubid, sysid);
1878            assert!(!n.is_null());
1879
1880            let copy = copy_notation(n);
1881            assert!(!copy.is_null());
1882            assert_ne!(copy, n);
1883            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1884            assert_eq!(string::xml_strcmp((*copy).PublicID, pubid), 0);
1885            assert_eq!(string::xml_strcmp((*copy).SystemID, sysid), 0);
1886
1887            free_notation(copy);
1888            free_dtd(dtd);
1889            allocator::xmlFreeImpl(doc as *mut c_void);
1890        }
1891    }
1892
1893    /// Verify that `copy_notation` accepts a NULL pointer.
1894    ///
1895    /// # Safety
1896    ///
1897    /// - NULL is allowed and is not dereferenced.
1898    #[test]
1899    fn test_copy_notation_null() {
1900        unsafe {
1901            assert!(copy_notation(ptr::null_mut()).is_null());
1902        }
1903    }
1904
1905    /// Verify that `free_notation` accepts a NULL pointer.
1906    ///
1907    /// # Safety
1908    ///
1909    /// - NULL is allowed and is not dereferenced.
1910    #[test]
1911    fn test_free_notation_null() {
1912        unsafe {
1913            free_notation(ptr::null_mut()); // Should not crash
1914        }
1915    }
1916
1917    // ── Content Model Tests ─────────────────────────────────────────────
1918
1919    /// Verify creating and freeing a content model.
1920    ///
1921    /// # Safety
1922    ///
1923    /// - The name buffer passed to `create_content_model` must be
1924    ///   NUL-terminated and alive for the call; the returned model is freed
1925    ///   with `free_content_model`.
1926    #[test]
1927    fn test_create_free_content_model() {
1928        unsafe {
1929            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1930            assert!(!cm.is_null());
1931            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1932            assert_eq!((*cm).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1933
1934            free_content_model(cm);
1935        }
1936    }
1937
1938    /// Verify creating a PCDATA content model.
1939    ///
1940    /// # Safety
1941    ///
1942    /// - The returned model must be valid while read and is freed with
1943    ///   `free_content_model`.
1944    #[test]
1945    fn test_create_content_model_pcdata() {
1946        unsafe {
1947            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
1948            assert!(!cm.is_null());
1949            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_PCDATA as c_int);
1950            free_content_model(cm);
1951        }
1952    }
1953
1954    /// Verify `copy_content_model` deep-copies a content model.
1955    ///
1956    /// # Safety
1957    ///
1958    /// - `cm` must be a valid `_xmlElementContent` while copied; both models
1959    ///   are freed with `free_content_model`.
1960    #[test]
1961    fn test_copy_content_model() {
1962        unsafe {
1963            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1964            assert!(!cm.is_null());
1965
1966            let copy = copy_content_model(cm);
1967            assert!(!copy.is_null());
1968            assert_ne!(copy, cm);
1969            assert_eq!((*copy).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1970            assert_eq!((*copy).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1971            assert_eq!(string::xml_strcmp((*copy).name, (*cm).name), 0);
1972
1973            free_content_model(cm);
1974            free_content_model(copy);
1975        }
1976    }
1977
1978    /// Verify that `copy_content_model` accepts a NULL pointer.
1979    ///
1980    /// # Safety
1981    ///
1982    /// - NULL is allowed and is not dereferenced.
1983    #[test]
1984    fn test_copy_content_model_null() {
1985        unsafe {
1986            assert!(copy_content_model(ptr::null_mut()).is_null());
1987        }
1988    }
1989
1990    /// Verify that `free_content_model` accepts a NULL pointer.
1991    ///
1992    /// # Safety
1993    ///
1994    /// - NULL is allowed and is not dereferenced.
1995    #[test]
1996    fn test_free_content_model_null() {
1997        unsafe {
1998            free_content_model(ptr::null_mut()); // Should not crash
1999        }
2000    }
2001
2002    /// Verify creating a sequence content model with two children.
2003    ///
2004    /// # Safety
2005    ///
2006    /// - `c1`, `c2`, and `seq` must be valid `_xmlElementContent` pointers
2007    ///   whose `parent`/`c1`/`c2` links are consistent before `free_content_model`
2008    ///   walks them.
2009    #[test]
2010    fn test_create_sequence_content_model() {
2011        unsafe {
2012            let c1 = create_content_model(c_str(b"a"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2013            let c2 = create_content_model(c_str(b"b"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2014            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
2015            assert!(!seq.is_null());
2016            (*seq).c1 = c1;
2017            (*seq).c2 = c2;
2018            (*c1).parent = seq;
2019            (*c2).parent = seq;
2020
2021            free_content_model(seq);
2022        }
2023    }
2024
2025    // ── Element Declaration Tests ───────────────────────────────────────
2026
2027    /// Verify adding and looking up an element declaration.
2028    ///
2029    /// # Safety
2030    ///
2031    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2032    ///   must be valid and alive for the calls; the DTD is freed with
2033    ///   `free_dtd` and the doc with `xmlFreeImpl`.
2034    #[test]
2035    fn test_add_get_element() {
2036        unsafe {
2037            let (doc, dtd) = make_doc_and_dtd();
2038            let name = c_str(b"myElement");
2039
2040            let elem =
2041                add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
2042            assert!(!elem.is_null());
2043            assert_eq!((*elem).etype, XML_ELEMENT_TYPE_EMPTY as c_int);
2044
2045            let found = get_element_decl(dtd, name);
2046            assert_eq!(found, elem);
2047
2048            let not_found = get_element_decl(dtd, c_str(b"nonexistent"));
2049            assert!(not_found.is_null());
2050
2051            free_dtd(dtd);
2052            allocator::xmlFreeImpl(doc as *mut c_void);
2053        }
2054    }
2055
2056    /// Verify that adding a duplicate element declaration returns the
2057    /// existing declaration unchanged.
2058    ///
2059    /// # Safety
2060    ///
2061    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated name buffer must
2062    ///   be valid and alive for the calls; the DTD is freed with `free_dtd`
2063    ///   and the doc with `xmlFreeImpl`.
2064    #[test]
2065    fn test_add_element_duplicate() {
2066        unsafe {
2067            let (doc, dtd) = make_doc_and_dtd();
2068            let name = c_str(b"dup");
2069
2070            let e1 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
2071            assert!(!e1.is_null());
2072
2073            let e2 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ANY as c_int, ptr::null_mut());
2074            assert_eq!(e1, e2); // Same pointer returned
2075            assert_eq!((*e2).type_, XML_ELEMENT_DECL as c_int); // node type
2076            assert_eq!((*e2).etype, XML_ELEMENT_TYPE_EMPTY as c_int); // Still empty
2077
2078            free_dtd(dtd);
2079            allocator::xmlFreeImpl(doc as *mut c_void);
2080        }
2081    }
2082
2083    /// Verify that adding an element to a NULL DTD returns NULL.
2084    ///
2085    /// # Safety
2086    ///
2087    /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
2088    ///   buffer must be NUL-terminated.
2089    #[test]
2090    fn test_add_element_null_dtd() {
2091        unsafe {
2092            let elem = add_element_decl(
2093                ptr::null_mut(),
2094                c_str(b"test"),
2095                XML_ELEMENT_TYPE_EMPTY as c_int,
2096                ptr::null_mut(),
2097            );
2098            assert!(elem.is_null());
2099        }
2100    }
2101
2102    /// Verify `copy_element` deep-copies an element declaration.
2103    ///
2104    /// # Safety
2105    ///
2106    /// - `elem` and its content model must be valid while copied; the copy is
2107    ///   freed with `free_element`, the DTD with `free_dtd`, and the doc with
2108    ///   `xmlFreeImpl`.
2109    #[test]
2110    fn test_copy_element() {
2111        unsafe {
2112            let (doc, dtd) = make_doc_and_dtd();
2113            let name = c_str(b"source");
2114            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2115
2116            let elem = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ELEMENT as c_int, cm);
2117            assert!(!elem.is_null());
2118
2119            let copy = copy_element(elem);
2120            assert!(!copy.is_null());
2121            assert_ne!(copy, elem);
2122            assert_eq!((*copy).type_, XML_ELEMENT_DECL as c_int);
2123            assert_eq!((*copy).etype, XML_ELEMENT_TYPE_ELEMENT as c_int);
2124            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
2125            assert!(!(*copy).content.is_null());
2126            assert_ne!((*copy).content, cm);
2127
2128            free_element(copy);
2129            free_dtd(dtd);
2130            allocator::xmlFreeImpl(doc as *mut c_void);
2131        }
2132    }
2133
2134    /// Verify that `free_element` accepts a NULL pointer.
2135    ///
2136    /// # Safety
2137    ///
2138    /// - NULL is allowed and is not dereferenced.
2139    #[test]
2140    fn test_free_element_null() {
2141        unsafe {
2142            free_element(ptr::null_mut()); // Should not crash
2143        }
2144    }
2145
2146    // ── Attribute Declaration Tests ─────────────────────────────────────
2147
2148    /// Verify adding and looking up an attribute declaration.
2149    ///
2150    /// # Safety
2151    ///
2152    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2153    ///   must be valid and alive for the calls; the DTD is freed with
2154    ///   `free_dtd` and the doc with `xmlFreeImpl`.
2155    #[test]
2156    fn test_add_get_attribute() {
2157        unsafe {
2158            let (doc, dtd) = make_doc_and_dtd();
2159            let elem_name = c_str(b"elem");
2160            let attr_name = c_str(b"attr1");
2161
2162            let elem = add_element_decl(
2163                dtd,
2164                elem_name,
2165                XML_ELEMENT_TYPE_EMPTY as c_int,
2166                ptr::null_mut(),
2167            );
2168            assert!(!elem.is_null());
2169
2170            let attr = add_attribute_decl(
2171                dtd,
2172                elem,
2173                attr_name,
2174                ptr::null(),
2175                XML_ATTRIBUTE_CDATA as c_int,
2176                XML_ATTRIBUTE_IMPLIED as c_int,
2177                ptr::null(),
2178                ptr::null_mut(),
2179            );
2180            assert!(!attr.is_null());
2181            assert_eq!((*attr).atype, XML_ATTRIBUTE_CDATA as c_int);
2182            assert_eq!((*attr).def, XML_ATTRIBUTE_IMPLIED as c_int);
2183
2184            // Lookup by element + attribute name
2185            let found = get_attribute_decl(dtd, elem, attr_name, 0);
2186            assert_eq!(found, attr);
2187
2188            // Lookup non-existent
2189            let not_found = get_attribute_decl(dtd, elem, c_str(b"nonexistent"), 0);
2190            assert!(not_found.is_null());
2191
2192            free_dtd(dtd);
2193            allocator::xmlFreeImpl(doc as *mut c_void);
2194        }
2195    }
2196
2197    /// Verify adding an attribute declaration with a default value.
2198    ///
2199    /// # Safety
2200    ///
2201    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2202    ///   must be valid and alive for the calls; the DTD is freed with
2203    ///   `free_dtd` and the doc with `xmlFreeImpl`.
2204    #[test]
2205    fn test_add_attribute_with_default() {
2206        unsafe {
2207            let (doc, dtd) = make_doc_and_dtd();
2208            let elem_name = c_str(b"elem");
2209            let attr_name = c_str(b"color");
2210            let default_val = c_str(b"red");
2211
2212            let elem = add_element_decl(
2213                dtd,
2214                elem_name,
2215                XML_ELEMENT_TYPE_EMPTY as c_int,
2216                ptr::null_mut(),
2217            );
2218
2219            let attr = add_attribute_decl(
2220                dtd,
2221                elem,
2222                attr_name,
2223                ptr::null(),
2224                XML_ATTRIBUTE_CDATA as c_int,
2225                XML_ATTRIBUTE_FIXED as c_int,
2226                default_val,
2227                ptr::null_mut(),
2228            );
2229            assert!(!attr.is_null());
2230            assert_eq!((*attr).def, XML_ATTRIBUTE_FIXED as c_int);
2231            assert_eq!(string::xml_strcmp((*attr).defaultValue, default_val), 0);
2232
2233            free_dtd(dtd);
2234            allocator::xmlFreeImpl(doc as *mut c_void);
2235        }
2236    }
2237
2238    /// Verify adding an attribute with an enumeration of values.
2239    ///
2240    /// # Safety
2241    ///
2242    /// - The `_xmlEnumeration` chain built with `xmlMallocZero` must consist
2243    ///   of valid structs with NUL-terminated duplicated names before it is
2244    ///   handed to `add_attribute_decl`; the DTD owns and frees it via
2245    ///   `free_dtd`, and the doc is freed with `xmlFreeImpl`.
2246    #[test]
2247    fn test_add_attribute_enumeration() {
2248        unsafe {
2249            let (doc, dtd) = make_doc_and_dtd();
2250            let elem_name = c_str(b"elem");
2251            let attr_name = c_str(b"size");
2252
2253            // Build enumeration: small, medium, large
2254            let v3 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2255                as *mut _xmlEnumeration;
2256            (*v3).name = string::xml_strdup(c_str(b"large"));
2257            let v2 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2258                as *mut _xmlEnumeration;
2259            (*v2).name = string::xml_strdup(c_str(b"medium"));
2260            (*v2).next = v3;
2261            let v1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2262                as *mut _xmlEnumeration;
2263            (*v1).name = string::xml_strdup(c_str(b"small"));
2264            (*v1).next = v2;
2265
2266            let elem = add_element_decl(
2267                dtd,
2268                elem_name,
2269                XML_ELEMENT_TYPE_EMPTY as c_int,
2270                ptr::null_mut(),
2271            );
2272            let attr = add_attribute_decl(
2273                dtd,
2274                elem,
2275                attr_name,
2276                ptr::null(),
2277                XML_ATTRIBUTE_ENUMERATION as c_int,
2278                XML_ATTRIBUTE_REQUIRED as c_int,
2279                ptr::null(),
2280                v1,
2281            );
2282            assert!(!attr.is_null());
2283            assert_eq!((*attr).atype, XML_ATTRIBUTE_ENUMERATION as c_int);
2284
2285            free_dtd(dtd);
2286            allocator::xmlFreeImpl(doc as *mut c_void);
2287        }
2288    }
2289
2290    /// Verify that adding an attribute to a NULL DTD returns NULL.
2291    ///
2292    /// # Safety
2293    ///
2294    /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
2295    ///   buffer must be NUL-terminated.
2296    #[test]
2297    fn test_add_attribute_null_dtd() {
2298        unsafe {
2299            let attr = add_attribute_decl(
2300                ptr::null_mut(),
2301                ptr::null_mut(),
2302                c_str(b"test"),
2303                ptr::null(),
2304                XML_ATTRIBUTE_CDATA as c_int,
2305                XML_ATTRIBUTE_IMPLIED as c_int,
2306                ptr::null(),
2307                ptr::null_mut(),
2308            );
2309            assert!(attr.is_null());
2310        }
2311    }
2312
2313    /// Verify `copy_attribute_decl` deep-copies an attribute declaration.
2314    ///
2315    /// # Safety
2316    ///
2317    /// - `attr` must be a valid `_xmlAttribute` while copied; the copy is
2318    ///   freed with `free_attribute`, the DTD with `free_dtd`, and the doc
2319    ///   with `xmlFreeImpl`.
2320    #[test]
2321    fn test_copy_attribute() {
2322        unsafe {
2323            let (doc, dtd) = make_doc_and_dtd();
2324            let elem_name = c_str(b"elem");
2325            let attr_name = c_str(b"id");
2326            let default_val = c_str(b"default");
2327
2328            let elem = add_element_decl(
2329                dtd,
2330                elem_name,
2331                XML_ELEMENT_TYPE_EMPTY as c_int,
2332                ptr::null_mut(),
2333            );
2334            let attr = add_attribute_decl(
2335                dtd,
2336                elem,
2337                attr_name,
2338                ptr::null(),
2339                XML_ATTRIBUTE_ID as c_int,
2340                XML_ATTRIBUTE_IMPLIED as c_int,
2341                default_val,
2342                ptr::null_mut(),
2343            );
2344            assert!(!attr.is_null());
2345
2346            let copy = copy_attribute_decl(attr);
2347            assert!(!copy.is_null());
2348            assert_ne!(copy, attr);
2349            assert_eq!((*copy).atype, XML_ATTRIBUTE_ID as c_int);
2350            assert_eq!(string::xml_strcmp((*copy).name, attr_name), 0);
2351
2352            free_attribute(copy);
2353            free_dtd(dtd);
2354            allocator::xmlFreeImpl(doc as *mut c_void);
2355        }
2356    }
2357
2358    /// Verify that `free_attribute` accepts a NULL pointer.
2359    ///
2360    /// # Safety
2361    ///
2362    /// - NULL is allowed and is not dereferenced.
2363    #[test]
2364    fn test_free_attribute_null() {
2365        unsafe {
2366            free_attribute(ptr::null_mut()); // Should not crash
2367        }
2368    }
2369
2370    // ── Content Model Validation Tests ──────────────────────────────────
2371
2372    /// Verify that `valid_content_model` rejects a NULL model.
2373    ///
2374    /// # Safety
2375    ///
2376    /// - Passing a NULL `model` is allowed and is not dereferenced.
2377    #[test]
2378    fn test_valid_content_model_null() {
2379        unsafe {
2380            assert_eq!(
2381                valid_content_model(ptr::null_mut(), &[]),
2382                ContentModelResult::Invalid
2383            );
2384        }
2385    }
2386
2387    /// Verify PCDATA content-model validation.
2388    ///
2389    /// # Safety
2390    ///
2391    /// - `cm` must be a valid `_xmlElementContent` while validated; the name
2392    ///   buffer must be NUL-terminated and alive for the call. The model is
2393    ///   freed with `free_content_model`.
2394    #[test]
2395    fn test_valid_content_model_pcdata() {
2396        unsafe {
2397            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
2398            assert!(!cm.is_null());
2399
2400            // Empty content is valid for PCDATA
2401            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Valid);
2402
2403            // Non-empty content is invalid for PCDATA
2404            let name = c_str(b"child");
2405            assert_eq!(
2406                valid_content_model(cm, &[name]),
2407                ContentModelResult::Invalid
2408            );
2409
2410            free_content_model(cm);
2411        }
2412    }
2413
2414    /// Verify single-element content-model validation.
2415    ///
2416    /// # Safety
2417    ///
2418    /// - `cm` must be a valid `_xmlElementContent` and the name buffers
2419    ///   NUL-terminated and alive for the calls; the model is freed with
2420    ///   `free_content_model`.
2421    #[test]
2422    fn test_valid_content_model_element() {
2423        unsafe {
2424            let child_name = c_str(b"child");
2425            let cm = create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2426            assert!(!cm.is_null());
2427
2428            // Correct element
2429            assert_eq!(
2430                valid_content_model(cm, &[child_name]),
2431                ContentModelResult::Valid
2432            );
2433
2434            // Wrong element
2435            let other = c_str(b"other");
2436            assert_eq!(
2437                valid_content_model(cm, &[other]),
2438                ContentModelResult::Invalid
2439            );
2440
2441            // Too many elements
2442            assert_eq!(
2443                valid_content_model(cm, &[child_name, child_name]),
2444                ContentModelResult::Invalid
2445            );
2446
2447            // Empty
2448            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Invalid);
2449
2450            free_content_model(cm);
2451        }
2452    }
2453
2454    /// Verify sequence content-model validation.
2455    ///
2456    /// # Safety
2457    ///
2458    /// - `c1`, `c2`, and `seq` must be valid, linked `_xmlElementContent`
2459    ///   structs and the name buffers NUL-terminated for the calls; the
2460    ///   sequence is freed with `free_content_model`.
2461    #[test]
2462    fn test_valid_content_model_seq() {
2463        unsafe {
2464            let a_name = c_str(b"a");
2465            let b_name = c_str(b"b");
2466
2467            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2468            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2469            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
2470            (*seq).c1 = c1;
2471            (*seq).c2 = c2;
2472            (*c1).parent = seq;
2473            (*c2).parent = seq;
2474
2475            // Correct sequence
2476            assert_eq!(
2477                valid_content_model(seq, &[a_name, b_name]),
2478                ContentModelResult::Valid
2479            );
2480
2481            // Wrong order
2482            assert_eq!(
2483                valid_content_model(seq, &[b_name, a_name]),
2484                ContentModelResult::Invalid
2485            );
2486
2487            // Missing element
2488            assert_eq!(
2489                valid_content_model(seq, &[a_name]),
2490                ContentModelResult::Invalid
2491            );
2492
2493            free_content_model(seq);
2494        }
2495    }
2496
2497    /// Verify choice content-model validation.
2498    ///
2499    /// # Safety
2500    ///
2501    /// - `c1`, `c2`, and `choice` must be valid, linked `_xmlElementContent`
2502    ///   structs and the name buffers NUL-terminated for the calls; the
2503    ///   choice is freed with `free_content_model`.
2504    #[test]
2505    fn test_valid_content_model_or() {
2506        unsafe {
2507            let a_name = c_str(b"a");
2508            let b_name = c_str(b"b");
2509
2510            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2511            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2512            let choice = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_OR as c_int);
2513            (*choice).c1 = c1;
2514            (*choice).c2 = c2;
2515            (*c1).parent = choice;
2516            (*c2).parent = choice;
2517
2518            // First alternative
2519            assert_eq!(
2520                valid_content_model(choice, &[a_name]),
2521                ContentModelResult::Valid
2522            );
2523
2524            // Second alternative
2525            assert_eq!(
2526                valid_content_model(choice, &[b_name]),
2527                ContentModelResult::Valid
2528            );
2529
2530            // Neither
2531            let other = c_str(b"other");
2532            assert_eq!(
2533                valid_content_model(choice, &[other]),
2534                ContentModelResult::Invalid
2535            );
2536
2537            free_content_model(choice);
2538        }
2539    }
2540
2541    /// Verify optional-occurrence content-model validation.
2542    ///
2543    /// # Safety
2544    ///
2545    /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2546    ///   NUL-terminated for the calls; the model is freed with
2547    ///   `free_content_model`.
2548    #[test]
2549    fn test_valid_content_model_optional() {
2550        unsafe {
2551            let a_name = c_str(b"a");
2552
2553            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2554            (*c1).ocur = XML_ELEMENT_CONTENT_OPT as c_int;
2555
2556            // Empty is valid for optional
2557            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2558
2559            // One is valid
2560            assert_eq!(
2561                valid_content_model(c1, &[a_name]),
2562                ContentModelResult::Valid
2563            );
2564
2565            free_content_model(c1);
2566        }
2567    }
2568
2569    /// Verify zero-or-more content-model validation.
2570    ///
2571    /// # Safety
2572    ///
2573    /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2574    ///   NUL-terminated for the calls; the model is freed with
2575    ///   `free_content_model`.
2576    #[test]
2577    fn test_valid_content_model_zero_or_more() {
2578        unsafe {
2579            let a_name = c_str(b"a");
2580
2581            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2582            (*c1).ocur = XML_ELEMENT_CONTENT_MULT as c_int;
2583
2584            // Empty is valid
2585            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2586
2587            // One is valid
2588            assert_eq!(
2589                valid_content_model(c1, &[a_name]),
2590                ContentModelResult::Valid
2591            );
2592
2593            // Multiple is valid
2594            assert_eq!(
2595                valid_content_model(c1, &[a_name, a_name, a_name]),
2596                ContentModelResult::Valid
2597            );
2598
2599            free_content_model(c1);
2600        }
2601    }
2602
2603    /// Verify one-or-more content-model validation.
2604    ///
2605    /// # Safety
2606    ///
2607    /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2608    ///   NUL-terminated for the calls; the model is freed with
2609    ///   `free_content_model`.
2610    #[test]
2611    fn test_valid_content_model_one_or_more() {
2612        unsafe {
2613            let a_name = c_str(b"a");
2614
2615            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2616            (*c1).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
2617
2618            // Empty is invalid
2619            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Invalid);
2620
2621            // One is valid
2622            assert_eq!(
2623                valid_content_model(c1, &[a_name]),
2624                ContentModelResult::Valid
2625            );
2626
2627            // Multiple is valid
2628            assert_eq!(
2629                valid_content_model(c1, &[a_name, a_name]),
2630                ContentModelResult::Valid
2631            );
2632
2633            free_content_model(c1);
2634        }
2635    }
2636}