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