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        // Placeholder upgrade (add_element_decl carries the attribute chain
797        // of a removed UNDEFINED placeholder to the real declaration).
798        let mut elem_attrs: *mut _xmlAttribute = ptr::null_mut();
799        // UPSTREAM-PARITY (valid.c xmlAddElementDecl): the table is created
800        // lazily on first use.
801        if (*dtd).elements.is_null() {
802            (*dtd).elements = hash::hash_create(8) as *mut c_void;
803        }
804        let d = &*dtd;
805
806        // Check if element already exists
807        let existing = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
808        if !existing.is_null() {
809            let ex = existing as *mut _xmlElement;
810            if (*ex).etype != XML_ELEMENT_TYPE_UNDEFINED as c_int {
811                return ex;
812            }
813            // UPSTREAM-PARITY (valid.c xmlAddElementDecl): an UNDEFINED
814            // placeholder (created when an ATTLIST named an as-yet
815            // undeclared element) is REMOVED and freed; its attribute
816            // declarations are carried over to the real declaration that
817            // follows ("lookup old attributes inserted on an undefined
818            // element in the internal subset"). The placeholder was never
819            // linked into the DTD child list (get_element_decl_created), so
820            // only the hash entry is removed.
821            let old_attributes = (*ex).attributes;
822            (*ex).attributes = ptr::null_mut();
823            hash::hash_remove_entry(d.elements as *mut hash::HashTable, name, None);
824            free_element(ex);
825            elem_attrs = old_attributes;
826        }
827
828        // SAFETY: Allocate zero-initialized memory for the element.
829        let elem = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
830        if elem.is_null() {
831            return ptr::null_mut();
832        }
833
834        // UPSTREAM-PARITY (valid.c xmlAddElementDecl): the node type is
835        // XML_ELEMENT_DECL; the element type (EMPTY/ANY/MIXED/ELEMENT) is
836        // stored in `etype` only. The caller passes the element type as
837        // `type_` (matching the upstream parameter list).
838        (*elem).name = string::xml_strdup(name);
839        (*elem).type_ = XML_ELEMENT_DECL as c_int;
840        (*elem).etype = type_; // xmlElementTypeVal mirrors xmlElementType here
841        (*elem).content = content; // Takes ownership of the content model
842        (*elem).attributes = elem_attrs;
843        (*elem).prefix = ptr::null_mut();
844        (*elem).children = ptr::null_mut();
845        (*elem).last = ptr::null_mut();
846        (*elem).parent = dtd;
847        (*elem).next = ptr::null_mut();
848        (*elem).prev = ptr::null_mut();
849        (*elem).doc = (*dtd).doc;
850        (*elem).cont_model = ptr::null_mut();
851
852        // Add to hash table
853        let ret = hash::hash_add_entry(
854            d.elements as *mut hash::HashTable,
855            name,
856            elem as *mut c_void,
857        );
858        if ret != 0 {
859            // Failed to add
860            free_element(elem);
861            return ptr::null_mut();
862        }
863
864        // UPSTREAM-PARITY (valid.c xmlAddElementDecl "Link it to the DTD"):
865        // the element decl is a child node of the DTD.
866        if (*dtd).last.is_null() {
867            (*dtd).children = elem as *mut _xmlNode;
868            (*dtd).last = elem as *mut _xmlNode;
869        } else {
870            (*(*dtd).last).next = elem as *mut _xmlNode;
871            (*elem).prev = (*dtd).last;
872            (*dtd).last = elem as *mut _xmlNode;
873        }
874
875        elem
876    }
877}
878
879/// Look up an element declaration by name.
880///
881/// # UPSTREAM-PARITY
882///
883/// ```c
884/// xmlElementPtr xmlGetElementDecl(xmlDtdPtr dtd, const xmlChar *name);
885/// ```
886///
887/// # SAFETY
888///
889/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
890/// - `name` must be a valid null-terminated string.
891pub unsafe fn get_element_decl(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlElement {
892    if dtd.is_null() || name.is_null() {
893        return ptr::null_mut();
894    }
895
896    unsafe {
897        let d = &*dtd;
898        if d.elements.is_null() {
899            return ptr::null_mut();
900        }
901        let payload = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
902        payload as *mut _xmlElement
903    }
904}
905
906/// UPSTREAM-PARITY (valid.c xmlGetDtdElementDesc2): lookup an element
907/// declaration, creating an UNDEFINED placeholder when missing. The
908/// placeholder is registered in the elements table but NOT linked into the
909/// DTD's child list (the attribute-decl path that triggers this leaves the
910/// element undeclared).
911///
912/// # SAFETY
913///
914/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
915/// - `name` must be a valid null-terminated string.
916pub unsafe fn get_element_decl_created(
917    dtd: *mut _xmlDtd,
918    name: *const xmlChar,
919) -> *mut _xmlElement {
920    if dtd.is_null() || name.is_null() {
921        return ptr::null_mut();
922    }
923
924    unsafe {
925        if (*dtd).elements.is_null() {
926            (*dtd).elements = hash::hash_create(8) as *mut c_void;
927        }
928        let existing = hash::hash_lookup((*dtd).elements as *mut hash::HashTable, name);
929        if !existing.is_null() {
930            return existing as *mut _xmlElement;
931        }
932
933        let elem = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
934        if elem.is_null() {
935            return ptr::null_mut();
936        }
937        (*elem).type_ = XML_ELEMENT_DECL as c_int;
938        (*elem).name = string::xml_strdup(name);
939        (*elem).etype = XML_ELEMENT_TYPE_UNDEFINED as c_int;
940        (*elem).doc = (*dtd).doc;
941        (*elem).parent = dtd;
942        if hash::hash_add_entry(
943            (*dtd).elements as *mut hash::HashTable,
944            name,
945            elem as *mut c_void,
946        ) != 0
947        {
948            free_element(elem);
949            return ptr::null_mut();
950        }
951        elem
952    }
953}
954
955/// Deep copy an element declaration.
956///
957/// # UPSTREAM-PARITY
958///
959/// ```c
960/// xmlElementPtr xmlCopyElement(xmlElementPtr elem);
961/// ```
962///
963/// # SAFETY
964///
965/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
966pub unsafe fn copy_element(elem: *mut _xmlElement) -> *mut _xmlElement {
967    if elem.is_null() {
968        return ptr::null_mut();
969    }
970
971    unsafe {
972        let e = &*elem;
973
974        // SAFETY: Allocate zero-initialized memory for the copy.
975        let copy = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
976        if copy.is_null() {
977            return ptr::null_mut();
978        }
979
980        (*copy).name = string::xml_strdup(e.name);
981        (*copy).type_ = e.type_;
982        (*copy).etype = e.etype;
983        (*copy).content = copy_content_model(e.content);
984        (*copy).prefix = string::xml_strdup(e.prefix);
985        (*copy)._private = e._private;
986        (*copy).parent = e.parent;
987        (*copy).doc = e.doc;
988
989        // Copy attribute declarations (linked list)
990        if !e.attributes.is_null() {
991            // UPSTREAM-PARITY: We copy the attribute linked list by
992            // iterating and copying each attribute.
993            let mut src_attr = e.attributes;
994            let mut prev_copy: *mut _xmlAttribute = ptr::null_mut();
995            let mut first_copy: *mut _xmlAttribute = ptr::null_mut();
996
997            while !src_attr.is_null() {
998                let attr_copy = copy_attribute_decl(src_attr);
999                if attr_copy.is_null() {
1000                    // Free what we've copied so far
1001                    let mut to_free = first_copy;
1002                    while !to_free.is_null() {
1003                        let next = (*to_free).nexth;
1004                        free_attribute(to_free);
1005                        to_free = next;
1006                    }
1007                    allocator::xmlFreeImpl(copy as *mut c_void);
1008                    return ptr::null_mut();
1009                }
1010
1011                if prev_copy.is_null() {
1012                    first_copy = attr_copy;
1013                } else {
1014                    (*prev_copy).nexth = attr_copy;
1015                }
1016                prev_copy = attr_copy;
1017                src_attr = (*src_attr).nexth;
1018            }
1019
1020            (*copy).attributes = first_copy;
1021        }
1022
1023        copy
1024    }
1025}
1026
1027/// Free an element declaration and its content model.
1028///
1029/// # UPSTREAM-PARITY
1030///
1031/// ```c
1032/// void xmlFreeElement(xmlElementPtr elem);
1033/// ```
1034///
1035/// Frees the element declaration and its content model, but NOT the
1036/// attribute declarations (which are owned by the DTD's attribute hash).
1037///
1038/// # SAFETY
1039///
1040/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
1041pub unsafe fn free_element(elem: *mut _xmlElement) {
1042    if elem.is_null() {
1043        return;
1044    }
1045
1046    unsafe {
1047        // Free name
1048        if !(*elem).name.is_null() {
1049            allocator::xmlFreeImpl((*elem).name as *mut c_void);
1050        }
1051
1052        // Free prefix
1053        if !(*elem).prefix.is_null() {
1054            allocator::xmlFreeImpl((*elem).prefix as *mut c_void);
1055        }
1056
1057        // Free content model
1058        if !(*elem).content.is_null() {
1059            free_content_model((*elem).content);
1060        }
1061
1062        // Free the compiled content-model NFA (xmlValidBuildContentModel)
1063        // UPSTREAM-PARITY: xmlFreeElement releases contModel via xmlRegFreeRegexp.
1064        if !(*elem).cont_model.is_null() {
1065            crate::xml::validation::free_content_model_nfa(
1066                (*elem).cont_model as *mut crate::xml::validation::ContentModelNfa,
1067            );
1068        }
1069
1070        // UPSTREAM-PARITY: The attributes linked list on the element
1071        // declaration is NOT owned by the element. The DTD's attribute
1072        // hash table is the sole owner. When the DTD is freed, the
1073        // hash table's deallocator frees all attributes.
1074        // Therefore, we do NOT free the attributes list here.
1075        (*elem).attributes = ptr::null_mut();
1076
1077        allocator::xmlFreeImpl(elem as *mut c_void);
1078    }
1079}
1080
1081// ═══════════════════════════════════════════════════════════════════════════════
1082// Attribute Declarations
1083// ═══════════════════════════════════════════════════════════════════════════════
1084
1085/// Free an enumeration value tree.
1086///
1087/// # SAFETY
1088///
1089/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
1090unsafe fn free_enumeration(tree: *mut _xmlEnumeration) {
1091    if tree.is_null() {
1092        return;
1093    }
1094
1095    unsafe {
1096        let mut cur = tree;
1097        while !cur.is_null() {
1098            let next = (*cur).next;
1099            if !(*cur).name.is_null() {
1100                allocator::xmlFreeImpl((*cur).name as *mut c_void);
1101            }
1102            allocator::xmlFreeImpl(cur as *mut c_void);
1103            cur = next;
1104        }
1105    }
1106}
1107
1108/// Deep copy an enumeration value tree.
1109///
1110/// # SAFETY
1111///
1112/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
1113unsafe fn copy_enumeration(tree: *mut _xmlEnumeration) -> *mut _xmlEnumeration {
1114    if tree.is_null() {
1115        return ptr::null_mut();
1116    }
1117
1118    unsafe {
1119        let mut src = tree;
1120        let mut first_copy: *mut _xmlEnumeration = ptr::null_mut();
1121        let mut prev_copy: *mut _xmlEnumeration = ptr::null_mut();
1122
1123        while !src.is_null() {
1124            let copy = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1125                as *mut _xmlEnumeration;
1126            if copy.is_null() {
1127                // Free what we've allocated so far
1128                let mut to_free = first_copy;
1129                while !to_free.is_null() {
1130                    let next = (*to_free).next;
1131                    if !(*to_free).name.is_null() {
1132                        allocator::xmlFreeImpl((*to_free).name as *mut c_void);
1133                    }
1134                    allocator::xmlFreeImpl(to_free as *mut c_void);
1135                    to_free = next;
1136                }
1137                return ptr::null_mut();
1138            }
1139
1140            (*copy).name = string::xml_strdup((*src).name);
1141            (*copy).next = ptr::null_mut();
1142
1143            if prev_copy.is_null() {
1144                first_copy = copy;
1145            } else {
1146                (*prev_copy).next = copy;
1147            }
1148            prev_copy = copy;
1149            src = (*src).next;
1150        }
1151
1152        first_copy
1153    }
1154}
1155
1156/// Add an attribute declaration to a DTD.
1157///
1158/// # UPSTREAM-PARITY
1159///
1160/// ```c
1161/// xmlAttributePtr xmlAddAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
1162///                                     const xmlChar *name, int type, int def,
1163///                                     const xmlChar *defaultValue,
1164///                                     xmlEnumerationPtr tree);
1165/// ```
1166///
1167/// Adds an attribute declaration to both the DTD's attribute hash table
1168/// (keyed by element name + attribute name) and the element's linked list.
1169/// If an attribute with the same name already exists for this element,
1170/// the existing declaration is returned.
1171///
1172/// # SAFETY
1173///
1174/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1175/// - `name` must be a valid null-terminated string.
1176/// - `elem`, `defaultValue`, `tree` may be NULL.
1177#[allow(clippy::too_many_arguments)]
1178pub unsafe fn add_attribute_decl(
1179    dtd: *mut _xmlDtd,
1180    elem: *mut _xmlElement,
1181    name: *const xmlChar,
1182    ns: *const xmlChar,
1183    type_: c_int,
1184    def: c_int,
1185    defaultValue: *const xmlChar,
1186    tree: *mut _xmlEnumeration,
1187) -> *mut _xmlAttribute {
1188    if dtd.is_null() || name.is_null() {
1189        return ptr::null_mut();
1190    }
1191
1192    unsafe {
1193        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): the table is
1194        // created lazily on first use.
1195        if (*dtd).attributes.is_null() {
1196            (*dtd).attributes = hash::hash_create(8) as *mut c_void;
1197        }
1198        let d = &*dtd;
1199        let elem_name = if elem.is_null() {
1200            ptr::null()
1201        } else {
1202            (*elem).name
1203        };
1204
1205        // Check if attribute already exists for this element
1206        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): the attribute table
1207        // is keyed by (name, ns, elem) — xmlHashAdd3/xmlHashLookup3 with the
1208        // namespace as the middle key (R-000176: the pre-2.10 candidate
1209        // signature dropped the ns key).
1210        let existing =
1211            hash::hash_lookup3(d.attributes as *mut hash::HashTable, name, ns, elem_name);
1212        if !existing.is_null() {
1213            return existing as *mut _xmlAttribute;
1214        }
1215
1216        // SAFETY: Allocate zero-initialized memory for the attribute.
1217        let attr =
1218            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1219        if attr.is_null() {
1220            return ptr::null_mut();
1221        }
1222
1223        (*attr).type_ = XML_ATTRIBUTE_DECL as c_int;
1224        (*attr).name = string::xml_strdup(name);
1225        (*attr).parent = dtd;
1226        (*attr).doc = d.doc;
1227        (*attr).nexth = ptr::null_mut();
1228        (*attr).atype = type_;
1229        (*attr).def = def;
1230        (*attr).defaultValue = string::xml_strdup(defaultValue);
1231        (*attr).tree = tree; // Takes ownership of the enumeration tree
1232                             // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): `prefix` mirrors the
1233                             // ns argument; NULL ns leaves it NULL.
1234        (*attr).prefix = if ns.is_null() {
1235            ptr::null_mut()
1236        } else {
1237            string::xml_strdup(ns)
1238        };
1239        (*attr).elem = string::xml_strdup(elem_name);
1240
1241        // Add to DTD's attribute hash table (keyed by attribute name,
1242        // namespace, element name — upstream xmlHashAdd3).
1243        let ret = hash::hash_add_entry3(
1244            d.attributes as *mut hash::HashTable,
1245            name,
1246            ns,
1247            elem_name,
1248            attr as *mut c_void,
1249        );
1250        if ret != 0 {
1251            // Failed to add
1252            if !(*attr).defaultValue.is_null() {
1253                allocator::xmlFreeImpl((*attr).defaultValue as *mut c_void);
1254            }
1255            if !(*attr).name.is_null() {
1256                allocator::xmlFreeImpl((*attr).name as *mut c_void);
1257            }
1258            if !(*attr).elem.is_null() {
1259                allocator::xmlFreeImpl((*attr).elem as *mut c_void);
1260            }
1261            if !(*attr).prefix.is_null() {
1262                allocator::xmlFreeImpl((*attr).prefix as *mut c_void);
1263            }
1264            allocator::xmlFreeImpl(attr as *mut c_void);
1265            // Don't free tree - caller still owns it on failure
1266            return ptr::null_mut();
1267        }
1268
1269        // Add to element's linked list
1270        if !elem.is_null() {
1271            (*attr).nexth = (*elem).attributes;
1272            (*elem).attributes = attr;
1273        }
1274
1275        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl "Link it to the
1276        // DTD"): the attribute decl is a child node of the DTD.
1277        if (*dtd).last.is_null() {
1278            (*dtd).children = attr as *mut _xmlNode;
1279            (*dtd).last = attr as *mut _xmlNode;
1280        } else {
1281            (*(*dtd).last).next = attr as *mut _xmlNode;
1282            (*attr).prev = (*dtd).last;
1283            (*dtd).last = attr as *mut _xmlNode;
1284        }
1285
1286        attr
1287    }
1288}
1289
1290/// Look up an attribute declaration by element name and attribute name.
1291///
1292/// # UPSTREAM-PARITY
1293///
1294/// ```c
1295/// xmlAttributePtr xmlGetAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
1296///                                     const xmlChar *name, int namePrefix);
1297/// ```
1298///
1299/// The `namePrefix` parameter is ignored in this implementation
1300/// (it's a legacy parameter in libxml2).
1301///
1302/// # SAFETY
1303///
1304/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1305/// - `name` must be a valid null-terminated string.
1306/// - `elem` may be NULL.
1307pub unsafe fn get_attribute_decl(
1308    dtd: *mut _xmlDtd,
1309    elem: *mut _xmlElement,
1310    name: *const xmlChar,
1311    _namePrefix: c_int,
1312) -> *mut _xmlAttribute {
1313    if dtd.is_null() || name.is_null() {
1314        return ptr::null_mut();
1315    }
1316
1317    unsafe {
1318        let d = &*dtd;
1319        let elem_name = if elem.is_null() {
1320            ptr::null()
1321        } else {
1322            (*elem).name
1323        };
1324
1325        // UPSTREAM-PARITY (valid.c xmlGetDtdQAttrDesc): keyed by
1326        // (name, prefix, elem).
1327        let payload = hash::hash_lookup3(
1328            d.attributes as *mut hash::HashTable,
1329            name,
1330            ptr::null(),
1331            elem_name,
1332        );
1333        payload as *mut _xmlAttribute
1334    }
1335}
1336
1337/// Deep copy an attribute declaration.
1338///
1339/// # UPSTREAM-PARITY
1340///
1341/// ```c
1342/// xmlAttributePtr xmlCopyAttribute(xmlAttributePtr attr);
1343/// ```
1344///
1345/// # SAFETY
1346///
1347/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1348pub unsafe fn copy_attribute_decl(attr: *mut _xmlAttribute) -> *mut _xmlAttribute {
1349    if attr.is_null() {
1350        return ptr::null_mut();
1351    }
1352
1353    unsafe {
1354        let a = &*attr;
1355
1356        // SAFETY: Allocate zero-initialized memory for the copy.
1357        let copy =
1358            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1359        if copy.is_null() {
1360            return ptr::null_mut();
1361        }
1362
1363        (*copy).type_ = a.type_;
1364        (*copy).name = string::xml_strdup(a.name);
1365        (*copy).parent = a.parent;
1366        (*copy).doc = a.doc;
1367        (*copy).nexth = ptr::null_mut();
1368        (*copy).atype = a.atype;
1369        (*copy).def = a.def;
1370        (*copy).defaultValue = string::xml_strdup(a.defaultValue);
1371        (*copy).tree = copy_enumeration(a.tree);
1372        (*copy).prefix = string::xml_strdup(a.prefix);
1373        (*copy).elem = string::xml_strdup(a.elem);
1374
1375        copy
1376    }
1377}
1378
1379/// Free an attribute declaration.
1380///
1381/// # UPSTREAM-PARITY
1382///
1383/// ```c
1384/// void xmlFreeAttribute(xmlAttributePtr attr);
1385/// ```
1386///
1387/// # SAFETY
1388///
1389/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1390pub unsafe fn free_attribute(attr: *mut _xmlAttribute) {
1391    if attr.is_null() {
1392        return;
1393    }
1394
1395    unsafe {
1396        let a = &*attr;
1397
1398        if !a.name.is_null() {
1399            allocator::xmlFreeImpl(a.name as *mut c_void);
1400        }
1401        if !a.defaultValue.is_null() {
1402            allocator::xmlFreeImpl(a.defaultValue as *mut c_void);
1403        }
1404        if !a.prefix.is_null() {
1405            allocator::xmlFreeImpl(a.prefix as *mut c_void);
1406        }
1407        if !a.elem.is_null() {
1408            allocator::xmlFreeImpl(a.elem as *mut c_void);
1409        }
1410        if !a.tree.is_null() {
1411            free_enumeration(a.tree);
1412        }
1413
1414        allocator::xmlFreeImpl(attr as *mut c_void);
1415    }
1416}
1417
1418// ═══════════════════════════════════════════════════════════════════════════════
1419// Content Model Validation (Automata-based)
1420// ═══════════════════════════════════════════════════════════════════════════════
1421
1422/// Result of content model validation.
1423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1424pub enum ContentModelResult {
1425    /// Content is valid.
1426    Valid,
1427    /// Content is invalid.
1428    Invalid,
1429    /// Content model is indeterminate (mixed content with PCDATA).
1430    Indeterminate,
1431}
1432
1433/// Validate content (a list of element names) against a content model,
1434/// taking occurrence indicators into account.
1435///
1436/// # UPSTREAM-PARITY
1437///
1438/// ```c
1439/// int xmlValidContentModel(xmlElementContentPtr model, ...)
1440/// ```
1441///
1442/// This implements a simple recursive descent validator for content models.
1443/// For simple content models (EMPTY, ANY, PCDATA), the check is direct.
1444/// For sequence/choice models, it recursively validates.
1445///
1446/// Returns `ContentModelResult::Valid` if the content matches the model,
1447/// `ContentModelResult::Invalid` otherwise.
1448///
1449/// # SAFETY
1450///
1451/// - `model` must be a valid pointer to an _xmlElementContent, or NULL.
1452/// - `names` must be a slice of element names (null-terminated xmlChar strings).
1453pub unsafe fn valid_content_model(
1454    model: *mut _xmlElementContent,
1455    names: &[*const xmlChar],
1456) -> ContentModelResult {
1457    if model.is_null() {
1458        return ContentModelResult::Invalid;
1459    }
1460
1461    unsafe {
1462        let m = &*model;
1463
1464        // Handle occurrence indicators at this level first
1465        match m.ocur as u32 {
1466            o if o == XML_ELEMENT_CONTENT_OPT as u32 => {
1467                // Optional: zero or one occurrence
1468                if names.is_empty() {
1469                    return ContentModelResult::Valid;
1470                }
1471                return valid_content_model_inner(model, names);
1472            }
1473            o if o == XML_ELEMENT_CONTENT_MULT as u32 => {
1474                // Zero or more
1475                if names.is_empty() {
1476                    return ContentModelResult::Valid;
1477                }
1478                return valid_content_model_zero_or_more(model, names);
1479            }
1480            o if o == XML_ELEMENT_CONTENT_PLUS as u32 => {
1481                // One or more
1482                if names.is_empty() {
1483                    return ContentModelResult::Invalid;
1484                }
1485                return valid_content_model_one_or_more(model, names);
1486            }
1487            _ => {}
1488        }
1489
1490        valid_content_model_inner(model, names)
1491    }
1492}
1493
1494/// Validate content against a content model without considering occurrence.
1495///
1496/// # Safety
1497///
1498/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1499///   dereferenced to read `type_` and `name`, and its `c1`/`c2` children are
1500///   followed when non-NULL.
1501/// - Each entry in `names` must be NULL or a valid pointer to a
1502///   NUL-terminated `xmlChar` element name; non-NULL entries are compared
1503///   with `xml_strcmp`.
1504unsafe fn valid_content_model_inner(
1505    model: *mut _xmlElementContent,
1506    names: &[*const xmlChar],
1507) -> ContentModelResult {
1508    unsafe {
1509        let m = &*model;
1510
1511        match m.type_ as u32 {
1512            t if t == XML_ELEMENT_CONTENT_PCDATA as u32 => {
1513                // PCDATA: content must be empty (just text)
1514                if names.is_empty() {
1515                    ContentModelResult::Valid
1516                } else {
1517                    ContentModelResult::Invalid
1518                }
1519            }
1520            t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
1521                // Single element: must match exactly one element
1522                if names.len() != 1 {
1523                    return ContentModelResult::Invalid;
1524                }
1525                if names[0].is_null() {
1526                    return ContentModelResult::Invalid;
1527                }
1528                // Compare with model name
1529                if string::xml_strcmp(names[0], m.name) != 0 {
1530                    return ContentModelResult::Invalid;
1531                }
1532                ContentModelResult::Valid
1533            }
1534            t if t == XML_ELEMENT_CONTENT_SEQ as u32 => {
1535                // Sequence: validate children in order
1536                valid_content_model_seq(m, names)
1537            }
1538            t if t == XML_ELEMENT_CONTENT_OR as u32 => {
1539                // Choice: one of the alternatives must match all names
1540                valid_content_model_or(m, names)
1541            }
1542            _ => ContentModelResult::Invalid,
1543        }
1544    }
1545}
1546
1547/// Validate content for zero-or-more occurrence.
1548///
1549/// # Safety
1550///
1551/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1552///   forwarded to `valid_content_model_inner`, which dereferences it.
1553/// - Each entry in `names` must be NULL or a valid pointer to a
1554///   NUL-terminated `xmlChar` element name.
1555unsafe fn valid_content_model_zero_or_more(
1556    model: *mut _xmlElementContent,
1557    names: &[*const xmlChar],
1558) -> ContentModelResult {
1559    // Zero or more: try each possible split
1560    let mut i = 0;
1561    while i <= names.len() {
1562        let consumed = &names[..i];
1563        let remaining = &names[i..];
1564
1565        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1566        if consumed_valid == ContentModelResult::Valid {
1567            if remaining.is_empty() {
1568                return ContentModelResult::Valid;
1569            }
1570            // Try to match remaining with same model
1571            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1572            if remaining_valid == ContentModelResult::Valid {
1573                return ContentModelResult::Valid;
1574            }
1575        }
1576
1577        i += 1;
1578    }
1579    ContentModelResult::Invalid
1580}
1581
1582/// Validate content for one-or-more occurrence.
1583///
1584/// # Safety
1585///
1586/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1587///   forwarded to `valid_content_model_inner`, which dereferences it.
1588/// - Each entry in `names` must be NULL or a valid pointer to a
1589///   NUL-terminated `xmlChar` element name.
1590unsafe fn valid_content_model_one_or_more(
1591    model: *mut _xmlElementContent,
1592    names: &[*const xmlChar],
1593) -> ContentModelResult {
1594    // One or more: must match at least once
1595    let mut i = 1;
1596    while i <= names.len() {
1597        let consumed = &names[..i];
1598        let remaining = &names[i..];
1599
1600        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1601        if consumed_valid == ContentModelResult::Valid {
1602            if remaining.is_empty() {
1603                return ContentModelResult::Valid;
1604            }
1605            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1606            if remaining_valid == ContentModelResult::Valid {
1607                return ContentModelResult::Valid;
1608            }
1609        }
1610
1611        i += 1;
1612    }
1613    ContentModelResult::Invalid
1614}
1615
1616/// Validate content against a sequence content model.
1617///
1618/// # Safety
1619///
1620/// - The `model` reference must point to a live `_xmlElementContent`.
1621/// - Non-NULL `c1`/`c2` children must be valid pointers to
1622///   `_xmlElementContent`; they are passed to `valid_content_model`.
1623/// - Each entry in `names` must be NULL or a valid pointer to a
1624///   NUL-terminated `xmlChar` element name.
1625unsafe fn valid_content_model_seq(
1626    model: &_xmlElementContent,
1627    names: &[*const xmlChar],
1628) -> ContentModelResult {
1629    // For a sequence, we need to split the names between c1 and c2
1630    // This is a simplified validation - full automata-based validation
1631    // would be more complex.
1632
1633    let c1 = model.c1;
1634    let c2 = model.c2;
1635
1636    if c1.is_null() && c2.is_null() {
1637        return ContentModelResult::Valid;
1638    }
1639
1640    if c1.is_null() {
1641        return unsafe { valid_content_model(c2, names) };
1642    }
1643
1644    if c2.is_null() {
1645        return unsafe { valid_content_model(c1, names) };
1646    }
1647
1648    // Try to split the names at each possible position
1649    // This implements a simple backtracking validator
1650    for split in 0..=names.len() {
1651        let left = &names[..split];
1652        let right = &names[split..];
1653
1654        let left_valid = unsafe { valid_content_model(c1, left) };
1655        if left_valid != ContentModelResult::Valid {
1656            continue;
1657        }
1658
1659        let right_valid = unsafe { valid_content_model(c2, right) };
1660        if right_valid == ContentModelResult::Valid {
1661            return ContentModelResult::Valid;
1662        }
1663    }
1664
1665    ContentModelResult::Invalid
1666}
1667
1668/// Validate content against a choice content model.
1669///
1670/// # Safety
1671///
1672/// - The `model` reference must point to a live `_xmlElementContent`.
1673/// - Non-NULL `c1`/`c2` children must be valid pointers to
1674///   `_xmlElementContent`; they are passed to `valid_content_model`.
1675/// - Each entry in `names` must be NULL or a valid pointer to a
1676///   NUL-terminated `xmlChar` element name.
1677unsafe fn valid_content_model_or(
1678    model: &_xmlElementContent,
1679    names: &[*const xmlChar],
1680) -> ContentModelResult {
1681    let c1 = model.c1;
1682    let c2 = model.c2;
1683
1684    if c1.is_null() && c2.is_null() {
1685        return ContentModelResult::Invalid;
1686    }
1687
1688    if !c1.is_null() {
1689        let result = unsafe { valid_content_model(c1, names) };
1690        if result == ContentModelResult::Valid {
1691            return ContentModelResult::Valid;
1692        }
1693    }
1694
1695    if !c2.is_null() {
1696        let result = unsafe { valid_content_model(c2, names) };
1697        if result == ContentModelResult::Valid {
1698            return ContentModelResult::Valid;
1699        }
1700    }
1701
1702    ContentModelResult::Invalid
1703}
1704
1705// ═══════════════════════════════════════════════════════════════════════════════
1706// Tests
1707// ═══════════════════════════════════════════════════════════════════════════════
1708
1709#[cfg(test)]
1710mod tests {
1711    use super::*;
1712
1713    use core::ffi::c_void;
1714    use core::ptr;
1715
1716    // ── Helpers ──────────────────────────────────────────────────────────
1717
1718    unsafe fn c_str(s: &[u8]) -> *const xmlChar {
1719        // Create a null-terminated xmlChar string
1720        let len = s.len();
1721        let buf = allocator::xmlMallocImpl(len + 1) as *mut xmlChar;
1722        assert!(!buf.is_null());
1723        ptr::copy_nonoverlapping(s.as_ptr(), buf, len);
1724        *buf.add(len) = 0;
1725        buf as *const xmlChar
1726    }
1727
1728    unsafe fn make_doc_and_dtd() -> (*mut _xmlDoc, *mut _xmlDtd) {
1729        let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1730        assert!(!doc.is_null());
1731        (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1732        (*doc).doc = doc;
1733        let dtd = create_int_subset(doc, c_str(b"root"), ptr::null(), ptr::null());
1734        assert!(!dtd.is_null());
1735        (doc, dtd)
1736    }
1737
1738    // ── DTD Access Tests ────────────────────────────────────────────────
1739
1740    /// Verify that `get_int_subset` accepts a NULL document.
1741    ///
1742    /// # Safety
1743    ///
1744    /// - NULL is allowed and is never dereferenced.
1745    #[test]
1746    fn test_get_int_subset_null() {
1747        {
1748            assert!(get_int_subset(ptr::null()).is_null());
1749        }
1750    }
1751
1752    /// Verify that `create_int_subset` attaches a DTD to a document.
1753    ///
1754    /// # Safety
1755    ///
1756    /// - `doc` and `dtd` from `make_doc_and_dtd` are heap-allocated structs
1757    ///   that must be valid while their fields are read; the DTD is freed with
1758    ///   `free_dtd` and the doc with `xmlFreeImpl`.
1759    #[test]
1760    fn test_create_int_subset() {
1761        unsafe {
1762            let (doc, dtd) = make_doc_and_dtd();
1763            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1764            assert!(!(*dtd).name.is_null());
1765            assert_eq!((*doc).intSubset, dtd);
1766
1767            // Cleanup
1768            free_dtd(dtd);
1769            allocator::xmlFreeImpl(doc as *mut c_void);
1770        }
1771    }
1772
1773    /// Verify that `create_int_subset` with a NULL doc still allocates and
1774    /// returns an unattached DTD (upstream tree.c `xmlCreateIntSubset` —
1775    /// HOSTILE-ABI A37).
1776    ///
1777    /// # Safety
1778    ///
1779    /// - A NULL `doc` is allowed and is not dereferenced; the returned DTD
1780    ///   is freed with `free_dtd`.
1781    #[test]
1782    fn test_create_int_subset_null_doc() {
1783        unsafe {
1784            let dtd = create_int_subset(ptr::null_mut(), c_str(b"root"), ptr::null(), ptr::null());
1785            assert!(!dtd.is_null());
1786            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1787            assert!((*dtd).parent.is_null());
1788            assert!((*dtd).doc.is_null());
1789            free_dtd(dtd);
1790        }
1791    }
1792
1793    /// Verify that `new_dtd` creates a DTD node and attaches it to the doc.
1794    ///
1795    /// # Safety
1796    ///
1797    /// - `doc` is a heap-allocated `_xmlDoc` and the strings passed to
1798    ///   `new_dtd` are NUL-terminated allocations that stay alive for the call.
1799    /// - `dtd` must be freed with `free_dtd` and the doc with `xmlFreeImpl`.
1800    #[test]
1801    fn test_new_dtd() {
1802        unsafe {
1803            let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1804            assert!(!doc.is_null());
1805            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1806            (*doc).doc = doc;
1807
1808            let dtd = new_dtd(doc, c_str(b"test"), c_str(b"-//TEST//"), c_str(b"test.dtd"));
1809            assert!(!dtd.is_null());
1810            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1811            assert_eq!((*doc).intSubset, dtd);
1812
1813            free_dtd(dtd);
1814            allocator::xmlFreeImpl(doc as *mut c_void);
1815        }
1816    }
1817
1818    /// Verify that `new_dtd` works without a document.
1819    ///
1820    /// # Safety
1821    ///
1822    /// - The strings passed to `new_dtd` must be NUL-terminated and alive for
1823    ///   the call; the returned DTD is freed with `free_dtd`.
1824    #[test]
1825    fn test_new_dtd_no_doc() {
1826        unsafe {
1827            let dtd = new_dtd(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1828            assert!(!dtd.is_null());
1829            free_dtd(dtd);
1830        }
1831    }
1832
1833    // ── Notation Tests ──────────────────────────────────────────────────
1834
1835    /// Verify adding and looking up a notation declaration.
1836    ///
1837    /// # Safety
1838    ///
1839    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
1840    ///   must be valid and alive for the calls; the DTD is freed with
1841    ///   `free_dtd` and the doc with `xmlFreeImpl`.
1842    #[test]
1843    fn test_add_get_notation() {
1844        unsafe {
1845            let (doc, dtd) = make_doc_and_dtd();
1846            let name = c_str(b"note");
1847            let pubid = c_str(b"-//TEST//NOTATION");
1848            let sysid = c_str(b"note.ent");
1849
1850            let n = add_notation_decl(dtd, name, pubid, sysid);
1851            assert!(!n.is_null());
1852            assert_eq!(string::xml_strcmp((*n).name, name), 0);
1853
1854            // Lookup
1855            let found = get_notation_decl(dtd, name);
1856            assert_eq!(found, n);
1857
1858            // Lookup non-existent
1859            let not_found = get_notation_decl(dtd, c_str(b"nonexistent"));
1860            assert!(not_found.is_null());
1861
1862            free_dtd(dtd);
1863            allocator::xmlFreeImpl(doc as *mut c_void);
1864        }
1865    }
1866
1867    /// Verify that adding a notation to a NULL DTD returns NULL.
1868    ///
1869    /// # Safety
1870    ///
1871    /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
1872    ///   buffer must be NUL-terminated.
1873    #[test]
1874    fn test_add_notation_null_dtd() {
1875        unsafe {
1876            let n = add_notation_decl(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1877            assert!(n.is_null());
1878        }
1879    }
1880
1881    /// Verify `copy_notation` deep-copies a notation declaration.
1882    ///
1883    /// # Safety
1884    ///
1885    /// - `n` from `add_notation_decl` must be a valid `_xmlNotation` while
1886    ///   copied; the copy is freed with `free_notation`, the DTD with
1887    ///   `free_dtd`, and the doc with `xmlFreeImpl`.
1888    #[test]
1889    fn test_copy_notation() {
1890        unsafe {
1891            let (doc, dtd) = make_doc_and_dtd();
1892            let name = c_str(b"note1");
1893            let pubid = c_str(b"public");
1894            let sysid = c_str(b"system");
1895
1896            let n = add_notation_decl(dtd, name, pubid, sysid);
1897            assert!(!n.is_null());
1898
1899            let copy = copy_notation(n);
1900            assert!(!copy.is_null());
1901            assert_ne!(copy, n);
1902            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1903            assert_eq!(string::xml_strcmp((*copy).PublicID, pubid), 0);
1904            assert_eq!(string::xml_strcmp((*copy).SystemID, sysid), 0);
1905
1906            free_notation(copy);
1907            free_dtd(dtd);
1908            allocator::xmlFreeImpl(doc as *mut c_void);
1909        }
1910    }
1911
1912    /// Verify that `copy_notation` accepts a NULL pointer.
1913    ///
1914    /// # Safety
1915    ///
1916    /// - NULL is allowed and is not dereferenced.
1917    #[test]
1918    fn test_copy_notation_null() {
1919        unsafe {
1920            assert!(copy_notation(ptr::null_mut()).is_null());
1921        }
1922    }
1923
1924    /// Verify that `free_notation` accepts a NULL pointer.
1925    ///
1926    /// # Safety
1927    ///
1928    /// - NULL is allowed and is not dereferenced.
1929    #[test]
1930    fn test_free_notation_null() {
1931        unsafe {
1932            free_notation(ptr::null_mut()); // Should not crash
1933        }
1934    }
1935
1936    // ── Content Model Tests ─────────────────────────────────────────────
1937
1938    /// Verify creating and freeing a content model.
1939    ///
1940    /// # Safety
1941    ///
1942    /// - The name buffer passed to `create_content_model` must be
1943    ///   NUL-terminated and alive for the call; the returned model is freed
1944    ///   with `free_content_model`.
1945    #[test]
1946    fn test_create_free_content_model() {
1947        unsafe {
1948            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1949            assert!(!cm.is_null());
1950            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1951            assert_eq!((*cm).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1952
1953            free_content_model(cm);
1954        }
1955    }
1956
1957    /// Verify creating a PCDATA content model.
1958    ///
1959    /// # Safety
1960    ///
1961    /// - The returned model must be valid while read and is freed with
1962    ///   `free_content_model`.
1963    #[test]
1964    fn test_create_content_model_pcdata() {
1965        unsafe {
1966            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
1967            assert!(!cm.is_null());
1968            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_PCDATA as c_int);
1969            free_content_model(cm);
1970        }
1971    }
1972
1973    /// Verify `copy_content_model` deep-copies a content model.
1974    ///
1975    /// # Safety
1976    ///
1977    /// - `cm` must be a valid `_xmlElementContent` while copied; both models
1978    ///   are freed with `free_content_model`.
1979    #[test]
1980    fn test_copy_content_model() {
1981        unsafe {
1982            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1983            assert!(!cm.is_null());
1984
1985            let copy = copy_content_model(cm);
1986            assert!(!copy.is_null());
1987            assert_ne!(copy, cm);
1988            assert_eq!((*copy).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1989            assert_eq!((*copy).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1990            assert_eq!(string::xml_strcmp((*copy).name, (*cm).name), 0);
1991
1992            free_content_model(cm);
1993            free_content_model(copy);
1994        }
1995    }
1996
1997    /// Verify that `copy_content_model` accepts a NULL pointer.
1998    ///
1999    /// # Safety
2000    ///
2001    /// - NULL is allowed and is not dereferenced.
2002    #[test]
2003    fn test_copy_content_model_null() {
2004        unsafe {
2005            assert!(copy_content_model(ptr::null_mut()).is_null());
2006        }
2007    }
2008
2009    /// Verify that `free_content_model` accepts a NULL pointer.
2010    ///
2011    /// # Safety
2012    ///
2013    /// - NULL is allowed and is not dereferenced.
2014    #[test]
2015    fn test_free_content_model_null() {
2016        unsafe {
2017            free_content_model(ptr::null_mut()); // Should not crash
2018        }
2019    }
2020
2021    /// Verify creating a sequence content model with two children.
2022    ///
2023    /// # Safety
2024    ///
2025    /// - `c1`, `c2`, and `seq` must be valid `_xmlElementContent` pointers
2026    ///   whose `parent`/`c1`/`c2` links are consistent before `free_content_model`
2027    ///   walks them.
2028    #[test]
2029    fn test_create_sequence_content_model() {
2030        unsafe {
2031            let c1 = create_content_model(c_str(b"a"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2032            let c2 = create_content_model(c_str(b"b"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2033            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
2034            assert!(!seq.is_null());
2035            (*seq).c1 = c1;
2036            (*seq).c2 = c2;
2037            (*c1).parent = seq;
2038            (*c2).parent = seq;
2039
2040            free_content_model(seq);
2041        }
2042    }
2043
2044    // ── Element Declaration Tests ───────────────────────────────────────
2045
2046    /// Verify adding and looking up an element declaration.
2047    ///
2048    /// # Safety
2049    ///
2050    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2051    ///   must be valid and alive for the calls; the DTD is freed with
2052    ///   `free_dtd` and the doc with `xmlFreeImpl`.
2053    #[test]
2054    fn test_add_get_element() {
2055        unsafe {
2056            let (doc, dtd) = make_doc_and_dtd();
2057            let name = c_str(b"myElement");
2058
2059            let elem =
2060                add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
2061            assert!(!elem.is_null());
2062            assert_eq!((*elem).etype, XML_ELEMENT_TYPE_EMPTY as c_int);
2063
2064            let found = get_element_decl(dtd, name);
2065            assert_eq!(found, elem);
2066
2067            let not_found = get_element_decl(dtd, c_str(b"nonexistent"));
2068            assert!(not_found.is_null());
2069
2070            free_dtd(dtd);
2071            allocator::xmlFreeImpl(doc as *mut c_void);
2072        }
2073    }
2074
2075    /// Verify that adding a duplicate element declaration returns the
2076    /// existing declaration unchanged.
2077    ///
2078    /// # Safety
2079    ///
2080    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated name buffer must
2081    ///   be valid and alive for the calls; the DTD is freed with `free_dtd`
2082    ///   and the doc with `xmlFreeImpl`.
2083    #[test]
2084    fn test_add_element_duplicate() {
2085        unsafe {
2086            let (doc, dtd) = make_doc_and_dtd();
2087            let name = c_str(b"dup");
2088
2089            let e1 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
2090            assert!(!e1.is_null());
2091
2092            let e2 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ANY as c_int, ptr::null_mut());
2093            assert_eq!(e1, e2); // Same pointer returned
2094            assert_eq!((*e2).type_, XML_ELEMENT_DECL as c_int); // node type
2095            assert_eq!((*e2).etype, XML_ELEMENT_TYPE_EMPTY as c_int); // Still empty
2096
2097            free_dtd(dtd);
2098            allocator::xmlFreeImpl(doc as *mut c_void);
2099        }
2100    }
2101
2102    /// Verify that adding an element to a NULL DTD returns NULL.
2103    ///
2104    /// # Safety
2105    ///
2106    /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
2107    ///   buffer must be NUL-terminated.
2108    #[test]
2109    fn test_add_element_null_dtd() {
2110        unsafe {
2111            let elem = add_element_decl(
2112                ptr::null_mut(),
2113                c_str(b"test"),
2114                XML_ELEMENT_TYPE_EMPTY as c_int,
2115                ptr::null_mut(),
2116            );
2117            assert!(elem.is_null());
2118        }
2119    }
2120
2121    /// Verify `copy_element` deep-copies an element declaration.
2122    ///
2123    /// # Safety
2124    ///
2125    /// - `elem` and its content model must be valid while copied; the copy is
2126    ///   freed with `free_element`, the DTD with `free_dtd`, and the doc with
2127    ///   `xmlFreeImpl`.
2128    #[test]
2129    fn test_copy_element() {
2130        unsafe {
2131            let (doc, dtd) = make_doc_and_dtd();
2132            let name = c_str(b"source");
2133            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2134
2135            let elem = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ELEMENT as c_int, cm);
2136            assert!(!elem.is_null());
2137
2138            let copy = copy_element(elem);
2139            assert!(!copy.is_null());
2140            assert_ne!(copy, elem);
2141            assert_eq!((*copy).type_, XML_ELEMENT_DECL as c_int);
2142            assert_eq!((*copy).etype, XML_ELEMENT_TYPE_ELEMENT as c_int);
2143            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
2144            assert!(!(*copy).content.is_null());
2145            assert_ne!((*copy).content, cm);
2146
2147            free_element(copy);
2148            free_dtd(dtd);
2149            allocator::xmlFreeImpl(doc as *mut c_void);
2150        }
2151    }
2152
2153    /// Verify that `free_element` accepts a NULL pointer.
2154    ///
2155    /// # Safety
2156    ///
2157    /// - NULL is allowed and is not dereferenced.
2158    #[test]
2159    fn test_free_element_null() {
2160        unsafe {
2161            free_element(ptr::null_mut()); // Should not crash
2162        }
2163    }
2164
2165    // ── Attribute Declaration Tests ─────────────────────────────────────
2166
2167    /// Verify adding and looking up an attribute declaration.
2168    ///
2169    /// # Safety
2170    ///
2171    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2172    ///   must be valid and alive for the calls; the DTD is freed with
2173    ///   `free_dtd` and the doc with `xmlFreeImpl`.
2174    #[test]
2175    fn test_add_get_attribute() {
2176        unsafe {
2177            let (doc, dtd) = make_doc_and_dtd();
2178            let elem_name = c_str(b"elem");
2179            let attr_name = c_str(b"attr1");
2180
2181            let elem = add_element_decl(
2182                dtd,
2183                elem_name,
2184                XML_ELEMENT_TYPE_EMPTY as c_int,
2185                ptr::null_mut(),
2186            );
2187            assert!(!elem.is_null());
2188
2189            let attr = add_attribute_decl(
2190                dtd,
2191                elem,
2192                attr_name,
2193                ptr::null(),
2194                XML_ATTRIBUTE_CDATA as c_int,
2195                XML_ATTRIBUTE_IMPLIED as c_int,
2196                ptr::null(),
2197                ptr::null_mut(),
2198            );
2199            assert!(!attr.is_null());
2200            assert_eq!((*attr).atype, XML_ATTRIBUTE_CDATA as c_int);
2201            assert_eq!((*attr).def, XML_ATTRIBUTE_IMPLIED as c_int);
2202
2203            // Lookup by element + attribute name
2204            let found = get_attribute_decl(dtd, elem, attr_name, 0);
2205            assert_eq!(found, attr);
2206
2207            // Lookup non-existent
2208            let not_found = get_attribute_decl(dtd, elem, c_str(b"nonexistent"), 0);
2209            assert!(not_found.is_null());
2210
2211            free_dtd(dtd);
2212            allocator::xmlFreeImpl(doc as *mut c_void);
2213        }
2214    }
2215
2216    /// Verify adding an attribute declaration with a default value.
2217    ///
2218    /// # Safety
2219    ///
2220    /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2221    ///   must be valid and alive for the calls; the DTD is freed with
2222    ///   `free_dtd` and the doc with `xmlFreeImpl`.
2223    #[test]
2224    fn test_add_attribute_with_default() {
2225        unsafe {
2226            let (doc, dtd) = make_doc_and_dtd();
2227            let elem_name = c_str(b"elem");
2228            let attr_name = c_str(b"color");
2229            let default_val = c_str(b"red");
2230
2231            let elem = add_element_decl(
2232                dtd,
2233                elem_name,
2234                XML_ELEMENT_TYPE_EMPTY as c_int,
2235                ptr::null_mut(),
2236            );
2237
2238            let attr = add_attribute_decl(
2239                dtd,
2240                elem,
2241                attr_name,
2242                ptr::null(),
2243                XML_ATTRIBUTE_CDATA as c_int,
2244                XML_ATTRIBUTE_FIXED as c_int,
2245                default_val,
2246                ptr::null_mut(),
2247            );
2248            assert!(!attr.is_null());
2249            assert_eq!((*attr).def, XML_ATTRIBUTE_FIXED as c_int);
2250            assert_eq!(string::xml_strcmp((*attr).defaultValue, default_val), 0);
2251
2252            free_dtd(dtd);
2253            allocator::xmlFreeImpl(doc as *mut c_void);
2254        }
2255    }
2256
2257    /// Verify adding an attribute with an enumeration of values.
2258    ///
2259    /// # Safety
2260    ///
2261    /// - The `_xmlEnumeration` chain built with `xmlMallocZero` must consist
2262    ///   of valid structs with NUL-terminated duplicated names before it is
2263    ///   handed to `add_attribute_decl`; the DTD owns and frees it via
2264    ///   `free_dtd`, and the doc is freed with `xmlFreeImpl`.
2265    #[test]
2266    fn test_add_attribute_enumeration() {
2267        unsafe {
2268            let (doc, dtd) = make_doc_and_dtd();
2269            let elem_name = c_str(b"elem");
2270            let attr_name = c_str(b"size");
2271
2272            // Build enumeration: small, medium, large
2273            let v3 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2274                as *mut _xmlEnumeration;
2275            (*v3).name = string::xml_strdup(c_str(b"large"));
2276            let v2 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2277                as *mut _xmlEnumeration;
2278            (*v2).name = string::xml_strdup(c_str(b"medium"));
2279            (*v2).next = v3;
2280            let v1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2281                as *mut _xmlEnumeration;
2282            (*v1).name = string::xml_strdup(c_str(b"small"));
2283            (*v1).next = v2;
2284
2285            let elem = add_element_decl(
2286                dtd,
2287                elem_name,
2288                XML_ELEMENT_TYPE_EMPTY as c_int,
2289                ptr::null_mut(),
2290            );
2291            let attr = add_attribute_decl(
2292                dtd,
2293                elem,
2294                attr_name,
2295                ptr::null(),
2296                XML_ATTRIBUTE_ENUMERATION as c_int,
2297                XML_ATTRIBUTE_REQUIRED as c_int,
2298                ptr::null(),
2299                v1,
2300            );
2301            assert!(!attr.is_null());
2302            assert_eq!((*attr).atype, XML_ATTRIBUTE_ENUMERATION as c_int);
2303
2304            free_dtd(dtd);
2305            allocator::xmlFreeImpl(doc as *mut c_void);
2306        }
2307    }
2308
2309    /// Verify that adding an attribute to a NULL DTD returns NULL.
2310    ///
2311    /// # Safety
2312    ///
2313    /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
2314    ///   buffer must be NUL-terminated.
2315    #[test]
2316    fn test_add_attribute_null_dtd() {
2317        unsafe {
2318            let attr = add_attribute_decl(
2319                ptr::null_mut(),
2320                ptr::null_mut(),
2321                c_str(b"test"),
2322                ptr::null(),
2323                XML_ATTRIBUTE_CDATA as c_int,
2324                XML_ATTRIBUTE_IMPLIED as c_int,
2325                ptr::null(),
2326                ptr::null_mut(),
2327            );
2328            assert!(attr.is_null());
2329        }
2330    }
2331
2332    /// Verify `copy_attribute_decl` deep-copies an attribute declaration.
2333    ///
2334    /// # Safety
2335    ///
2336    /// - `attr` must be a valid `_xmlAttribute` while copied; the copy is
2337    ///   freed with `free_attribute`, the DTD with `free_dtd`, and the doc
2338    ///   with `xmlFreeImpl`.
2339    #[test]
2340    fn test_copy_attribute() {
2341        unsafe {
2342            let (doc, dtd) = make_doc_and_dtd();
2343            let elem_name = c_str(b"elem");
2344            let attr_name = c_str(b"id");
2345            let default_val = c_str(b"default");
2346
2347            let elem = add_element_decl(
2348                dtd,
2349                elem_name,
2350                XML_ELEMENT_TYPE_EMPTY as c_int,
2351                ptr::null_mut(),
2352            );
2353            let attr = add_attribute_decl(
2354                dtd,
2355                elem,
2356                attr_name,
2357                ptr::null(),
2358                XML_ATTRIBUTE_ID as c_int,
2359                XML_ATTRIBUTE_IMPLIED as c_int,
2360                default_val,
2361                ptr::null_mut(),
2362            );
2363            assert!(!attr.is_null());
2364
2365            let copy = copy_attribute_decl(attr);
2366            assert!(!copy.is_null());
2367            assert_ne!(copy, attr);
2368            assert_eq!((*copy).atype, XML_ATTRIBUTE_ID as c_int);
2369            assert_eq!(string::xml_strcmp((*copy).name, attr_name), 0);
2370
2371            free_attribute(copy);
2372            free_dtd(dtd);
2373            allocator::xmlFreeImpl(doc as *mut c_void);
2374        }
2375    }
2376
2377    /// Verify that `free_attribute` accepts a NULL pointer.
2378    ///
2379    /// # Safety
2380    ///
2381    /// - NULL is allowed and is not dereferenced.
2382    #[test]
2383    fn test_free_attribute_null() {
2384        unsafe {
2385            free_attribute(ptr::null_mut()); // Should not crash
2386        }
2387    }
2388
2389    // ── Content Model Validation Tests ──────────────────────────────────
2390
2391    /// Verify that `valid_content_model` rejects a NULL model.
2392    ///
2393    /// # Safety
2394    ///
2395    /// - Passing a NULL `model` is allowed and is not dereferenced.
2396    #[test]
2397    fn test_valid_content_model_null() {
2398        unsafe {
2399            assert_eq!(
2400                valid_content_model(ptr::null_mut(), &[]),
2401                ContentModelResult::Invalid
2402            );
2403        }
2404    }
2405
2406    /// Verify PCDATA content-model validation.
2407    ///
2408    /// # Safety
2409    ///
2410    /// - `cm` must be a valid `_xmlElementContent` while validated; the name
2411    ///   buffer must be NUL-terminated and alive for the call. The model is
2412    ///   freed with `free_content_model`.
2413    #[test]
2414    fn test_valid_content_model_pcdata() {
2415        unsafe {
2416            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
2417            assert!(!cm.is_null());
2418
2419            // Empty content is valid for PCDATA
2420            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Valid);
2421
2422            // Non-empty content is invalid for PCDATA
2423            let name = c_str(b"child");
2424            assert_eq!(
2425                valid_content_model(cm, &[name]),
2426                ContentModelResult::Invalid
2427            );
2428
2429            free_content_model(cm);
2430        }
2431    }
2432
2433    /// Verify single-element content-model validation.
2434    ///
2435    /// # Safety
2436    ///
2437    /// - `cm` must be a valid `_xmlElementContent` and the name buffers
2438    ///   NUL-terminated and alive for the calls; the model is freed with
2439    ///   `free_content_model`.
2440    #[test]
2441    fn test_valid_content_model_element() {
2442        unsafe {
2443            let child_name = c_str(b"child");
2444            let cm = create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2445            assert!(!cm.is_null());
2446
2447            // Correct element
2448            assert_eq!(
2449                valid_content_model(cm, &[child_name]),
2450                ContentModelResult::Valid
2451            );
2452
2453            // Wrong element
2454            let other = c_str(b"other");
2455            assert_eq!(
2456                valid_content_model(cm, &[other]),
2457                ContentModelResult::Invalid
2458            );
2459
2460            // Too many elements
2461            assert_eq!(
2462                valid_content_model(cm, &[child_name, child_name]),
2463                ContentModelResult::Invalid
2464            );
2465
2466            // Empty
2467            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Invalid);
2468
2469            free_content_model(cm);
2470        }
2471    }
2472
2473    /// Verify sequence content-model validation.
2474    ///
2475    /// # Safety
2476    ///
2477    /// - `c1`, `c2`, and `seq` must be valid, linked `_xmlElementContent`
2478    ///   structs and the name buffers NUL-terminated for the calls; the
2479    ///   sequence is freed with `free_content_model`.
2480    #[test]
2481    fn test_valid_content_model_seq() {
2482        unsafe {
2483            let a_name = c_str(b"a");
2484            let b_name = c_str(b"b");
2485
2486            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2487            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2488            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
2489            (*seq).c1 = c1;
2490            (*seq).c2 = c2;
2491            (*c1).parent = seq;
2492            (*c2).parent = seq;
2493
2494            // Correct sequence
2495            assert_eq!(
2496                valid_content_model(seq, &[a_name, b_name]),
2497                ContentModelResult::Valid
2498            );
2499
2500            // Wrong order
2501            assert_eq!(
2502                valid_content_model(seq, &[b_name, a_name]),
2503                ContentModelResult::Invalid
2504            );
2505
2506            // Missing element
2507            assert_eq!(
2508                valid_content_model(seq, &[a_name]),
2509                ContentModelResult::Invalid
2510            );
2511
2512            free_content_model(seq);
2513        }
2514    }
2515
2516    /// Verify choice content-model validation.
2517    ///
2518    /// # Safety
2519    ///
2520    /// - `c1`, `c2`, and `choice` must be valid, linked `_xmlElementContent`
2521    ///   structs and the name buffers NUL-terminated for the calls; the
2522    ///   choice is freed with `free_content_model`.
2523    #[test]
2524    fn test_valid_content_model_or() {
2525        unsafe {
2526            let a_name = c_str(b"a");
2527            let b_name = c_str(b"b");
2528
2529            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2530            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2531            let choice = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_OR as c_int);
2532            (*choice).c1 = c1;
2533            (*choice).c2 = c2;
2534            (*c1).parent = choice;
2535            (*c2).parent = choice;
2536
2537            // First alternative
2538            assert_eq!(
2539                valid_content_model(choice, &[a_name]),
2540                ContentModelResult::Valid
2541            );
2542
2543            // Second alternative
2544            assert_eq!(
2545                valid_content_model(choice, &[b_name]),
2546                ContentModelResult::Valid
2547            );
2548
2549            // Neither
2550            let other = c_str(b"other");
2551            assert_eq!(
2552                valid_content_model(choice, &[other]),
2553                ContentModelResult::Invalid
2554            );
2555
2556            free_content_model(choice);
2557        }
2558    }
2559
2560    /// Verify optional-occurrence content-model validation.
2561    ///
2562    /// # Safety
2563    ///
2564    /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2565    ///   NUL-terminated for the calls; the model is freed with
2566    ///   `free_content_model`.
2567    #[test]
2568    fn test_valid_content_model_optional() {
2569        unsafe {
2570            let a_name = c_str(b"a");
2571
2572            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2573            (*c1).ocur = XML_ELEMENT_CONTENT_OPT as c_int;
2574
2575            // Empty is valid for optional
2576            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2577
2578            // One is valid
2579            assert_eq!(
2580                valid_content_model(c1, &[a_name]),
2581                ContentModelResult::Valid
2582            );
2583
2584            free_content_model(c1);
2585        }
2586    }
2587
2588    /// Verify zero-or-more content-model validation.
2589    ///
2590    /// # Safety
2591    ///
2592    /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2593    ///   NUL-terminated for the calls; the model is freed with
2594    ///   `free_content_model`.
2595    #[test]
2596    fn test_valid_content_model_zero_or_more() {
2597        unsafe {
2598            let a_name = c_str(b"a");
2599
2600            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2601            (*c1).ocur = XML_ELEMENT_CONTENT_MULT as c_int;
2602
2603            // Empty is valid
2604            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2605
2606            // One is valid
2607            assert_eq!(
2608                valid_content_model(c1, &[a_name]),
2609                ContentModelResult::Valid
2610            );
2611
2612            // Multiple is valid
2613            assert_eq!(
2614                valid_content_model(c1, &[a_name, a_name, a_name]),
2615                ContentModelResult::Valid
2616            );
2617
2618            free_content_model(c1);
2619        }
2620    }
2621
2622    /// Verify one-or-more content-model validation.
2623    ///
2624    /// # Safety
2625    ///
2626    /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2627    ///   NUL-terminated for the calls; the model is freed with
2628    ///   `free_content_model`.
2629    #[test]
2630    fn test_valid_content_model_one_or_more() {
2631        unsafe {
2632            let a_name = c_str(b"a");
2633
2634            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2635            (*c1).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
2636
2637            // Empty is invalid
2638            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Invalid);
2639
2640            // One is valid
2641            assert_eq!(
2642                valid_content_model(c1, &[a_name]),
2643                ContentModelResult::Valid
2644            );
2645
2646            // Multiple is valid
2647            assert_eq!(
2648                valid_content_model(c1, &[a_name, a_name]),
2649                ContentModelResult::Valid
2650            );
2651
2652            free_content_model(c1);
2653        }
2654    }
2655}