Skip to main content

libxml_rs/xml/dtd/
mod.rs

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