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
10use core::ffi::c_void;
11use core::ptr;
12use std::os::raw::{c_char, c_int, c_uint, c_ulong};
13
14use crate::abi::allocator;
15use crate::abi::structs::*;
16use crate::abi::types::xmlAttributeDefault::*;
17use crate::abi::types::xmlAttributeType::*;
18use crate::abi::types::xmlElementContentOccur::*;
19use crate::abi::types::xmlElementContentType::*;
20use crate::abi::types::xmlElementType::*;
21use crate::abi::types::xmlElementTypeVal::*;
22use crate::abi::types::*;
23use crate::xml::hash;
24use crate::xml::string;
25
26// ═══════════════════════════════════════════════════════════════════════════════
27// DTD Access
28// ═══════════════════════════════════════════════════════════════════════════════
29
30/// Get the internal subset of a document.
31///
32/// # UPSTREAM-PARITY
33///
34/// ```c
35/// xmlDtdPtr xmlGetIntSubset(const xmlDoc *doc);
36/// ```
37///
38/// Returns a pointer to the DTD, or NULL if none.
39pub fn get_int_subset(doc: *const _xmlDoc) -> *mut _xmlDtd {
40    if doc.is_null() {
41        return ptr::null_mut();
42    }
43    unsafe { (*doc).intSubset }
44}
45
46/// Create an internal subset (DTD) for a document.
47///
48/// # UPSTREAM-PARITY
49///
50/// ```c
51/// xmlDtdPtr xmlCreateIntSubset(xmlDocPtr doc, const xmlChar *name,
52///                              const xmlChar *ExternalID, const xmlChar *SystemID);
53/// ```
54///
55/// Creates a new DTD and attaches it as the document's internal subset.
56/// If the document already has an internal subset, it is replaced.
57///
58/// # SAFETY
59///
60/// - `doc` may be NULL (returns NULL).
61/// - `name`, `ExternalID`, `SystemID` must be valid null-terminated strings or NULL.
62pub unsafe fn create_int_subset(
63    doc: *mut _xmlDoc,
64    name: *const xmlChar,
65    ExternalID: *const xmlChar,
66    SystemID: *const xmlChar,
67) -> *mut _xmlDtd {
68    if doc.is_null() {
69        return ptr::null_mut();
70    }
71
72    // SAFETY: Allocate zero-initialized memory for the DTD.
73    let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
74    if dtd.is_null() {
75        return ptr::null_mut();
76    }
77
78    unsafe {
79        (*dtd).type_ = XML_DTD_NODE as c_int;
80        (*dtd).name = string::xml_strdup(name);
81        (*dtd).ExternalID = string::xml_strdup(ExternalID);
82        (*dtd).SystemID = string::xml_strdup(SystemID);
83        (*dtd).parent = doc;
84        (*dtd).doc = doc;
85
86        // Create hash tables for declarations
87        (*dtd).notations = hash::hash_create(8) as *mut c_void;
88        (*dtd).elements = hash::hash_create(16) as *mut c_void;
89        (*dtd).attributes = hash::hash_create(16) as *mut c_void;
90        (*dtd).entities = hash::hash_create(8) as *mut c_void;
91        (*dtd).pentities = hash::hash_create(8) as *mut c_void;
92
93        // Attach to document
94        (*doc).intSubset = dtd;
95    }
96
97    dtd
98}
99
100/// Create a new DTD.
101///
102/// # UPSTREAM-PARITY
103///
104/// ```c
105/// xmlDtdPtr xmlNewDtd(xmlDocPtr doc, const xmlChar *name,
106///                     const xmlChar *ExternalID, const xmlChar *SystemID);
107/// ```
108///
109/// Creates a new DTD and if `doc` is non-NULL and has no internal subset,
110/// attaches it as the document's internal subset.
111///
112/// # SAFETY
113///
114/// - `doc` may be NULL.
115/// - `name`, `ExternalID`, `SystemID` must be valid null-terminated strings or NULL.
116pub unsafe fn new_dtd(
117    doc: *mut _xmlDoc,
118    name: *const xmlChar,
119    ExternalID: *const xmlChar,
120    SystemID: *const xmlChar,
121) -> *mut _xmlDtd {
122    // SAFETY: Allocate zero-initialized memory for the DTD.
123    let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
124    if dtd.is_null() {
125        return ptr::null_mut();
126    }
127
128    unsafe {
129        (*dtd).type_ = XML_DTD_NODE as c_int;
130        (*dtd).name = string::xml_strdup(name);
131        (*dtd).ExternalID = string::xml_strdup(ExternalID);
132        (*dtd).SystemID = string::xml_strdup(SystemID);
133        (*dtd).parent = doc;
134        (*dtd).doc = doc;
135
136        // Create hash tables for declarations
137        (*dtd).notations = hash::hash_create(8) as *mut c_void;
138        (*dtd).elements = hash::hash_create(16) as *mut c_void;
139        (*dtd).attributes = hash::hash_create(16) as *mut c_void;
140        (*dtd).entities = hash::hash_create(8) as *mut c_void;
141        (*dtd).pentities = hash::hash_create(8) as *mut c_void;
142
143        // Attach to document if it has no internal subset yet
144        if !doc.is_null() && (*doc).intSubset.is_null() {
145            (*doc).intSubset = dtd;
146        }
147    }
148
149    dtd
150}
151
152/// extern "C" copier shims for `hash::hash_copy`.
153unsafe extern "C" fn copy_elem_cb(payload: *mut c_void, _name: *const xmlChar) -> *mut c_void {
154    unsafe { copy_element(payload as *mut _xmlElement) as *mut c_void }
155}
156
157unsafe extern "C" fn copy_attr_cb(payload: *mut c_void, _name: *const xmlChar) -> *mut c_void {
158    unsafe { copy_attribute_decl(payload as *mut _xmlAttribute) as *mut c_void }
159}
160
161unsafe extern "C" fn copy_ent_cb(payload: *mut c_void, _name: *const xmlChar) -> *mut c_void {
162    unsafe { crate::xml::entities::copy_entity(payload as *mut _xmlEntity) as *mut c_void }
163}
164
165unsafe extern "C" fn copy_notation_cb(payload: *mut c_void, _name: *const xmlChar) -> *mut c_void {
166    unsafe { copy_notation(payload as *mut _xmlNotation) as *mut c_void }
167}
168
169/// Deep-copy a DTD: name, external identifiers, and all declaration tables.
170///
171/// UPSTREAM-PARITY: the DTD portion of `xmlCopyDoc` / `xmlCopyDtd`.
172///
173/// Returns the new DTD, or NULL on failure.
174///
175/// # SAFETY
176///
177/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
178pub unsafe fn copy_dtd(dtd: *const _xmlDtd) -> *mut _xmlDtd {
179    if dtd.is_null() {
180        return ptr::null_mut();
181    }
182    let d = unsafe { &*dtd };
183
184    let copy = unsafe { allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd };
185    if copy.is_null() {
186        return ptr::null_mut();
187    }
188
189    unsafe {
190        (*copy).type_ = d.type_;
191        (*copy).name = string::xml_strdup(d.name);
192        (*copy).ExternalID = string::xml_strdup(d.ExternalID);
193        (*copy).SystemID = string::xml_strdup(d.SystemID);
194        (*copy).notations =
195            hash::hash_copy(d.notations as *mut hash::HashTable, Some(copy_notation_cb))
196                as *mut c_void;
197        (*copy).elements =
198            hash::hash_copy(d.elements as *mut hash::HashTable, Some(copy_elem_cb)) as *mut c_void;
199        (*copy).attributes =
200            hash::hash_copy(d.attributes as *mut hash::HashTable, Some(copy_attr_cb))
201                as *mut c_void;
202        (*copy).entities =
203            hash::hash_copy(d.entities as *mut hash::HashTable, Some(copy_ent_cb)) as *mut c_void;
204        (*copy).pentities =
205            hash::hash_copy(d.pentities as *mut hash::HashTable, Some(copy_ent_cb)) as *mut c_void;
206    }
207
208    copy
209}
210
211/// Free a DTD and all its declarations.
212///
213/// # UPSTREAM-PARITY
214///
215/// ```c
216/// void xmlFreeDtd(xmlDtdPtr dtd);
217/// ```
218///
219/// # SAFETY
220///
221/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
222pub unsafe fn free_dtd(dtd: *mut _xmlDtd) {
223    if dtd.is_null() {
224        return;
225    }
226
227    unsafe {
228        let d = &mut *dtd;
229
230        // Free hash tables with their deallocators
231        if !d.notations.is_null() {
232            hash::hash_free(
233                d.notations as *mut hash::HashTable,
234                Some(notation_deallocator),
235            );
236            d.notations = ptr::null_mut();
237        }
238        if !d.elements.is_null() {
239            hash::hash_free(
240                d.elements as *mut hash::HashTable,
241                Some(element_deallocator),
242            );
243            d.elements = ptr::null_mut();
244        }
245        if !d.attributes.is_null() {
246            hash::hash_free(
247                d.attributes as *mut hash::HashTable,
248                Some(attribute_deallocator),
249            );
250            d.attributes = ptr::null_mut();
251        }
252        if !d.entities.is_null() {
253            hash::hash_free(d.entities as *mut hash::HashTable, Some(entity_deallocator));
254            d.entities = ptr::null_mut();
255        }
256        if !d.pentities.is_null() {
257            hash::hash_free(
258                d.pentities as *mut hash::HashTable,
259                Some(entity_deallocator),
260            );
261            d.pentities = ptr::null_mut();
262        }
263
264        // Free strings
265        if !d.name.is_null() {
266            allocator::xmlFreeImpl(d.name as *mut c_void);
267        }
268        if !d.ExternalID.is_null() {
269            allocator::xmlFreeImpl(d.ExternalID as *mut c_void);
270        }
271        if !d.SystemID.is_null() {
272            allocator::xmlFreeImpl(d.SystemID as *mut c_void);
273        }
274
275        // Free children (tree nodes)
276        if !d.children.is_null() {
277            // Children are tree nodes, freed by the tree module
278            // For now we skip freeing children since we don't have the tree
279            // module's free_node_list accessible here. In practice,
280            // DTD children are freed by the caller or tree module.
281        }
282
283        allocator::xmlFreeImpl(dtd as *mut c_void);
284    }
285}
286
287// ═══════════════════════════════════════════════════════════════════════════════
288// Hash Deallocators
289// ═══════════════════════════════════════════════════════════════════════════════
290
291unsafe extern "C" fn notation_deallocator(payload: *mut c_void, _name: *mut u8) {
292    if !payload.is_null() {
293        free_notation(payload as *mut _xmlNotation);
294    }
295}
296
297unsafe extern "C" fn element_deallocator(payload: *mut c_void, _name: *mut u8) {
298    if !payload.is_null() {
299        free_element(payload as *mut _xmlElement);
300    }
301}
302
303unsafe extern "C" fn attribute_deallocator(payload: *mut c_void, _name: *mut u8) {
304    if !payload.is_null() {
305        free_attribute(payload as *mut _xmlAttribute);
306    }
307}
308
309unsafe extern "C" fn entity_deallocator(payload: *mut c_void, _name: *mut u8) {
310    if !payload.is_null() {
311        crate::xml::entities::free_entity(payload as *mut _xmlEntity);
312    }
313}
314
315// ═══════════════════════════════════════════════════════════════════════════════
316// Notation Declarations
317// ═══════════════════════════════════════════════════════════════════════════════
318
319/// Add a notation declaration to a DTD.
320///
321/// # UPSTREAM-PARITY
322///
323/// ```c
324/// xmlNotationPtr xmlAddNotationDecl(xmlDtdPtr dtd, const xmlChar *name,
325///                                   const xmlChar *PublicID,
326///                                   const xmlChar *SystemID);
327/// ```
328///
329/// # SAFETY
330///
331/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
332/// - `name` must be a valid null-terminated string.
333/// - `PublicID`, `SystemID` may be NULL.
334pub unsafe fn add_notation_decl(
335    dtd: *mut _xmlDtd,
336    name: *const xmlChar,
337    PublicID: *const xmlChar,
338    SystemID: *const xmlChar,
339) -> *mut _xmlNotation {
340    if dtd.is_null() || name.is_null() {
341        return ptr::null_mut();
342    }
343
344    unsafe {
345        let d = &*dtd;
346
347        // Check if notation already exists
348        let existing = hash::hash_lookup(d.notations as *mut hash::HashTable, name);
349        if !existing.is_null() {
350            return existing as *mut _xmlNotation;
351        }
352
353        // SAFETY: Allocate zero-initialized memory for the notation.
354        let not = allocator::xmlMallocZero(size_of::<_xmlNotation>() as usize) as *mut _xmlNotation;
355        if not.is_null() {
356            return ptr::null_mut();
357        }
358
359        (*not).name = string::xml_strdup(name);
360        (*not).PublicID = string::xml_strdup(PublicID);
361        (*not).SystemID = string::xml_strdup(SystemID);
362
363        // Add to hash table
364        let ret = hash::hash_add_entry(
365            d.notations as *mut hash::HashTable,
366            name,
367            not as *mut c_void,
368        );
369        if ret != 0 {
370            // Failed to add (shouldn't happen since we checked)
371            free_notation(not);
372            return ptr::null_mut();
373        }
374
375        not
376    }
377}
378
379/// Look up a notation declaration by name.
380///
381/// # UPSTREAM-PARITY
382///
383/// ```c
384/// xmlNotationPtr xmlGetNotationDecl(xmlDtdPtr dtd, const xmlChar *name);
385/// ```
386///
387/// # SAFETY
388///
389/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
390/// - `name` must be a valid null-terminated string.
391pub unsafe fn get_notation_decl(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlNotation {
392    if dtd.is_null() || name.is_null() {
393        return ptr::null_mut();
394    }
395
396    unsafe {
397        let d = &*dtd;
398        let payload = hash::hash_lookup(d.notations as *mut hash::HashTable, name);
399        payload as *mut _xmlNotation
400    }
401}
402
403/// Deep copy a notation declaration.
404///
405/// # UPSTREAM-PARITY
406///
407/// ```c
408/// xmlNotationPtr xmlCopyNotation(xmlNotationPtr notation);
409/// ```
410///
411/// # SAFETY
412///
413/// - `notation` must be a valid pointer to an _xmlNotation, or NULL.
414pub unsafe fn copy_notation(notation: *mut _xmlNotation) -> *mut _xmlNotation {
415    if notation.is_null() {
416        return ptr::null_mut();
417    }
418
419    unsafe {
420        let n = &*notation;
421        // SAFETY: Allocate zero-initialized memory for the copy.
422        let copy =
423            allocator::xmlMallocZero(size_of::<_xmlNotation>() as usize) as *mut _xmlNotation;
424        if copy.is_null() {
425            return ptr::null_mut();
426        }
427
428        (*copy).name = string::xml_strdup(n.name);
429        (*copy).PublicID = string::xml_strdup(n.PublicID);
430        (*copy).SystemID = string::xml_strdup(n.SystemID);
431
432        copy
433    }
434}
435
436/// Free a notation declaration.
437///
438/// # UPSTREAM-PARITY
439///
440/// ```c
441/// void xmlFreeNotation(xmlNotationPtr notation);
442/// ```
443///
444/// # SAFETY
445///
446/// - `notation` must be a valid pointer to an _xmlNotation, or NULL.
447pub unsafe fn free_notation(notation: *mut _xmlNotation) {
448    if notation.is_null() {
449        return;
450    }
451
452    unsafe {
453        let n = &*notation;
454        if !n.name.is_null() {
455            allocator::xmlFreeImpl(n.name as *mut c_void);
456        }
457        if !n.PublicID.is_null() {
458            allocator::xmlFreeImpl(n.PublicID as *mut c_void);
459        }
460        if !n.SystemID.is_null() {
461            allocator::xmlFreeImpl(n.SystemID as *mut c_void);
462        }
463        allocator::xmlFreeImpl(notation as *mut c_void);
464    }
465}
466
467// ═══════════════════════════════════════════════════════════════════════════════
468// Element Content Model Functions
469// ═══════════════════════════════════════════════════════════════════════════════
470
471/// Create a new element content model node.
472///
473/// # UPSTREAM-PARITY
474///
475/// ```c
476/// xmlElementContentPtr xmlNewElementContent(const xmlChar *name, int type);
477/// ```
478///
479/// Creates a content model node with the given name and type.
480/// The `ocur` field is set to XML_ELEMENT_CONTENT_ONCE by default.
481///
482/// # SAFETY
483///
484/// - `name` may be NULL (for PCDATA and connector types).
485pub unsafe fn create_content_model(name: *const xmlChar, type_: c_int) -> *mut _xmlElementContent {
486    // SAFETY: Allocate zero-initialized memory for the content model.
487    let content = allocator::xmlMallocZero(size_of::<_xmlElementContent>() as usize)
488        as *mut _xmlElementContent;
489    if content.is_null() {
490        return ptr::null_mut();
491    }
492
493    unsafe {
494        (*content).type_ = type_;
495        (*content).ocur = XML_ELEMENT_CONTENT_ONCE as c_int;
496        (*content).name = string::xml_strdup(name);
497        (*content).c1 = ptr::null_mut();
498        (*content).c2 = ptr::null_mut();
499        (*content).parent = ptr::null_mut();
500        (*content).prefix = ptr::null_mut();
501    }
502
503    content
504}
505
506/// Free an element content model tree.
507///
508/// # UPSTREAM-PARITY
509///
510/// ```c
511/// void xmlFreeElementContent(xmlElementContentPtr cur);
512/// ```
513///
514/// Recursively frees the entire content model tree.
515///
516/// # SAFETY
517///
518/// - `cur` must be a valid pointer to an _xmlElementContent, or NULL.
519pub unsafe fn free_content_model(cur: *mut _xmlElementContent) {
520    if cur.is_null() {
521        return;
522    }
523
524    unsafe {
525        let c = &*cur;
526
527        // Recursively free children
528        if !c.c1.is_null() {
529            free_content_model(c.c1);
530        }
531        if !c.c2.is_null() {
532            free_content_model(c.c2);
533        }
534
535        // Free name and prefix
536        if !c.name.is_null() {
537            allocator::xmlFreeImpl(c.name as *mut c_void);
538        }
539        if !c.prefix.is_null() {
540            allocator::xmlFreeImpl(c.prefix as *mut c_void);
541        }
542
543        allocator::xmlFreeImpl(cur as *mut c_void);
544    }
545}
546
547/// Deep copy an element content model tree.
548///
549/// # UPSTREAM-PARITY
550///
551/// ```c
552/// xmlElementContentPtr xmlCopyElementContent(xmlElementContentPtr content);
553/// ```
554///
555/// Recursively copies the entire content model tree.
556///
557/// # SAFETY
558///
559/// - `content` must be a valid pointer to an _xmlElementContent, or NULL.
560pub unsafe fn copy_content_model(content: *mut _xmlElementContent) -> *mut _xmlElementContent {
561    if content.is_null() {
562        return ptr::null_mut();
563    }
564
565    unsafe {
566        let c = &*content;
567
568        // SAFETY: Allocate zero-initialized memory for the copy.
569        let copy = allocator::xmlMallocZero(size_of::<_xmlElementContent>() as usize)
570            as *mut _xmlElementContent;
571        if copy.is_null() {
572            return ptr::null_mut();
573        }
574
575        (*copy).type_ = c.type_;
576        (*copy).ocur = c.ocur;
577        (*copy).name = string::xml_strdup(c.name);
578        (*copy).prefix = string::xml_strdup(c.prefix);
579
580        // Recursively copy children
581        if !c.c1.is_null() {
582            (*copy).c1 = copy_content_model(c.c1);
583            if !(*copy).c1.is_null() {
584                (*(*copy).c1).parent = copy;
585            }
586        }
587        if !c.c2.is_null() {
588            (*copy).c2 = copy_content_model(c.c2);
589            if !(*copy).c2.is_null() {
590                (*(*copy).c2).parent = copy;
591            }
592        }
593
594        copy
595    }
596}
597
598// ═══════════════════════════════════════════════════════════════════════════════
599// Element Declarations
600// ═══════════════════════════════════════════════════════════════════════════════
601
602/// Add an element declaration to a DTD.
603///
604/// # UPSTREAM-PARITY
605///
606/// ```c
607/// xmlElementPtr xmlAddElementDecl(xmlDtdPtr dtd, const xmlChar *name, int type,
608///                                 xmlElementContentPtr content);
609/// ```
610///
611/// If an element with the same name already exists, the existing one is returned
612/// and no new declaration is created.
613///
614/// # SAFETY
615///
616/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
617/// - `name` must be a valid null-terminated string.
618pub unsafe fn add_element_decl(
619    dtd: *mut _xmlDtd,
620    name: *const xmlChar,
621    type_: c_int,
622    content: *mut _xmlElementContent,
623) -> *mut _xmlElement {
624    if dtd.is_null() || name.is_null() {
625        return ptr::null_mut();
626    }
627
628    unsafe {
629        let d = &*dtd;
630
631        // Check if element already exists
632        let existing = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
633        if !existing.is_null() {
634            return existing as *mut _xmlElement;
635        }
636
637        // SAFETY: Allocate zero-initialized memory for the element.
638        let elem = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
639        if elem.is_null() {
640            return ptr::null_mut();
641        }
642
643        (*elem).name = string::xml_strdup(name);
644        (*elem).type_ = type_;
645        (*elem).etype = type_; // xmlElementTypeVal mirrors xmlElementType here
646        (*elem).content = content; // Takes ownership of the content model
647        (*elem).attributes = ptr::null_mut();
648        (*elem).prefix = ptr::null_mut();
649        (*elem).children = ptr::null_mut();
650        (*elem).last = ptr::null_mut();
651        (*elem).parent = dtd;
652        (*elem).next = ptr::null_mut();
653        (*elem).prev = ptr::null_mut();
654        (*elem).doc = ptr::null_mut();
655        (*elem).cont_model = ptr::null_mut();
656
657        // Add to hash table
658        let ret = hash::hash_add_entry(
659            d.elements as *mut hash::HashTable,
660            name,
661            elem as *mut c_void,
662        );
663        if ret != 0 {
664            // Failed to add
665            free_element(elem);
666            return ptr::null_mut();
667        }
668
669        elem
670    }
671}
672
673/// Look up an element declaration by name.
674///
675/// # UPSTREAM-PARITY
676///
677/// ```c
678/// xmlElementPtr xmlGetElementDecl(xmlDtdPtr dtd, const xmlChar *name);
679/// ```
680///
681/// # SAFETY
682///
683/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
684/// - `name` must be a valid null-terminated string.
685pub unsafe fn get_element_decl(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlElement {
686    if dtd.is_null() || name.is_null() {
687        return ptr::null_mut();
688    }
689
690    unsafe {
691        let d = &*dtd;
692        let payload = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
693        payload as *mut _xmlElement
694    }
695}
696
697/// Deep copy an element declaration.
698///
699/// # UPSTREAM-PARITY
700///
701/// ```c
702/// xmlElementPtr xmlCopyElement(xmlElementPtr elem);
703/// ```
704///
705/// # SAFETY
706///
707/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
708pub unsafe fn copy_element(elem: *mut _xmlElement) -> *mut _xmlElement {
709    if elem.is_null() {
710        return ptr::null_mut();
711    }
712
713    unsafe {
714        let e = &*elem;
715
716        // SAFETY: Allocate zero-initialized memory for the copy.
717        let copy = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
718        if copy.is_null() {
719            return ptr::null_mut();
720        }
721
722        (*copy).name = string::xml_strdup(e.name);
723        (*copy).type_ = e.type_;
724        (*copy).etype = e.etype;
725        (*copy).content = copy_content_model(e.content);
726        (*copy).prefix = string::xml_strdup(e.prefix);
727        (*copy)._private = e._private;
728        (*copy).parent = e.parent;
729        (*copy).doc = e.doc;
730
731        // Copy attribute declarations (linked list)
732        if !e.attributes.is_null() {
733            // UPSTREAM-PARITY: We copy the attribute linked list by
734            // iterating and copying each attribute.
735            let mut src_attr = e.attributes;
736            let mut prev_copy: *mut _xmlAttribute = ptr::null_mut();
737            let mut first_copy: *mut _xmlAttribute = ptr::null_mut();
738
739            while !src_attr.is_null() {
740                let attr_copy = copy_attribute_decl(src_attr);
741                if attr_copy.is_null() {
742                    // Free what we've copied so far
743                    let mut to_free = first_copy;
744                    while !to_free.is_null() {
745                        let next = (*to_free).nexth;
746                        free_attribute(to_free);
747                        to_free = next;
748                    }
749                    allocator::xmlFreeImpl(copy as *mut c_void);
750                    return ptr::null_mut();
751                }
752
753                if prev_copy.is_null() {
754                    first_copy = attr_copy;
755                } else {
756                    (*prev_copy).nexth = attr_copy;
757                }
758                prev_copy = attr_copy;
759                src_attr = (*src_attr).nexth;
760            }
761
762            (*copy).attributes = first_copy;
763        }
764
765        copy
766    }
767}
768
769/// Free an element declaration and its content model.
770///
771/// # UPSTREAM-PARITY
772///
773/// ```c
774/// void xmlFreeElement(xmlElementPtr elem);
775/// ```
776///
777/// Frees the element declaration and its content model, but NOT the
778/// attribute declarations (which are owned by the DTD's attribute hash).
779///
780/// # SAFETY
781///
782/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
783pub unsafe fn free_element(elem: *mut _xmlElement) {
784    if elem.is_null() {
785        return;
786    }
787
788    unsafe {
789        // Free name
790        if !(*elem).name.is_null() {
791            allocator::xmlFreeImpl((*elem).name as *mut c_void);
792        }
793
794        // Free prefix
795        if !(*elem).prefix.is_null() {
796            allocator::xmlFreeImpl((*elem).prefix as *mut c_void);
797        }
798
799        // Free content model
800        if !(*elem).content.is_null() {
801            free_content_model((*elem).content);
802        }
803
804        // Free the compiled content-model NFA (xmlValidBuildContentModel)
805        // UPSTREAM-PARITY: xmlFreeElement releases contModel via xmlRegFreeRegexp.
806        if !(*elem).cont_model.is_null() {
807            crate::xml::validation::free_content_model_nfa(
808                (*elem).cont_model as *mut crate::xml::validation::ContentModelNfa,
809            );
810        }
811
812        // UPSTREAM-PARITY: The attributes linked list on the element
813        // declaration is NOT owned by the element. The DTD's attribute
814        // hash table is the sole owner. When the DTD is freed, the
815        // hash table's deallocator frees all attributes.
816        // Therefore, we do NOT free the attributes list here.
817        (*elem).attributes = ptr::null_mut();
818
819        allocator::xmlFreeImpl(elem as *mut c_void);
820    }
821}
822
823// ═══════════════════════════════════════════════════════════════════════════════
824// Attribute Declarations
825// ═══════════════════════════════════════════════════════════════════════════════
826
827/// Free an enumeration value tree.
828///
829/// # SAFETY
830///
831/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
832unsafe fn free_enumeration(tree: *mut _xmlEnumeration) {
833    if tree.is_null() {
834        return;
835    }
836
837    unsafe {
838        let mut cur = tree;
839        while !cur.is_null() {
840            let next = (*cur).next;
841            if !(*cur).name.is_null() {
842                allocator::xmlFreeImpl((*cur).name as *mut c_void);
843            }
844            allocator::xmlFreeImpl(cur as *mut c_void);
845            cur = next;
846        }
847    }
848}
849
850/// Deep copy an enumeration value tree.
851///
852/// # SAFETY
853///
854/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
855unsafe fn copy_enumeration(tree: *mut _xmlEnumeration) -> *mut _xmlEnumeration {
856    if tree.is_null() {
857        return ptr::null_mut();
858    }
859
860    unsafe {
861        let mut src = tree;
862        let mut first_copy: *mut _xmlEnumeration = ptr::null_mut();
863        let mut prev_copy: *mut _xmlEnumeration = ptr::null_mut();
864
865        while !src.is_null() {
866            let copy = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
867                as *mut _xmlEnumeration;
868            if copy.is_null() {
869                // Free what we've allocated so far
870                let mut to_free = first_copy;
871                while !to_free.is_null() {
872                    let next = (*to_free).next;
873                    if !(*to_free).name.is_null() {
874                        allocator::xmlFreeImpl((*to_free).name as *mut c_void);
875                    }
876                    allocator::xmlFreeImpl(to_free as *mut c_void);
877                    to_free = next;
878                }
879                return ptr::null_mut();
880            }
881
882            (*copy).name = string::xml_strdup((*src).name);
883            (*copy).next = ptr::null_mut();
884
885            if prev_copy.is_null() {
886                first_copy = copy;
887            } else {
888                (*prev_copy).next = copy;
889            }
890            prev_copy = copy;
891            src = (*src).next;
892        }
893
894        first_copy
895    }
896}
897
898/// Add an attribute declaration to a DTD.
899///
900/// # UPSTREAM-PARITY
901///
902/// ```c
903/// xmlAttributePtr xmlAddAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
904///                                     const xmlChar *name, int type, int def,
905///                                     const xmlChar *defaultValue,
906///                                     xmlEnumerationPtr tree);
907/// ```
908///
909/// Adds an attribute declaration to both the DTD's attribute hash table
910/// (keyed by element name + attribute name) and the element's linked list.
911/// If an attribute with the same name already exists for this element,
912/// the existing declaration is returned.
913///
914/// # SAFETY
915///
916/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
917/// - `name` must be a valid null-terminated string.
918/// - `elem`, `defaultValue`, `tree` may be NULL.
919pub unsafe fn add_attribute_decl(
920    dtd: *mut _xmlDtd,
921    elem: *mut _xmlElement,
922    name: *const xmlChar,
923    type_: c_int,
924    def: c_int,
925    defaultValue: *const xmlChar,
926    tree: *mut _xmlEnumeration,
927) -> *mut _xmlAttribute {
928    if dtd.is_null() || name.is_null() {
929        return ptr::null_mut();
930    }
931
932    unsafe {
933        let d = &*dtd;
934        let elem_name = if elem.is_null() {
935            ptr::null()
936        } else {
937            (*elem).name
938        };
939
940        // Check if attribute already exists for this element
941        let existing = hash::hash_lookup2(d.attributes as *mut hash::HashTable, elem_name, name);
942        if !existing.is_null() {
943            return existing as *mut _xmlAttribute;
944        }
945
946        // SAFETY: Allocate zero-initialized memory for the attribute.
947        let attr =
948            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
949        if attr.is_null() {
950            return ptr::null_mut();
951        }
952
953        (*attr).type_ = XML_ATTRIBUTE_DECL as c_int;
954        (*attr).name = string::xml_strdup(name);
955        (*attr).parent = dtd;
956        (*attr).doc = d.doc;
957        (*attr).nexth = ptr::null_mut();
958        (*attr).atype = type_;
959        (*attr).def = def;
960        (*attr).defaultValue = string::xml_strdup(defaultValue);
961        (*attr).tree = tree; // Takes ownership of the enumeration tree
962        (*attr).prefix = ptr::null_mut();
963        (*attr).elem = string::xml_strdup(elem_name);
964
965        // Add to DTD's attribute hash table (keyed by element name + attribute name)
966        let ret = hash::hash_add_entry2(
967            d.attributes as *mut hash::HashTable,
968            elem_name,
969            name,
970            attr as *mut c_void,
971        );
972        if ret != 0 {
973            // Failed to add
974            if !(*attr).defaultValue.is_null() {
975                allocator::xmlFreeImpl((*attr).defaultValue as *mut c_void);
976            }
977            if !(*attr).name.is_null() {
978                allocator::xmlFreeImpl((*attr).name as *mut c_void);
979            }
980            if !(*attr).elem.is_null() {
981                allocator::xmlFreeImpl((*attr).elem as *mut c_void);
982            }
983            allocator::xmlFreeImpl(attr as *mut c_void);
984            // Don't free tree - caller still owns it on failure
985            return ptr::null_mut();
986        }
987
988        // Add to element's linked list
989        if !elem.is_null() {
990            (*attr).nexth = (*elem).attributes;
991            (*elem).attributes = attr;
992        }
993
994        attr
995    }
996}
997
998/// Look up an attribute declaration by element name and attribute name.
999///
1000/// # UPSTREAM-PARITY
1001///
1002/// ```c
1003/// xmlAttributePtr xmlGetAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
1004///                                     const xmlChar *name, int namePrefix);
1005/// ```
1006///
1007/// The `namePrefix` parameter is ignored in this implementation
1008/// (it's a legacy parameter in libxml2).
1009///
1010/// # SAFETY
1011///
1012/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1013/// - `name` must be a valid null-terminated string.
1014/// - `elem` may be NULL.
1015pub unsafe fn get_attribute_decl(
1016    dtd: *mut _xmlDtd,
1017    elem: *mut _xmlElement,
1018    name: *const xmlChar,
1019    _namePrefix: c_int,
1020) -> *mut _xmlAttribute {
1021    if dtd.is_null() || name.is_null() {
1022        return ptr::null_mut();
1023    }
1024
1025    unsafe {
1026        let d = &*dtd;
1027        let elem_name = if elem.is_null() {
1028            ptr::null()
1029        } else {
1030            (*elem).name
1031        };
1032
1033        let payload = hash::hash_lookup2(d.attributes as *mut hash::HashTable, elem_name, name);
1034        payload as *mut _xmlAttribute
1035    }
1036}
1037
1038/// Deep copy an attribute declaration.
1039///
1040/// # UPSTREAM-PARITY
1041///
1042/// ```c
1043/// xmlAttributePtr xmlCopyAttribute(xmlAttributePtr attr);
1044/// ```
1045///
1046/// # SAFETY
1047///
1048/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1049pub unsafe fn copy_attribute_decl(attr: *mut _xmlAttribute) -> *mut _xmlAttribute {
1050    if attr.is_null() {
1051        return ptr::null_mut();
1052    }
1053
1054    unsafe {
1055        let a = &*attr;
1056
1057        // SAFETY: Allocate zero-initialized memory for the copy.
1058        let copy =
1059            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1060        if copy.is_null() {
1061            return ptr::null_mut();
1062        }
1063
1064        (*copy).type_ = a.type_;
1065        (*copy).name = string::xml_strdup(a.name);
1066        (*copy).parent = a.parent;
1067        (*copy).doc = a.doc;
1068        (*copy).nexth = ptr::null_mut();
1069        (*copy).atype = a.atype;
1070        (*copy).def = a.def;
1071        (*copy).defaultValue = string::xml_strdup(a.defaultValue);
1072        (*copy).tree = copy_enumeration(a.tree);
1073        (*copy).prefix = string::xml_strdup(a.prefix);
1074        (*copy).elem = string::xml_strdup(a.elem);
1075
1076        copy
1077    }
1078}
1079
1080/// Free an attribute declaration.
1081///
1082/// # UPSTREAM-PARITY
1083///
1084/// ```c
1085/// void xmlFreeAttribute(xmlAttributePtr attr);
1086/// ```
1087///
1088/// # SAFETY
1089///
1090/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1091pub unsafe fn free_attribute(attr: *mut _xmlAttribute) {
1092    if attr.is_null() {
1093        return;
1094    }
1095
1096    unsafe {
1097        let a = &*attr;
1098
1099        if !a.name.is_null() {
1100            allocator::xmlFreeImpl(a.name as *mut c_void);
1101        }
1102        if !a.defaultValue.is_null() {
1103            allocator::xmlFreeImpl(a.defaultValue as *mut c_void);
1104        }
1105        if !a.prefix.is_null() {
1106            allocator::xmlFreeImpl(a.prefix as *mut c_void);
1107        }
1108        if !a.elem.is_null() {
1109            allocator::xmlFreeImpl(a.elem as *mut c_void);
1110        }
1111        if !a.tree.is_null() {
1112            free_enumeration(a.tree);
1113        }
1114
1115        allocator::xmlFreeImpl(attr as *mut c_void);
1116    }
1117}
1118
1119// ═══════════════════════════════════════════════════════════════════════════════
1120// Content Model Validation (Automata-based)
1121// ═══════════════════════════════════════════════════════════════════════════════
1122
1123/// Result of content model validation.
1124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1125pub enum ContentModelResult {
1126    /// Content is valid.
1127    Valid,
1128    /// Content is invalid.
1129    Invalid,
1130    /// Content model is indeterminate (mixed content with PCDATA).
1131    Indeterminate,
1132}
1133
1134/// Validate content (a list of element names) against a content model,
1135/// taking occurrence indicators into account.
1136///
1137/// # UPSTREAM-PARITY
1138///
1139/// ```c
1140/// int xmlValidContentModel(xmlElementContentPtr model, ...)
1141/// ```
1142///
1143/// This implements a simple recursive descent validator for content models.
1144/// For simple content models (EMPTY, ANY, PCDATA), the check is direct.
1145/// For sequence/choice models, it recursively validates.
1146///
1147/// Returns `ContentModelResult::Valid` if the content matches the model,
1148/// `ContentModelResult::Invalid` otherwise.
1149///
1150/// # SAFETY
1151///
1152/// - `model` must be a valid pointer to an _xmlElementContent, or NULL.
1153/// - `names` must be a slice of element names (null-terminated xmlChar strings).
1154pub unsafe fn valid_content_model(
1155    model: *mut _xmlElementContent,
1156    names: &[*const xmlChar],
1157) -> ContentModelResult {
1158    if model.is_null() {
1159        return ContentModelResult::Invalid;
1160    }
1161
1162    unsafe {
1163        let m = &*model;
1164
1165        // Handle occurrence indicators at this level first
1166        match m.ocur as u32 {
1167            o if o == XML_ELEMENT_CONTENT_OPT as u32 => {
1168                // Optional: zero or one occurrence
1169                if names.is_empty() {
1170                    return ContentModelResult::Valid;
1171                }
1172                return valid_content_model_inner(model, names);
1173            }
1174            o if o == XML_ELEMENT_CONTENT_MULT as u32 => {
1175                // Zero or more
1176                if names.is_empty() {
1177                    return ContentModelResult::Valid;
1178                }
1179                return valid_content_model_zero_or_more(model, names);
1180            }
1181            o if o == XML_ELEMENT_CONTENT_PLUS as u32 => {
1182                // One or more
1183                if names.is_empty() {
1184                    return ContentModelResult::Invalid;
1185                }
1186                return valid_content_model_one_or_more(model, names);
1187            }
1188            _ => {}
1189        }
1190
1191        valid_content_model_inner(model, names)
1192    }
1193}
1194
1195/// Validate content against a content model without considering occurrence.
1196unsafe fn valid_content_model_inner(
1197    model: *mut _xmlElementContent,
1198    names: &[*const xmlChar],
1199) -> ContentModelResult {
1200    unsafe {
1201        let m = &*model;
1202
1203        match m.type_ as u32 {
1204            t if t == XML_ELEMENT_CONTENT_PCDATA as u32 => {
1205                // PCDATA: content must be empty (just text)
1206                if names.is_empty() {
1207                    ContentModelResult::Valid
1208                } else {
1209                    ContentModelResult::Invalid
1210                }
1211            }
1212            t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
1213                // Single element: must match exactly one element
1214                if names.len() != 1 {
1215                    return ContentModelResult::Invalid;
1216                }
1217                if names[0].is_null() {
1218                    return ContentModelResult::Invalid;
1219                }
1220                // Compare with model name
1221                if string::xml_strcmp(names[0], m.name) != 0 {
1222                    return ContentModelResult::Invalid;
1223                }
1224                ContentModelResult::Valid
1225            }
1226            t if t == XML_ELEMENT_CONTENT_SEQ as u32 => {
1227                // Sequence: validate children in order
1228                valid_content_model_seq(m, names)
1229            }
1230            t if t == XML_ELEMENT_CONTENT_OR as u32 => {
1231                // Choice: one of the alternatives must match all names
1232                valid_content_model_or(m, names)
1233            }
1234            _ => ContentModelResult::Invalid,
1235        }
1236    }
1237}
1238
1239/// Validate content for zero-or-more occurrence.
1240unsafe fn valid_content_model_zero_or_more(
1241    model: *mut _xmlElementContent,
1242    names: &[*const xmlChar],
1243) -> ContentModelResult {
1244    // Zero or more: try each possible split
1245    let mut i = 0;
1246    while i <= names.len() {
1247        let consumed = &names[..i];
1248        let remaining = &names[i..];
1249
1250        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1251        if consumed_valid == ContentModelResult::Valid {
1252            if remaining.is_empty() {
1253                return ContentModelResult::Valid;
1254            }
1255            // Try to match remaining with same model
1256            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1257            if remaining_valid == ContentModelResult::Valid {
1258                return ContentModelResult::Valid;
1259            }
1260        }
1261
1262        i += 1;
1263    }
1264    ContentModelResult::Invalid
1265}
1266
1267/// Validate content for one-or-more occurrence.
1268unsafe fn valid_content_model_one_or_more(
1269    model: *mut _xmlElementContent,
1270    names: &[*const xmlChar],
1271) -> ContentModelResult {
1272    // One or more: must match at least once
1273    let mut i = 1;
1274    while i <= names.len() {
1275        let consumed = &names[..i];
1276        let remaining = &names[i..];
1277
1278        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1279        if consumed_valid == ContentModelResult::Valid {
1280            if remaining.is_empty() {
1281                return ContentModelResult::Valid;
1282            }
1283            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1284            if remaining_valid == ContentModelResult::Valid {
1285                return ContentModelResult::Valid;
1286            }
1287        }
1288
1289        i += 1;
1290    }
1291    ContentModelResult::Invalid
1292}
1293
1294/// Validate content against a sequence content model.
1295unsafe fn valid_content_model_seq(
1296    model: &_xmlElementContent,
1297    names: &[*const xmlChar],
1298) -> ContentModelResult {
1299    // For a sequence, we need to split the names between c1 and c2
1300    // This is a simplified validation - full automata-based validation
1301    // would be more complex.
1302
1303    let c1 = model.c1;
1304    let c2 = model.c2;
1305
1306    if c1.is_null() && c2.is_null() {
1307        return ContentModelResult::Valid;
1308    }
1309
1310    if c1.is_null() {
1311        return unsafe { valid_content_model(c2, names) };
1312    }
1313
1314    if c2.is_null() {
1315        return unsafe { valid_content_model(c1, names) };
1316    }
1317
1318    // Try to split the names at each possible position
1319    // This implements a simple backtracking validator
1320    for split in 0..=names.len() {
1321        let left = &names[..split];
1322        let right = &names[split..];
1323
1324        let left_valid = unsafe { valid_content_model(c1, left) };
1325        if left_valid != ContentModelResult::Valid {
1326            continue;
1327        }
1328
1329        let right_valid = unsafe { valid_content_model(c2, right) };
1330        if right_valid == ContentModelResult::Valid {
1331            return ContentModelResult::Valid;
1332        }
1333    }
1334
1335    ContentModelResult::Invalid
1336}
1337
1338/// Validate content against a choice content model.
1339unsafe fn valid_content_model_or(
1340    model: &_xmlElementContent,
1341    names: &[*const xmlChar],
1342) -> ContentModelResult {
1343    let c1 = model.c1;
1344    let c2 = model.c2;
1345
1346    if c1.is_null() && c2.is_null() {
1347        return ContentModelResult::Invalid;
1348    }
1349
1350    if !c1.is_null() {
1351        let result = unsafe { valid_content_model(c1, names) };
1352        if result == ContentModelResult::Valid {
1353            return ContentModelResult::Valid;
1354        }
1355    }
1356
1357    if !c2.is_null() {
1358        let result = unsafe { valid_content_model(c2, names) };
1359        if result == ContentModelResult::Valid {
1360            return ContentModelResult::Valid;
1361        }
1362    }
1363
1364    ContentModelResult::Invalid
1365}
1366
1367// ═══════════════════════════════════════════════════════════════════════════════
1368// Tests
1369// ═══════════════════════════════════════════════════════════════════════════════
1370
1371#[cfg(test)]
1372mod tests {
1373    use super::*;
1374    use crate::abi::allocator::xmlFreeImpl;
1375    use crate::abi::structs::*;
1376    use core::ffi::c_void;
1377    use core::ptr;
1378
1379    // ── Helpers ──────────────────────────────────────────────────────────
1380
1381    unsafe fn c_str(s: &[u8]) -> *const xmlChar {
1382        // Create a null-terminated xmlChar string
1383        let len = s.len();
1384        let buf = allocator::xmlMallocImpl(len + 1) as *mut xmlChar;
1385        assert!(!buf.is_null());
1386        ptr::copy_nonoverlapping(s.as_ptr(), buf, len);
1387        *buf.add(len) = 0;
1388        buf as *const xmlChar
1389    }
1390
1391    unsafe fn make_doc_and_dtd() -> (*mut _xmlDoc, *mut _xmlDtd) {
1392        let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1393        assert!(!doc.is_null());
1394        (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1395        (*doc).doc = doc;
1396        let dtd = create_int_subset(doc, c_str(b"root"), ptr::null(), ptr::null());
1397        assert!(!dtd.is_null());
1398        (doc, dtd)
1399    }
1400
1401    // ── DTD Access Tests ────────────────────────────────────────────────
1402
1403    #[test]
1404    fn test_get_int_subset_null() {
1405        unsafe {
1406            assert!(get_int_subset(ptr::null()).is_null());
1407        }
1408    }
1409
1410    #[test]
1411    fn test_create_int_subset() {
1412        unsafe {
1413            let (doc, dtd) = make_doc_and_dtd();
1414            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1415            assert!(!(*dtd).name.is_null());
1416            assert_eq!((*doc).intSubset, dtd);
1417
1418            // Cleanup
1419            free_dtd(dtd);
1420            allocator::xmlFreeImpl(doc as *mut c_void);
1421        }
1422    }
1423
1424    #[test]
1425    fn test_create_int_subset_null_doc() {
1426        unsafe {
1427            let dtd = create_int_subset(ptr::null_mut(), c_str(b"root"), ptr::null(), ptr::null());
1428            assert!(dtd.is_null());
1429        }
1430    }
1431
1432    #[test]
1433    fn test_new_dtd() {
1434        unsafe {
1435            let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1436            assert!(!doc.is_null());
1437            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1438            (*doc).doc = doc;
1439
1440            let dtd = new_dtd(doc, c_str(b"test"), c_str(b"-//TEST//"), c_str(b"test.dtd"));
1441            assert!(!dtd.is_null());
1442            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1443            assert_eq!((*doc).intSubset, dtd);
1444
1445            free_dtd(dtd);
1446            allocator::xmlFreeImpl(doc as *mut c_void);
1447        }
1448    }
1449
1450    #[test]
1451    fn test_new_dtd_no_doc() {
1452        unsafe {
1453            let dtd = new_dtd(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1454            assert!(!dtd.is_null());
1455            free_dtd(dtd);
1456        }
1457    }
1458
1459    // ── Notation Tests ──────────────────────────────────────────────────
1460
1461    #[test]
1462    fn test_add_get_notation() {
1463        unsafe {
1464            let (doc, dtd) = make_doc_and_dtd();
1465            let name = c_str(b"note");
1466            let pubid = c_str(b"-//TEST//NOTATION");
1467            let sysid = c_str(b"note.ent");
1468
1469            let n = add_notation_decl(dtd, name, pubid, sysid);
1470            assert!(!n.is_null());
1471            assert_eq!(string::xml_strcmp((*n).name, name), 0);
1472
1473            // Lookup
1474            let found = get_notation_decl(dtd, name);
1475            assert_eq!(found, n);
1476
1477            // Lookup non-existent
1478            let not_found = get_notation_decl(dtd, c_str(b"nonexistent"));
1479            assert!(not_found.is_null());
1480
1481            free_dtd(dtd);
1482            allocator::xmlFreeImpl(doc as *mut c_void);
1483        }
1484    }
1485
1486    #[test]
1487    fn test_add_notation_null_dtd() {
1488        unsafe {
1489            let n = add_notation_decl(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1490            assert!(n.is_null());
1491        }
1492    }
1493
1494    #[test]
1495    fn test_copy_notation() {
1496        unsafe {
1497            let (doc, dtd) = make_doc_and_dtd();
1498            let name = c_str(b"note1");
1499            let pubid = c_str(b"public");
1500            let sysid = c_str(b"system");
1501
1502            let n = add_notation_decl(dtd, name, pubid, sysid);
1503            assert!(!n.is_null());
1504
1505            let copy = copy_notation(n);
1506            assert!(!copy.is_null());
1507            assert_ne!(copy, n);
1508            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1509            assert_eq!(string::xml_strcmp((*copy).PublicID, pubid), 0);
1510            assert_eq!(string::xml_strcmp((*copy).SystemID, sysid), 0);
1511
1512            free_notation(copy);
1513            free_dtd(dtd);
1514            allocator::xmlFreeImpl(doc as *mut c_void);
1515        }
1516    }
1517
1518    #[test]
1519    fn test_copy_notation_null() {
1520        unsafe {
1521            assert!(copy_notation(ptr::null_mut()).is_null());
1522        }
1523    }
1524
1525    #[test]
1526    fn test_free_notation_null() {
1527        unsafe {
1528            free_notation(ptr::null_mut()); // Should not crash
1529        }
1530    }
1531
1532    // ── Content Model Tests ─────────────────────────────────────────────
1533
1534    #[test]
1535    fn test_create_free_content_model() {
1536        unsafe {
1537            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1538            assert!(!cm.is_null());
1539            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1540            assert_eq!((*cm).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1541
1542            free_content_model(cm);
1543        }
1544    }
1545
1546    #[test]
1547    fn test_create_content_model_pcdata() {
1548        unsafe {
1549            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
1550            assert!(!cm.is_null());
1551            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_PCDATA as c_int);
1552            free_content_model(cm);
1553        }
1554    }
1555
1556    #[test]
1557    fn test_copy_content_model() {
1558        unsafe {
1559            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1560            assert!(!cm.is_null());
1561
1562            let copy = copy_content_model(cm);
1563            assert!(!copy.is_null());
1564            assert_ne!(copy, cm);
1565            assert_eq!((*copy).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1566            assert_eq!((*copy).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1567            assert_eq!(string::xml_strcmp((*copy).name, (*cm).name), 0);
1568
1569            free_content_model(cm);
1570            free_content_model(copy);
1571        }
1572    }
1573
1574    #[test]
1575    fn test_copy_content_model_null() {
1576        unsafe {
1577            assert!(copy_content_model(ptr::null_mut()).is_null());
1578        }
1579    }
1580
1581    #[test]
1582    fn test_free_content_model_null() {
1583        unsafe {
1584            free_content_model(ptr::null_mut()); // Should not crash
1585        }
1586    }
1587
1588    #[test]
1589    fn test_create_sequence_content_model() {
1590        unsafe {
1591            let c1 = create_content_model(c_str(b"a"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1592            let c2 = create_content_model(c_str(b"b"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1593            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
1594            assert!(!seq.is_null());
1595            (*seq).c1 = c1;
1596            (*seq).c2 = c2;
1597            (*c1).parent = seq;
1598            (*c2).parent = seq;
1599
1600            free_content_model(seq);
1601        }
1602    }
1603
1604    // ── Element Declaration Tests ───────────────────────────────────────
1605
1606    #[test]
1607    fn test_add_get_element() {
1608        unsafe {
1609            let (doc, dtd) = make_doc_and_dtd();
1610            let name = c_str(b"myElement");
1611
1612            let elem =
1613                add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
1614            assert!(!elem.is_null());
1615            assert_eq!((*elem).etype, XML_ELEMENT_TYPE_EMPTY as c_int);
1616
1617            let found = get_element_decl(dtd, name);
1618            assert_eq!(found, elem);
1619
1620            let not_found = get_element_decl(dtd, c_str(b"nonexistent"));
1621            assert!(not_found.is_null());
1622
1623            free_dtd(dtd);
1624            allocator::xmlFreeImpl(doc as *mut c_void);
1625        }
1626    }
1627
1628    #[test]
1629    fn test_add_element_duplicate() {
1630        unsafe {
1631            let (doc, dtd) = make_doc_and_dtd();
1632            let name = c_str(b"dup");
1633
1634            let e1 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
1635            assert!(!e1.is_null());
1636
1637            let e2 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ANY as c_int, ptr::null_mut());
1638            assert_eq!(e1, e2); // Same pointer returned
1639            assert_eq!((*e2).type_, XML_ELEMENT_TYPE_EMPTY as c_int); // Still empty
1640
1641            free_dtd(dtd);
1642            allocator::xmlFreeImpl(doc as *mut c_void);
1643        }
1644    }
1645
1646    #[test]
1647    fn test_add_element_null_dtd() {
1648        unsafe {
1649            let elem = add_element_decl(
1650                ptr::null_mut(),
1651                c_str(b"test"),
1652                XML_ELEMENT_TYPE_EMPTY as c_int,
1653                ptr::null_mut(),
1654            );
1655            assert!(elem.is_null());
1656        }
1657    }
1658
1659    #[test]
1660    fn test_copy_element() {
1661        unsafe {
1662            let (doc, dtd) = make_doc_and_dtd();
1663            let name = c_str(b"source");
1664            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1665
1666            let elem = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ELEMENT as c_int, cm);
1667            assert!(!elem.is_null());
1668
1669            let copy = copy_element(elem);
1670            assert!(!copy.is_null());
1671            assert_ne!(copy, elem);
1672            assert_eq!((*copy).type_, XML_ELEMENT_TYPE_ELEMENT as c_int);
1673            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1674            assert!(!(*copy).content.is_null());
1675            assert_ne!((*copy).content, cm);
1676
1677            free_element(copy);
1678            free_dtd(dtd);
1679            allocator::xmlFreeImpl(doc as *mut c_void);
1680        }
1681    }
1682
1683    #[test]
1684    fn test_free_element_null() {
1685        unsafe {
1686            free_element(ptr::null_mut()); // Should not crash
1687        }
1688    }
1689
1690    // ── Attribute Declaration Tests ─────────────────────────────────────
1691
1692    #[test]
1693    fn test_add_get_attribute() {
1694        unsafe {
1695            let (doc, dtd) = make_doc_and_dtd();
1696            let elem_name = c_str(b"elem");
1697            let attr_name = c_str(b"attr1");
1698
1699            let elem = add_element_decl(
1700                dtd,
1701                elem_name,
1702                XML_ELEMENT_TYPE_EMPTY as c_int,
1703                ptr::null_mut(),
1704            );
1705            assert!(!elem.is_null());
1706
1707            let attr = add_attribute_decl(
1708                dtd,
1709                elem,
1710                attr_name,
1711                XML_ATTRIBUTE_CDATA as c_int,
1712                XML_ATTRIBUTE_IMPLIED as c_int,
1713                ptr::null(),
1714                ptr::null_mut(),
1715            );
1716            assert!(!attr.is_null());
1717            assert_eq!((*attr).atype, XML_ATTRIBUTE_CDATA as c_int);
1718            assert_eq!((*attr).def, XML_ATTRIBUTE_IMPLIED as c_int);
1719
1720            // Lookup by element + attribute name
1721            let found = get_attribute_decl(dtd, elem, attr_name, 0);
1722            assert_eq!(found, attr);
1723
1724            // Lookup non-existent
1725            let not_found = get_attribute_decl(dtd, elem, c_str(b"nonexistent"), 0);
1726            assert!(not_found.is_null());
1727
1728            free_dtd(dtd);
1729            allocator::xmlFreeImpl(doc as *mut c_void);
1730        }
1731    }
1732
1733    #[test]
1734    fn test_add_attribute_with_default() {
1735        unsafe {
1736            let (doc, dtd) = make_doc_and_dtd();
1737            let elem_name = c_str(b"elem");
1738            let attr_name = c_str(b"color");
1739            let default_val = c_str(b"red");
1740
1741            let elem = add_element_decl(
1742                dtd,
1743                elem_name,
1744                XML_ELEMENT_TYPE_EMPTY as c_int,
1745                ptr::null_mut(),
1746            );
1747
1748            let attr = add_attribute_decl(
1749                dtd,
1750                elem,
1751                attr_name,
1752                XML_ATTRIBUTE_CDATA as c_int,
1753                XML_ATTRIBUTE_FIXED as c_int,
1754                default_val,
1755                ptr::null_mut(),
1756            );
1757            assert!(!attr.is_null());
1758            assert_eq!((*attr).def, XML_ATTRIBUTE_FIXED as c_int);
1759            assert_eq!(string::xml_strcmp((*attr).defaultValue, default_val), 0);
1760
1761            free_dtd(dtd);
1762            allocator::xmlFreeImpl(doc as *mut c_void);
1763        }
1764    }
1765
1766    #[test]
1767    fn test_add_attribute_enumeration() {
1768        unsafe {
1769            let (doc, dtd) = make_doc_and_dtd();
1770            let elem_name = c_str(b"elem");
1771            let attr_name = c_str(b"size");
1772
1773            // Build enumeration: small, medium, large
1774            let v3 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1775                as *mut _xmlEnumeration;
1776            (*v3).name = string::xml_strdup(c_str(b"large"));
1777            let v2 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1778                as *mut _xmlEnumeration;
1779            (*v2).name = string::xml_strdup(c_str(b"medium"));
1780            (*v2).next = v3;
1781            let v1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1782                as *mut _xmlEnumeration;
1783            (*v1).name = string::xml_strdup(c_str(b"small"));
1784            (*v1).next = v2;
1785
1786            let elem = add_element_decl(
1787                dtd,
1788                elem_name,
1789                XML_ELEMENT_TYPE_EMPTY as c_int,
1790                ptr::null_mut(),
1791            );
1792            let attr = add_attribute_decl(
1793                dtd,
1794                elem,
1795                attr_name,
1796                XML_ATTRIBUTE_ENUMERATION as c_int,
1797                XML_ATTRIBUTE_REQUIRED as c_int,
1798                ptr::null(),
1799                v1,
1800            );
1801            assert!(!attr.is_null());
1802            assert_eq!((*attr).atype, XML_ATTRIBUTE_ENUMERATION as c_int);
1803
1804            free_dtd(dtd);
1805            allocator::xmlFreeImpl(doc as *mut c_void);
1806        }
1807    }
1808
1809    #[test]
1810    fn test_add_attribute_null_dtd() {
1811        unsafe {
1812            let attr = add_attribute_decl(
1813                ptr::null_mut(),
1814                ptr::null_mut(),
1815                c_str(b"test"),
1816                XML_ATTRIBUTE_CDATA as c_int,
1817                XML_ATTRIBUTE_IMPLIED as c_int,
1818                ptr::null(),
1819                ptr::null_mut(),
1820            );
1821            assert!(attr.is_null());
1822        }
1823    }
1824
1825    #[test]
1826    fn test_copy_attribute() {
1827        unsafe {
1828            let (doc, dtd) = make_doc_and_dtd();
1829            let elem_name = c_str(b"elem");
1830            let attr_name = c_str(b"id");
1831            let default_val = c_str(b"default");
1832
1833            let elem = add_element_decl(
1834                dtd,
1835                elem_name,
1836                XML_ELEMENT_TYPE_EMPTY as c_int,
1837                ptr::null_mut(),
1838            );
1839            let attr = add_attribute_decl(
1840                dtd,
1841                elem,
1842                attr_name,
1843                XML_ATTRIBUTE_ID as c_int,
1844                XML_ATTRIBUTE_IMPLIED as c_int,
1845                default_val,
1846                ptr::null_mut(),
1847            );
1848            assert!(!attr.is_null());
1849
1850            let copy = copy_attribute_decl(attr);
1851            assert!(!copy.is_null());
1852            assert_ne!(copy, attr);
1853            assert_eq!((*copy).atype, XML_ATTRIBUTE_ID as c_int);
1854            assert_eq!(string::xml_strcmp((*copy).name, attr_name), 0);
1855
1856            free_attribute(copy);
1857            free_dtd(dtd);
1858            allocator::xmlFreeImpl(doc as *mut c_void);
1859        }
1860    }
1861
1862    #[test]
1863    fn test_free_attribute_null() {
1864        unsafe {
1865            free_attribute(ptr::null_mut()); // Should not crash
1866        }
1867    }
1868
1869    // ── Content Model Validation Tests ──────────────────────────────────
1870
1871    #[test]
1872    fn test_valid_content_model_null() {
1873        unsafe {
1874            assert_eq!(
1875                valid_content_model(ptr::null_mut(), &[]),
1876                ContentModelResult::Invalid
1877            );
1878        }
1879    }
1880
1881    #[test]
1882    fn test_valid_content_model_pcdata() {
1883        unsafe {
1884            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
1885            assert!(!cm.is_null());
1886
1887            // Empty content is valid for PCDATA
1888            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Valid);
1889
1890            // Non-empty content is invalid for PCDATA
1891            let name = c_str(b"child");
1892            assert_eq!(
1893                valid_content_model(cm, &[name]),
1894                ContentModelResult::Invalid
1895            );
1896
1897            free_content_model(cm);
1898        }
1899    }
1900
1901    #[test]
1902    fn test_valid_content_model_element() {
1903        unsafe {
1904            let child_name = c_str(b"child");
1905            let cm = create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1906            assert!(!cm.is_null());
1907
1908            // Correct element
1909            assert_eq!(
1910                valid_content_model(cm, &[child_name]),
1911                ContentModelResult::Valid
1912            );
1913
1914            // Wrong element
1915            let other = c_str(b"other");
1916            assert_eq!(
1917                valid_content_model(cm, &[other]),
1918                ContentModelResult::Invalid
1919            );
1920
1921            // Too many elements
1922            assert_eq!(
1923                valid_content_model(cm, &[child_name, child_name]),
1924                ContentModelResult::Invalid
1925            );
1926
1927            // Empty
1928            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Invalid);
1929
1930            free_content_model(cm);
1931        }
1932    }
1933
1934    #[test]
1935    fn test_valid_content_model_seq() {
1936        unsafe {
1937            let a_name = c_str(b"a");
1938            let b_name = c_str(b"b");
1939
1940            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1941            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1942            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
1943            (*seq).c1 = c1;
1944            (*seq).c2 = c2;
1945            (*c1).parent = seq;
1946            (*c2).parent = seq;
1947
1948            // Correct sequence
1949            assert_eq!(
1950                valid_content_model(seq, &[a_name, b_name]),
1951                ContentModelResult::Valid
1952            );
1953
1954            // Wrong order
1955            assert_eq!(
1956                valid_content_model(seq, &[b_name, a_name]),
1957                ContentModelResult::Invalid
1958            );
1959
1960            // Missing element
1961            assert_eq!(
1962                valid_content_model(seq, &[a_name]),
1963                ContentModelResult::Invalid
1964            );
1965
1966            free_content_model(seq);
1967        }
1968    }
1969
1970    #[test]
1971    fn test_valid_content_model_or() {
1972        unsafe {
1973            let a_name = c_str(b"a");
1974            let b_name = c_str(b"b");
1975
1976            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1977            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1978            let choice = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_OR as c_int);
1979            (*choice).c1 = c1;
1980            (*choice).c2 = c2;
1981            (*c1).parent = choice;
1982            (*c2).parent = choice;
1983
1984            // First alternative
1985            assert_eq!(
1986                valid_content_model(choice, &[a_name]),
1987                ContentModelResult::Valid
1988            );
1989
1990            // Second alternative
1991            assert_eq!(
1992                valid_content_model(choice, &[b_name]),
1993                ContentModelResult::Valid
1994            );
1995
1996            // Neither
1997            let other = c_str(b"other");
1998            assert_eq!(
1999                valid_content_model(choice, &[other]),
2000                ContentModelResult::Invalid
2001            );
2002
2003            free_content_model(choice);
2004        }
2005    }
2006
2007    #[test]
2008    fn test_valid_content_model_optional() {
2009        unsafe {
2010            let a_name = c_str(b"a");
2011
2012            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2013            (*c1).ocur = XML_ELEMENT_CONTENT_OPT as c_int;
2014
2015            // Empty is valid for optional
2016            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2017
2018            // One is valid
2019            assert_eq!(
2020                valid_content_model(c1, &[a_name]),
2021                ContentModelResult::Valid
2022            );
2023
2024            free_content_model(c1);
2025        }
2026    }
2027
2028    #[test]
2029    fn test_valid_content_model_zero_or_more() {
2030        unsafe {
2031            let a_name = c_str(b"a");
2032
2033            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2034            (*c1).ocur = XML_ELEMENT_CONTENT_MULT as c_int;
2035
2036            // Empty is valid
2037            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2038
2039            // One is valid
2040            assert_eq!(
2041                valid_content_model(c1, &[a_name]),
2042                ContentModelResult::Valid
2043            );
2044
2045            // Multiple is valid
2046            assert_eq!(
2047                valid_content_model(c1, &[a_name, a_name, a_name]),
2048                ContentModelResult::Valid
2049            );
2050
2051            free_content_model(c1);
2052        }
2053    }
2054
2055    #[test]
2056    fn test_valid_content_model_one_or_more() {
2057        unsafe {
2058            let a_name = c_str(b"a");
2059
2060            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2061            (*c1).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
2062
2063            // Empty is invalid
2064            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Invalid);
2065
2066            // One is valid
2067            assert_eq!(
2068                valid_content_model(c1, &[a_name]),
2069                ContentModelResult::Valid
2070            );
2071
2072            // Multiple is valid
2073            assert_eq!(
2074                valid_content_model(c1, &[a_name, a_name]),
2075                ContentModelResult::Valid
2076            );
2077
2078            free_content_model(c1);
2079        }
2080    }
2081}