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::xmlFree(d.name as *mut c_void);
267        }
268        if !d.ExternalID.is_null() {
269            allocator::xmlFree(d.ExternalID as *mut c_void);
270        }
271        if !d.SystemID.is_null() {
272            allocator::xmlFree(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::xmlFree(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::xmlFree(n.name as *mut c_void);
456        }
457        if !n.PublicID.is_null() {
458            allocator::xmlFree(n.PublicID as *mut c_void);
459        }
460        if !n.SystemID.is_null() {
461            allocator::xmlFree(n.SystemID as *mut c_void);
462        }
463        allocator::xmlFree(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::xmlFree(c.name as *mut c_void);
538        }
539        if !c.prefix.is_null() {
540            allocator::xmlFree(c.prefix as *mut c_void);
541        }
542
543        allocator::xmlFree(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).content = content; // Takes ownership of the content model
646        (*elem).attributes = ptr::null_mut();
647        (*elem).prefix = ptr::null_mut();
648        (*elem).next = ptr::null_mut();
649        (*elem)._private = 0;
650
651        // Add to hash table
652        let ret = hash::hash_add_entry(
653            d.elements as *mut hash::HashTable,
654            name,
655            elem as *mut c_void,
656        );
657        if ret != 0 {
658            // Failed to add
659            free_element(elem);
660            return ptr::null_mut();
661        }
662
663        elem
664    }
665}
666
667/// Look up an element declaration by name.
668///
669/// # UPSTREAM-PARITY
670///
671/// ```c
672/// xmlElementPtr xmlGetElementDecl(xmlDtdPtr dtd, const xmlChar *name);
673/// ```
674///
675/// # SAFETY
676///
677/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
678/// - `name` must be a valid null-terminated string.
679pub unsafe fn get_element_decl(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlElement {
680    if dtd.is_null() || name.is_null() {
681        return ptr::null_mut();
682    }
683
684    unsafe {
685        let d = &*dtd;
686        let payload = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
687        payload as *mut _xmlElement
688    }
689}
690
691/// Deep copy an element declaration.
692///
693/// # UPSTREAM-PARITY
694///
695/// ```c
696/// xmlElementPtr xmlCopyElement(xmlElementPtr elem);
697/// ```
698///
699/// # SAFETY
700///
701/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
702pub unsafe fn copy_element(elem: *mut _xmlElement) -> *mut _xmlElement {
703    if elem.is_null() {
704        return ptr::null_mut();
705    }
706
707    unsafe {
708        let e = &*elem;
709
710        // SAFETY: Allocate zero-initialized memory for the copy.
711        let copy = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
712        if copy.is_null() {
713            return ptr::null_mut();
714        }
715
716        (*copy).name = string::xml_strdup(e.name);
717        (*copy).type_ = e.type_;
718        (*copy).content = copy_content_model(e.content);
719        (*copy).prefix = string::xml_strdup(e.prefix);
720        (*copy)._private = e._private;
721
722        // Copy attribute declarations (linked list)
723        if !e.attributes.is_null() {
724            // UPSTREAM-PARITY: We copy the attribute linked list by
725            // iterating and copying each attribute.
726            let mut src_attr = e.attributes;
727            let mut prev_copy: *mut _xmlAttribute = ptr::null_mut();
728            let mut first_copy: *mut _xmlAttribute = ptr::null_mut();
729
730            while !src_attr.is_null() {
731                let attr_copy = copy_attribute_decl(src_attr);
732                if attr_copy.is_null() {
733                    // Free what we've copied so far
734                    let mut to_free = first_copy;
735                    while !to_free.is_null() {
736                        let next = (*to_free).nexth;
737                        free_attribute(to_free);
738                        to_free = next;
739                    }
740                    allocator::xmlFree(copy as *mut c_void);
741                    return ptr::null_mut();
742                }
743
744                if prev_copy.is_null() {
745                    first_copy = attr_copy;
746                } else {
747                    (*prev_copy).nexth = attr_copy;
748                }
749                prev_copy = attr_copy;
750                src_attr = (*src_attr).nexth;
751            }
752
753            (*copy).attributes = first_copy;
754        }
755
756        copy
757    }
758}
759
760/// Free an element declaration and its content model.
761///
762/// # UPSTREAM-PARITY
763///
764/// ```c
765/// void xmlFreeElement(xmlElementPtr elem);
766/// ```
767///
768/// Frees the element declaration and its content model, but NOT the
769/// attribute declarations (which are owned by the DTD's attribute hash).
770///
771/// # SAFETY
772///
773/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
774pub unsafe fn free_element(elem: *mut _xmlElement) {
775    if elem.is_null() {
776        return;
777    }
778
779    unsafe {
780        // Free name
781        if !(*elem).name.is_null() {
782            allocator::xmlFree((*elem).name as *mut c_void);
783        }
784
785        // Free prefix
786        if !(*elem).prefix.is_null() {
787            allocator::xmlFree((*elem).prefix as *mut c_void);
788        }
789
790        // Free content model
791        if !(*elem).content.is_null() {
792            free_content_model((*elem).content);
793        }
794
795        // UPSTREAM-PARITY: The attributes linked list on the element
796        // declaration is NOT owned by the element. The DTD's attribute
797        // hash table is the sole owner. When the DTD is freed, the
798        // hash table's deallocator frees all attributes.
799        // Therefore, we do NOT free the attributes list here.
800        (*elem).attributes = ptr::null_mut();
801
802        allocator::xmlFree(elem as *mut c_void);
803    }
804}
805
806// ═══════════════════════════════════════════════════════════════════════════════
807// Attribute Declarations
808// ═══════════════════════════════════════════════════════════════════════════════
809
810/// Free an enumeration value tree.
811///
812/// # SAFETY
813///
814/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
815unsafe fn free_enumeration(tree: *mut _xmlEnumeration) {
816    if tree.is_null() {
817        return;
818    }
819
820    unsafe {
821        let mut cur = tree;
822        while !cur.is_null() {
823            let next = (*cur).next;
824            if !(*cur).name.is_null() {
825                allocator::xmlFree((*cur).name as *mut c_void);
826            }
827            allocator::xmlFree(cur as *mut c_void);
828            cur = next;
829        }
830    }
831}
832
833/// Deep copy an enumeration value tree.
834///
835/// # SAFETY
836///
837/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
838unsafe fn copy_enumeration(tree: *mut _xmlEnumeration) -> *mut _xmlEnumeration {
839    if tree.is_null() {
840        return ptr::null_mut();
841    }
842
843    unsafe {
844        let mut src = tree;
845        let mut first_copy: *mut _xmlEnumeration = ptr::null_mut();
846        let mut prev_copy: *mut _xmlEnumeration = ptr::null_mut();
847
848        while !src.is_null() {
849            let copy = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
850                as *mut _xmlEnumeration;
851            if copy.is_null() {
852                // Free what we've allocated so far
853                let mut to_free = first_copy;
854                while !to_free.is_null() {
855                    let next = (*to_free).next;
856                    if !(*to_free).name.is_null() {
857                        allocator::xmlFree((*to_free).name as *mut c_void);
858                    }
859                    allocator::xmlFree(to_free as *mut c_void);
860                    to_free = next;
861                }
862                return ptr::null_mut();
863            }
864
865            (*copy).name = string::xml_strdup((*src).name);
866            (*copy).next = ptr::null_mut();
867
868            if prev_copy.is_null() {
869                first_copy = copy;
870            } else {
871                (*prev_copy).next = copy;
872            }
873            prev_copy = copy;
874            src = (*src).next;
875        }
876
877        first_copy
878    }
879}
880
881/// Add an attribute declaration to a DTD.
882///
883/// # UPSTREAM-PARITY
884///
885/// ```c
886/// xmlAttributePtr xmlAddAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
887///                                     const xmlChar *name, int type, int def,
888///                                     const xmlChar *defaultValue,
889///                                     xmlEnumerationPtr tree);
890/// ```
891///
892/// Adds an attribute declaration to both the DTD's attribute hash table
893/// (keyed by element name + attribute name) and the element's linked list.
894/// If an attribute with the same name already exists for this element,
895/// the existing declaration is returned.
896///
897/// # SAFETY
898///
899/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
900/// - `name` must be a valid null-terminated string.
901/// - `elem`, `defaultValue`, `tree` may be NULL.
902pub unsafe fn add_attribute_decl(
903    dtd: *mut _xmlDtd,
904    elem: *mut _xmlElement,
905    name: *const xmlChar,
906    type_: c_int,
907    def: c_int,
908    defaultValue: *const xmlChar,
909    tree: *mut _xmlEnumeration,
910) -> *mut _xmlAttribute {
911    if dtd.is_null() || name.is_null() {
912        return ptr::null_mut();
913    }
914
915    unsafe {
916        let d = &*dtd;
917        let elem_name = if elem.is_null() {
918            ptr::null()
919        } else {
920            (*elem).name
921        };
922
923        // Check if attribute already exists for this element
924        let existing = hash::hash_lookup2(d.attributes as *mut hash::HashTable, elem_name, name);
925        if !existing.is_null() {
926            return existing as *mut _xmlAttribute;
927        }
928
929        // SAFETY: Allocate zero-initialized memory for the attribute.
930        let attr =
931            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
932        if attr.is_null() {
933            return ptr::null_mut();
934        }
935
936        (*attr).type_ = XML_ATTRIBUTE_DECL as c_int;
937        (*attr).name = string::xml_strdup(name);
938        (*attr).parent = dtd;
939        (*attr).doc = d.doc;
940        (*attr).nexth = ptr::null_mut();
941        (*attr).atype = type_;
942        (*attr).def = def;
943        (*attr).defaultValue = string::xml_strdup(defaultValue);
944        (*attr).tree = tree; // Takes ownership of the enumeration tree
945        (*attr).prefix = ptr::null_mut();
946        (*attr).elem = string::xml_strdup(elem_name);
947
948        // Add to DTD's attribute hash table (keyed by element name + attribute name)
949        let ret = hash::hash_add_entry2(
950            d.attributes as *mut hash::HashTable,
951            elem_name,
952            name,
953            attr as *mut c_void,
954        );
955        if ret != 0 {
956            // Failed to add
957            if !(*attr).defaultValue.is_null() {
958                allocator::xmlFree((*attr).defaultValue as *mut c_void);
959            }
960            if !(*attr).name.is_null() {
961                allocator::xmlFree((*attr).name as *mut c_void);
962            }
963            if !(*attr).elem.is_null() {
964                allocator::xmlFree((*attr).elem as *mut c_void);
965            }
966            allocator::xmlFree(attr as *mut c_void);
967            // Don't free tree - caller still owns it on failure
968            return ptr::null_mut();
969        }
970
971        // Add to element's linked list
972        if !elem.is_null() {
973            (*attr).nexth = (*elem).attributes;
974            (*elem).attributes = attr;
975        }
976
977        attr
978    }
979}
980
981/// Look up an attribute declaration by element name and attribute name.
982///
983/// # UPSTREAM-PARITY
984///
985/// ```c
986/// xmlAttributePtr xmlGetAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
987///                                     const xmlChar *name, int namePrefix);
988/// ```
989///
990/// The `namePrefix` parameter is ignored in this implementation
991/// (it's a legacy parameter in libxml2).
992///
993/// # SAFETY
994///
995/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
996/// - `name` must be a valid null-terminated string.
997/// - `elem` may be NULL.
998pub unsafe fn get_attribute_decl(
999    dtd: *mut _xmlDtd,
1000    elem: *mut _xmlElement,
1001    name: *const xmlChar,
1002    _namePrefix: c_int,
1003) -> *mut _xmlAttribute {
1004    if dtd.is_null() || name.is_null() {
1005        return ptr::null_mut();
1006    }
1007
1008    unsafe {
1009        let d = &*dtd;
1010        let elem_name = if elem.is_null() {
1011            ptr::null()
1012        } else {
1013            (*elem).name
1014        };
1015
1016        let payload = hash::hash_lookup2(d.attributes as *mut hash::HashTable, elem_name, name);
1017        payload as *mut _xmlAttribute
1018    }
1019}
1020
1021/// Deep copy an attribute declaration.
1022///
1023/// # UPSTREAM-PARITY
1024///
1025/// ```c
1026/// xmlAttributePtr xmlCopyAttribute(xmlAttributePtr attr);
1027/// ```
1028///
1029/// # SAFETY
1030///
1031/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1032pub unsafe fn copy_attribute_decl(attr: *mut _xmlAttribute) -> *mut _xmlAttribute {
1033    if attr.is_null() {
1034        return ptr::null_mut();
1035    }
1036
1037    unsafe {
1038        let a = &*attr;
1039
1040        // SAFETY: Allocate zero-initialized memory for the copy.
1041        let copy =
1042            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1043        if copy.is_null() {
1044            return ptr::null_mut();
1045        }
1046
1047        (*copy).type_ = a.type_;
1048        (*copy).name = string::xml_strdup(a.name);
1049        (*copy).parent = a.parent;
1050        (*copy).doc = a.doc;
1051        (*copy).nexth = ptr::null_mut();
1052        (*copy).atype = a.atype;
1053        (*copy).def = a.def;
1054        (*copy).defaultValue = string::xml_strdup(a.defaultValue);
1055        (*copy).tree = copy_enumeration(a.tree);
1056        (*copy).prefix = string::xml_strdup(a.prefix);
1057        (*copy).elem = string::xml_strdup(a.elem);
1058
1059        copy
1060    }
1061}
1062
1063/// Free an attribute declaration.
1064///
1065/// # UPSTREAM-PARITY
1066///
1067/// ```c
1068/// void xmlFreeAttribute(xmlAttributePtr attr);
1069/// ```
1070///
1071/// # SAFETY
1072///
1073/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1074pub unsafe fn free_attribute(attr: *mut _xmlAttribute) {
1075    if attr.is_null() {
1076        return;
1077    }
1078
1079    unsafe {
1080        let a = &*attr;
1081
1082        if !a.name.is_null() {
1083            allocator::xmlFree(a.name as *mut c_void);
1084        }
1085        if !a.defaultValue.is_null() {
1086            allocator::xmlFree(a.defaultValue as *mut c_void);
1087        }
1088        if !a.prefix.is_null() {
1089            allocator::xmlFree(a.prefix as *mut c_void);
1090        }
1091        if !a.elem.is_null() {
1092            allocator::xmlFree(a.elem as *mut c_void);
1093        }
1094        if !a.tree.is_null() {
1095            free_enumeration(a.tree);
1096        }
1097
1098        allocator::xmlFree(attr as *mut c_void);
1099    }
1100}
1101
1102// ═══════════════════════════════════════════════════════════════════════════════
1103// Content Model Validation (Automata-based)
1104// ═══════════════════════════════════════════════════════════════════════════════
1105
1106/// Result of content model validation.
1107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1108pub enum ContentModelResult {
1109    /// Content is valid.
1110    Valid,
1111    /// Content is invalid.
1112    Invalid,
1113    /// Content model is indeterminate (mixed content with PCDATA).
1114    Indeterminate,
1115}
1116
1117/// Validate content (a list of element names) against a content model,
1118/// taking occurrence indicators into account.
1119///
1120/// # UPSTREAM-PARITY
1121///
1122/// ```c
1123/// int xmlValidContentModel(xmlElementContentPtr model, ...)
1124/// ```
1125///
1126/// This implements a simple recursive descent validator for content models.
1127/// For simple content models (EMPTY, ANY, PCDATA), the check is direct.
1128/// For sequence/choice models, it recursively validates.
1129///
1130/// Returns `ContentModelResult::Valid` if the content matches the model,
1131/// `ContentModelResult::Invalid` otherwise.
1132///
1133/// # SAFETY
1134///
1135/// - `model` must be a valid pointer to an _xmlElementContent, or NULL.
1136/// - `names` must be a slice of element names (null-terminated xmlChar strings).
1137pub unsafe fn valid_content_model(
1138    model: *mut _xmlElementContent,
1139    names: &[*const xmlChar],
1140) -> ContentModelResult {
1141    if model.is_null() {
1142        return ContentModelResult::Invalid;
1143    }
1144
1145    unsafe {
1146        let m = &*model;
1147
1148        // Handle occurrence indicators at this level first
1149        match m.ocur as u32 {
1150            o if o == XML_ELEMENT_CONTENT_OPT as u32 => {
1151                // Optional: zero or one occurrence
1152                if names.is_empty() {
1153                    return ContentModelResult::Valid;
1154                }
1155                return valid_content_model_inner(model, names);
1156            }
1157            o if o == XML_ELEMENT_CONTENT_MULT as u32 => {
1158                // Zero or more
1159                if names.is_empty() {
1160                    return ContentModelResult::Valid;
1161                }
1162                return valid_content_model_zero_or_more(model, names);
1163            }
1164            o if o == XML_ELEMENT_CONTENT_PLUS as u32 => {
1165                // One or more
1166                if names.is_empty() {
1167                    return ContentModelResult::Invalid;
1168                }
1169                return valid_content_model_one_or_more(model, names);
1170            }
1171            _ => {}
1172        }
1173
1174        valid_content_model_inner(model, names)
1175    }
1176}
1177
1178/// Validate content against a content model without considering occurrence.
1179unsafe fn valid_content_model_inner(
1180    model: *mut _xmlElementContent,
1181    names: &[*const xmlChar],
1182) -> ContentModelResult {
1183    unsafe {
1184        let m = &*model;
1185
1186        match m.type_ as u32 {
1187            t if t == XML_ELEMENT_CONTENT_PCDATA as u32 => {
1188                // PCDATA: content must be empty (just text)
1189                if names.is_empty() {
1190                    ContentModelResult::Valid
1191                } else {
1192                    ContentModelResult::Invalid
1193                }
1194            }
1195            t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
1196                // Single element: must match exactly one element
1197                if names.len() != 1 {
1198                    return ContentModelResult::Invalid;
1199                }
1200                if names[0].is_null() {
1201                    return ContentModelResult::Invalid;
1202                }
1203                // Compare with model name
1204                if string::xml_strcmp(names[0], m.name) != 0 {
1205                    return ContentModelResult::Invalid;
1206                }
1207                ContentModelResult::Valid
1208            }
1209            t if t == XML_ELEMENT_CONTENT_SEQ as u32 => {
1210                // Sequence: validate children in order
1211                valid_content_model_seq(m, names)
1212            }
1213            t if t == XML_ELEMENT_CONTENT_OR as u32 => {
1214                // Choice: one of the alternatives must match all names
1215                valid_content_model_or(m, names)
1216            }
1217            _ => ContentModelResult::Invalid,
1218        }
1219    }
1220}
1221
1222/// Validate content for zero-or-more occurrence.
1223unsafe fn valid_content_model_zero_or_more(
1224    model: *mut _xmlElementContent,
1225    names: &[*const xmlChar],
1226) -> ContentModelResult {
1227    // Zero or more: try each possible split
1228    let mut i = 0;
1229    while i <= names.len() {
1230        let consumed = &names[..i];
1231        let remaining = &names[i..];
1232
1233        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1234        if consumed_valid == ContentModelResult::Valid {
1235            if remaining.is_empty() {
1236                return ContentModelResult::Valid;
1237            }
1238            // Try to match remaining with same model
1239            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1240            if remaining_valid == ContentModelResult::Valid {
1241                return ContentModelResult::Valid;
1242            }
1243        }
1244
1245        i += 1;
1246    }
1247    ContentModelResult::Invalid
1248}
1249
1250/// Validate content for one-or-more occurrence.
1251unsafe fn valid_content_model_one_or_more(
1252    model: *mut _xmlElementContent,
1253    names: &[*const xmlChar],
1254) -> ContentModelResult {
1255    // One or more: must match at least once
1256    let mut i = 1;
1257    while i <= names.len() {
1258        let consumed = &names[..i];
1259        let remaining = &names[i..];
1260
1261        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1262        if consumed_valid == ContentModelResult::Valid {
1263            if remaining.is_empty() {
1264                return ContentModelResult::Valid;
1265            }
1266            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1267            if remaining_valid == ContentModelResult::Valid {
1268                return ContentModelResult::Valid;
1269            }
1270        }
1271
1272        i += 1;
1273    }
1274    ContentModelResult::Invalid
1275}
1276
1277/// Validate content against a sequence content model.
1278unsafe fn valid_content_model_seq(
1279    model: &_xmlElementContent,
1280    names: &[*const xmlChar],
1281) -> ContentModelResult {
1282    // For a sequence, we need to split the names between c1 and c2
1283    // This is a simplified validation - full automata-based validation
1284    // would be more complex.
1285
1286    let c1 = model.c1;
1287    let c2 = model.c2;
1288
1289    if c1.is_null() && c2.is_null() {
1290        return ContentModelResult::Valid;
1291    }
1292
1293    if c1.is_null() {
1294        return unsafe { valid_content_model(c2, names) };
1295    }
1296
1297    if c2.is_null() {
1298        return unsafe { valid_content_model(c1, names) };
1299    }
1300
1301    // Try to split the names at each possible position
1302    // This implements a simple backtracking validator
1303    for split in 0..=names.len() {
1304        let left = &names[..split];
1305        let right = &names[split..];
1306
1307        let left_valid = unsafe { valid_content_model(c1, left) };
1308        if left_valid != ContentModelResult::Valid {
1309            continue;
1310        }
1311
1312        let right_valid = unsafe { valid_content_model(c2, right) };
1313        if right_valid == ContentModelResult::Valid {
1314            return ContentModelResult::Valid;
1315        }
1316    }
1317
1318    ContentModelResult::Invalid
1319}
1320
1321/// Validate content against a choice content model.
1322unsafe fn valid_content_model_or(
1323    model: &_xmlElementContent,
1324    names: &[*const xmlChar],
1325) -> ContentModelResult {
1326    let c1 = model.c1;
1327    let c2 = model.c2;
1328
1329    if c1.is_null() && c2.is_null() {
1330        return ContentModelResult::Invalid;
1331    }
1332
1333    if !c1.is_null() {
1334        let result = unsafe { valid_content_model(c1, names) };
1335        if result == ContentModelResult::Valid {
1336            return ContentModelResult::Valid;
1337        }
1338    }
1339
1340    if !c2.is_null() {
1341        let result = unsafe { valid_content_model(c2, names) };
1342        if result == ContentModelResult::Valid {
1343            return ContentModelResult::Valid;
1344        }
1345    }
1346
1347    ContentModelResult::Invalid
1348}
1349
1350// ═══════════════════════════════════════════════════════════════════════════════
1351// Tests
1352// ═══════════════════════════════════════════════════════════════════════════════
1353
1354#[cfg(test)]
1355mod tests {
1356    use super::*;
1357    use crate::abi::allocator::xmlFree;
1358    use crate::abi::structs::*;
1359    use core::ffi::c_void;
1360    use core::ptr;
1361
1362    // ── Helpers ──────────────────────────────────────────────────────────
1363
1364    unsafe fn c_str(s: &[u8]) -> *const xmlChar {
1365        // Create a null-terminated xmlChar string
1366        let len = s.len();
1367        let buf = allocator::xmlMalloc(len + 1) as *mut xmlChar;
1368        assert!(!buf.is_null());
1369        ptr::copy_nonoverlapping(s.as_ptr(), buf, len);
1370        *buf.add(len) = 0;
1371        buf as *const xmlChar
1372    }
1373
1374    unsafe fn make_doc_and_dtd() -> (*mut _xmlDoc, *mut _xmlDtd) {
1375        let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1376        assert!(!doc.is_null());
1377        (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1378        (*doc).doc = doc;
1379        let dtd = create_int_subset(doc, c_str(b"root"), ptr::null(), ptr::null());
1380        assert!(!dtd.is_null());
1381        (doc, dtd)
1382    }
1383
1384    // ── DTD Access Tests ────────────────────────────────────────────────
1385
1386    #[test]
1387    fn test_get_int_subset_null() {
1388        unsafe {
1389            assert!(get_int_subset(ptr::null()).is_null());
1390        }
1391    }
1392
1393    #[test]
1394    fn test_create_int_subset() {
1395        unsafe {
1396            let (doc, dtd) = make_doc_and_dtd();
1397            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1398            assert!(!(*dtd).name.is_null());
1399            assert_eq!((*doc).intSubset, dtd);
1400
1401            // Cleanup
1402            free_dtd(dtd);
1403            allocator::xmlFree(doc as *mut c_void);
1404        }
1405    }
1406
1407    #[test]
1408    fn test_create_int_subset_null_doc() {
1409        unsafe {
1410            let dtd = create_int_subset(ptr::null_mut(), c_str(b"root"), ptr::null(), ptr::null());
1411            assert!(dtd.is_null());
1412        }
1413    }
1414
1415    #[test]
1416    fn test_new_dtd() {
1417        unsafe {
1418            let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1419            assert!(!doc.is_null());
1420            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1421            (*doc).doc = doc;
1422
1423            let dtd = new_dtd(doc, c_str(b"test"), c_str(b"-//TEST//"), c_str(b"test.dtd"));
1424            assert!(!dtd.is_null());
1425            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1426            assert_eq!((*doc).intSubset, dtd);
1427
1428            free_dtd(dtd);
1429            allocator::xmlFree(doc as *mut c_void);
1430        }
1431    }
1432
1433    #[test]
1434    fn test_new_dtd_no_doc() {
1435        unsafe {
1436            let dtd = new_dtd(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1437            assert!(!dtd.is_null());
1438            free_dtd(dtd);
1439        }
1440    }
1441
1442    // ── Notation Tests ──────────────────────────────────────────────────
1443
1444    #[test]
1445    fn test_add_get_notation() {
1446        unsafe {
1447            let (doc, dtd) = make_doc_and_dtd();
1448            let name = c_str(b"note");
1449            let pubid = c_str(b"-//TEST//NOTATION");
1450            let sysid = c_str(b"note.ent");
1451
1452            let n = add_notation_decl(dtd, name, pubid, sysid);
1453            assert!(!n.is_null());
1454            assert_eq!(string::xml_strcmp((*n).name, name), 0);
1455
1456            // Lookup
1457            let found = get_notation_decl(dtd, name);
1458            assert_eq!(found, n);
1459
1460            // Lookup non-existent
1461            let not_found = get_notation_decl(dtd, c_str(b"nonexistent"));
1462            assert!(not_found.is_null());
1463
1464            free_dtd(dtd);
1465            allocator::xmlFree(doc as *mut c_void);
1466        }
1467    }
1468
1469    #[test]
1470    fn test_add_notation_null_dtd() {
1471        unsafe {
1472            let n = add_notation_decl(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1473            assert!(n.is_null());
1474        }
1475    }
1476
1477    #[test]
1478    fn test_copy_notation() {
1479        unsafe {
1480            let (doc, dtd) = make_doc_and_dtd();
1481            let name = c_str(b"note1");
1482            let pubid = c_str(b"public");
1483            let sysid = c_str(b"system");
1484
1485            let n = add_notation_decl(dtd, name, pubid, sysid);
1486            assert!(!n.is_null());
1487
1488            let copy = copy_notation(n);
1489            assert!(!copy.is_null());
1490            assert_ne!(copy, n);
1491            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1492            assert_eq!(string::xml_strcmp((*copy).PublicID, pubid), 0);
1493            assert_eq!(string::xml_strcmp((*copy).SystemID, sysid), 0);
1494
1495            free_notation(copy);
1496            free_dtd(dtd);
1497            allocator::xmlFree(doc as *mut c_void);
1498        }
1499    }
1500
1501    #[test]
1502    fn test_copy_notation_null() {
1503        unsafe {
1504            assert!(copy_notation(ptr::null_mut()).is_null());
1505        }
1506    }
1507
1508    #[test]
1509    fn test_free_notation_null() {
1510        unsafe {
1511            free_notation(ptr::null_mut()); // Should not crash
1512        }
1513    }
1514
1515    // ── Content Model Tests ─────────────────────────────────────────────
1516
1517    #[test]
1518    fn test_create_free_content_model() {
1519        unsafe {
1520            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1521            assert!(!cm.is_null());
1522            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1523            assert_eq!((*cm).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1524
1525            free_content_model(cm);
1526        }
1527    }
1528
1529    #[test]
1530    fn test_create_content_model_pcdata() {
1531        unsafe {
1532            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
1533            assert!(!cm.is_null());
1534            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_PCDATA as c_int);
1535            free_content_model(cm);
1536        }
1537    }
1538
1539    #[test]
1540    fn test_copy_content_model() {
1541        unsafe {
1542            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1543            assert!(!cm.is_null());
1544
1545            let copy = copy_content_model(cm);
1546            assert!(!copy.is_null());
1547            assert_ne!(copy, cm);
1548            assert_eq!((*copy).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1549            assert_eq!((*copy).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1550            assert_eq!(string::xml_strcmp((*copy).name, (*cm).name), 0);
1551
1552            free_content_model(cm);
1553            free_content_model(copy);
1554        }
1555    }
1556
1557    #[test]
1558    fn test_copy_content_model_null() {
1559        unsafe {
1560            assert!(copy_content_model(ptr::null_mut()).is_null());
1561        }
1562    }
1563
1564    #[test]
1565    fn test_free_content_model_null() {
1566        unsafe {
1567            free_content_model(ptr::null_mut()); // Should not crash
1568        }
1569    }
1570
1571    #[test]
1572    fn test_create_sequence_content_model() {
1573        unsafe {
1574            let c1 = create_content_model(c_str(b"a"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1575            let c2 = create_content_model(c_str(b"b"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1576            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
1577            assert!(!seq.is_null());
1578            (*seq).c1 = c1;
1579            (*seq).c2 = c2;
1580            (*c1).parent = seq;
1581            (*c2).parent = seq;
1582
1583            free_content_model(seq);
1584        }
1585    }
1586
1587    // ── Element Declaration Tests ───────────────────────────────────────
1588
1589    #[test]
1590    fn test_add_get_element() {
1591        unsafe {
1592            let (doc, dtd) = make_doc_and_dtd();
1593            let name = c_str(b"myElement");
1594
1595            let elem =
1596                add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
1597            assert!(!elem.is_null());
1598            assert_eq!((*elem).type_, XML_ELEMENT_TYPE_EMPTY as c_int);
1599
1600            let found = get_element_decl(dtd, name);
1601            assert_eq!(found, elem);
1602
1603            let not_found = get_element_decl(dtd, c_str(b"nonexistent"));
1604            assert!(not_found.is_null());
1605
1606            free_dtd(dtd);
1607            allocator::xmlFree(doc as *mut c_void);
1608        }
1609    }
1610
1611    #[test]
1612    fn test_add_element_duplicate() {
1613        unsafe {
1614            let (doc, dtd) = make_doc_and_dtd();
1615            let name = c_str(b"dup");
1616
1617            let e1 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
1618            assert!(!e1.is_null());
1619
1620            let e2 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ANY as c_int, ptr::null_mut());
1621            assert_eq!(e1, e2); // Same pointer returned
1622            assert_eq!((*e2).type_, XML_ELEMENT_TYPE_EMPTY as c_int); // Still empty
1623
1624            free_dtd(dtd);
1625            allocator::xmlFree(doc as *mut c_void);
1626        }
1627    }
1628
1629    #[test]
1630    fn test_add_element_null_dtd() {
1631        unsafe {
1632            let elem = add_element_decl(
1633                ptr::null_mut(),
1634                c_str(b"test"),
1635                XML_ELEMENT_TYPE_EMPTY as c_int,
1636                ptr::null_mut(),
1637            );
1638            assert!(elem.is_null());
1639        }
1640    }
1641
1642    #[test]
1643    fn test_copy_element() {
1644        unsafe {
1645            let (doc, dtd) = make_doc_and_dtd();
1646            let name = c_str(b"source");
1647            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1648
1649            let elem = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ELEMENT as c_int, cm);
1650            assert!(!elem.is_null());
1651
1652            let copy = copy_element(elem);
1653            assert!(!copy.is_null());
1654            assert_ne!(copy, elem);
1655            assert_eq!((*copy).type_, XML_ELEMENT_TYPE_ELEMENT as c_int);
1656            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1657            assert!(!(*copy).content.is_null());
1658            assert_ne!((*copy).content, cm);
1659
1660            free_element(copy);
1661            free_dtd(dtd);
1662            allocator::xmlFree(doc as *mut c_void);
1663        }
1664    }
1665
1666    #[test]
1667    fn test_free_element_null() {
1668        unsafe {
1669            free_element(ptr::null_mut()); // Should not crash
1670        }
1671    }
1672
1673    // ── Attribute Declaration Tests ─────────────────────────────────────
1674
1675    #[test]
1676    fn test_add_get_attribute() {
1677        unsafe {
1678            let (doc, dtd) = make_doc_and_dtd();
1679            let elem_name = c_str(b"elem");
1680            let attr_name = c_str(b"attr1");
1681
1682            let elem = add_element_decl(
1683                dtd,
1684                elem_name,
1685                XML_ELEMENT_TYPE_EMPTY as c_int,
1686                ptr::null_mut(),
1687            );
1688            assert!(!elem.is_null());
1689
1690            let attr = add_attribute_decl(
1691                dtd,
1692                elem,
1693                attr_name,
1694                XML_ATTRIBUTE_CDATA as c_int,
1695                XML_ATTRIBUTE_IMPLIED as c_int,
1696                ptr::null(),
1697                ptr::null_mut(),
1698            );
1699            assert!(!attr.is_null());
1700            assert_eq!((*attr).atype, XML_ATTRIBUTE_CDATA as c_int);
1701            assert_eq!((*attr).def, XML_ATTRIBUTE_IMPLIED as c_int);
1702
1703            // Lookup by element + attribute name
1704            let found = get_attribute_decl(dtd, elem, attr_name, 0);
1705            assert_eq!(found, attr);
1706
1707            // Lookup non-existent
1708            let not_found = get_attribute_decl(dtd, elem, c_str(b"nonexistent"), 0);
1709            assert!(not_found.is_null());
1710
1711            free_dtd(dtd);
1712            allocator::xmlFree(doc as *mut c_void);
1713        }
1714    }
1715
1716    #[test]
1717    fn test_add_attribute_with_default() {
1718        unsafe {
1719            let (doc, dtd) = make_doc_and_dtd();
1720            let elem_name = c_str(b"elem");
1721            let attr_name = c_str(b"color");
1722            let default_val = c_str(b"red");
1723
1724            let elem = add_element_decl(
1725                dtd,
1726                elem_name,
1727                XML_ELEMENT_TYPE_EMPTY as c_int,
1728                ptr::null_mut(),
1729            );
1730
1731            let attr = add_attribute_decl(
1732                dtd,
1733                elem,
1734                attr_name,
1735                XML_ATTRIBUTE_CDATA as c_int,
1736                XML_ATTRIBUTE_FIXED as c_int,
1737                default_val,
1738                ptr::null_mut(),
1739            );
1740            assert!(!attr.is_null());
1741            assert_eq!((*attr).def, XML_ATTRIBUTE_FIXED as c_int);
1742            assert_eq!(string::xml_strcmp((*attr).defaultValue, default_val), 0);
1743
1744            free_dtd(dtd);
1745            allocator::xmlFree(doc as *mut c_void);
1746        }
1747    }
1748
1749    #[test]
1750    fn test_add_attribute_enumeration() {
1751        unsafe {
1752            let (doc, dtd) = make_doc_and_dtd();
1753            let elem_name = c_str(b"elem");
1754            let attr_name = c_str(b"size");
1755
1756            // Build enumeration: small, medium, large
1757            let v3 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1758                as *mut _xmlEnumeration;
1759            (*v3).name = string::xml_strdup(c_str(b"large"));
1760            let v2 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1761                as *mut _xmlEnumeration;
1762            (*v2).name = string::xml_strdup(c_str(b"medium"));
1763            (*v2).next = v3;
1764            let v1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1765                as *mut _xmlEnumeration;
1766            (*v1).name = string::xml_strdup(c_str(b"small"));
1767            (*v1).next = v2;
1768
1769            let elem = add_element_decl(
1770                dtd,
1771                elem_name,
1772                XML_ELEMENT_TYPE_EMPTY as c_int,
1773                ptr::null_mut(),
1774            );
1775            let attr = add_attribute_decl(
1776                dtd,
1777                elem,
1778                attr_name,
1779                XML_ATTRIBUTE_ENUMERATION as c_int,
1780                XML_ATTRIBUTE_REQUIRED as c_int,
1781                ptr::null(),
1782                v1,
1783            );
1784            assert!(!attr.is_null());
1785            assert_eq!((*attr).atype, XML_ATTRIBUTE_ENUMERATION as c_int);
1786
1787            free_dtd(dtd);
1788            allocator::xmlFree(doc as *mut c_void);
1789        }
1790    }
1791
1792    #[test]
1793    fn test_add_attribute_null_dtd() {
1794        unsafe {
1795            let attr = add_attribute_decl(
1796                ptr::null_mut(),
1797                ptr::null_mut(),
1798                c_str(b"test"),
1799                XML_ATTRIBUTE_CDATA as c_int,
1800                XML_ATTRIBUTE_IMPLIED as c_int,
1801                ptr::null(),
1802                ptr::null_mut(),
1803            );
1804            assert!(attr.is_null());
1805        }
1806    }
1807
1808    #[test]
1809    fn test_copy_attribute() {
1810        unsafe {
1811            let (doc, dtd) = make_doc_and_dtd();
1812            let elem_name = c_str(b"elem");
1813            let attr_name = c_str(b"id");
1814            let default_val = c_str(b"default");
1815
1816            let elem = add_element_decl(
1817                dtd,
1818                elem_name,
1819                XML_ELEMENT_TYPE_EMPTY as c_int,
1820                ptr::null_mut(),
1821            );
1822            let attr = add_attribute_decl(
1823                dtd,
1824                elem,
1825                attr_name,
1826                XML_ATTRIBUTE_ID as c_int,
1827                XML_ATTRIBUTE_IMPLIED as c_int,
1828                default_val,
1829                ptr::null_mut(),
1830            );
1831            assert!(!attr.is_null());
1832
1833            let copy = copy_attribute_decl(attr);
1834            assert!(!copy.is_null());
1835            assert_ne!(copy, attr);
1836            assert_eq!((*copy).atype, XML_ATTRIBUTE_ID as c_int);
1837            assert_eq!(string::xml_strcmp((*copy).name, attr_name), 0);
1838
1839            free_attribute(copy);
1840            free_dtd(dtd);
1841            allocator::xmlFree(doc as *mut c_void);
1842        }
1843    }
1844
1845    #[test]
1846    fn test_free_attribute_null() {
1847        unsafe {
1848            free_attribute(ptr::null_mut()); // Should not crash
1849        }
1850    }
1851
1852    // ── Content Model Validation Tests ──────────────────────────────────
1853
1854    #[test]
1855    fn test_valid_content_model_null() {
1856        unsafe {
1857            assert_eq!(
1858                valid_content_model(ptr::null_mut(), &[]),
1859                ContentModelResult::Invalid
1860            );
1861        }
1862    }
1863
1864    #[test]
1865    fn test_valid_content_model_pcdata() {
1866        unsafe {
1867            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
1868            assert!(!cm.is_null());
1869
1870            // Empty content is valid for PCDATA
1871            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Valid);
1872
1873            // Non-empty content is invalid for PCDATA
1874            let name = c_str(b"child");
1875            assert_eq!(
1876                valid_content_model(cm, &[name]),
1877                ContentModelResult::Invalid
1878            );
1879
1880            free_content_model(cm);
1881        }
1882    }
1883
1884    #[test]
1885    fn test_valid_content_model_element() {
1886        unsafe {
1887            let child_name = c_str(b"child");
1888            let cm = create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1889            assert!(!cm.is_null());
1890
1891            // Correct element
1892            assert_eq!(
1893                valid_content_model(cm, &[child_name]),
1894                ContentModelResult::Valid
1895            );
1896
1897            // Wrong element
1898            let other = c_str(b"other");
1899            assert_eq!(
1900                valid_content_model(cm, &[other]),
1901                ContentModelResult::Invalid
1902            );
1903
1904            // Too many elements
1905            assert_eq!(
1906                valid_content_model(cm, &[child_name, child_name]),
1907                ContentModelResult::Invalid
1908            );
1909
1910            // Empty
1911            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Invalid);
1912
1913            free_content_model(cm);
1914        }
1915    }
1916
1917    #[test]
1918    fn test_valid_content_model_seq() {
1919        unsafe {
1920            let a_name = c_str(b"a");
1921            let b_name = c_str(b"b");
1922
1923            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1924            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1925            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
1926            (*seq).c1 = c1;
1927            (*seq).c2 = c2;
1928            (*c1).parent = seq;
1929            (*c2).parent = seq;
1930
1931            // Correct sequence
1932            assert_eq!(
1933                valid_content_model(seq, &[a_name, b_name]),
1934                ContentModelResult::Valid
1935            );
1936
1937            // Wrong order
1938            assert_eq!(
1939                valid_content_model(seq, &[b_name, a_name]),
1940                ContentModelResult::Invalid
1941            );
1942
1943            // Missing element
1944            assert_eq!(
1945                valid_content_model(seq, &[a_name]),
1946                ContentModelResult::Invalid
1947            );
1948
1949            free_content_model(seq);
1950        }
1951    }
1952
1953    #[test]
1954    fn test_valid_content_model_or() {
1955        unsafe {
1956            let a_name = c_str(b"a");
1957            let b_name = c_str(b"b");
1958
1959            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1960            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1961            let choice = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_OR as c_int);
1962            (*choice).c1 = c1;
1963            (*choice).c2 = c2;
1964            (*c1).parent = choice;
1965            (*c2).parent = choice;
1966
1967            // First alternative
1968            assert_eq!(
1969                valid_content_model(choice, &[a_name]),
1970                ContentModelResult::Valid
1971            );
1972
1973            // Second alternative
1974            assert_eq!(
1975                valid_content_model(choice, &[b_name]),
1976                ContentModelResult::Valid
1977            );
1978
1979            // Neither
1980            let other = c_str(b"other");
1981            assert_eq!(
1982                valid_content_model(choice, &[other]),
1983                ContentModelResult::Invalid
1984            );
1985
1986            free_content_model(choice);
1987        }
1988    }
1989
1990    #[test]
1991    fn test_valid_content_model_optional() {
1992        unsafe {
1993            let a_name = c_str(b"a");
1994
1995            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1996            (*c1).ocur = XML_ELEMENT_CONTENT_OPT as c_int;
1997
1998            // Empty is valid for optional
1999            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2000
2001            // One is valid
2002            assert_eq!(
2003                valid_content_model(c1, &[a_name]),
2004                ContentModelResult::Valid
2005            );
2006
2007            free_content_model(c1);
2008        }
2009    }
2010
2011    #[test]
2012    fn test_valid_content_model_zero_or_more() {
2013        unsafe {
2014            let a_name = c_str(b"a");
2015
2016            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2017            (*c1).ocur = XML_ELEMENT_CONTENT_MULT as c_int;
2018
2019            // Empty is valid
2020            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2021
2022            // One is valid
2023            assert_eq!(
2024                valid_content_model(c1, &[a_name]),
2025                ContentModelResult::Valid
2026            );
2027
2028            // Multiple is valid
2029            assert_eq!(
2030                valid_content_model(c1, &[a_name, a_name, a_name]),
2031                ContentModelResult::Valid
2032            );
2033
2034            free_content_model(c1);
2035        }
2036    }
2037
2038    #[test]
2039    fn test_valid_content_model_one_or_more() {
2040        unsafe {
2041            let a_name = c_str(b"a");
2042
2043            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2044            (*c1).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
2045
2046            // Empty is invalid
2047            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Invalid);
2048
2049            // One is valid
2050            assert_eq!(
2051                valid_content_model(c1, &[a_name]),
2052                ContentModelResult::Valid
2053            );
2054
2055            // Multiple is valid
2056            assert_eq!(
2057                valid_content_model(c1, &[a_name, a_name]),
2058                ContentModelResult::Valid
2059            );
2060
2061            free_content_model(c1);
2062        }
2063    }
2064}