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