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.
1093pub unsafe fn add_attribute_decl(
1094    dtd: *mut _xmlDtd,
1095    elem: *mut _xmlElement,
1096    name: *const xmlChar,
1097    type_: c_int,
1098    def: c_int,
1099    defaultValue: *const xmlChar,
1100    tree: *mut _xmlEnumeration,
1101) -> *mut _xmlAttribute {
1102    if dtd.is_null() || name.is_null() {
1103        return ptr::null_mut();
1104    }
1105
1106    unsafe {
1107        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): the table is
1108        // created lazily on first use.
1109        if (*dtd).attributes.is_null() {
1110            (*dtd).attributes = hash::hash_create(8) as *mut c_void;
1111        }
1112        let d = &*dtd;
1113        let elem_name = if elem.is_null() {
1114            ptr::null()
1115        } else {
1116            (*elem).name
1117        };
1118
1119        // Check if attribute already exists for this element
1120        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): the attribute table
1121        // is keyed by (name, prefix, elem).
1122        let existing = hash::hash_lookup3(
1123            d.attributes as *mut hash::HashTable,
1124            name,
1125            ptr::null(),
1126            elem_name,
1127        );
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        (*attr).prefix = ptr::null_mut();
1149        (*attr).elem = string::xml_strdup(elem_name);
1150
1151        // Add to DTD's attribute hash table (keyed by attribute name,
1152        // prefix, element name — upstream xmlHashAdd3).
1153        let ret = hash::hash_add_entry3(
1154            d.attributes as *mut hash::HashTable,
1155            name,
1156            ptr::null(),
1157            elem_name,
1158            attr as *mut c_void,
1159        );
1160        if ret != 0 {
1161            // Failed to add
1162            if !(*attr).defaultValue.is_null() {
1163                allocator::xmlFreeImpl((*attr).defaultValue as *mut c_void);
1164            }
1165            if !(*attr).name.is_null() {
1166                allocator::xmlFreeImpl((*attr).name as *mut c_void);
1167            }
1168            if !(*attr).elem.is_null() {
1169                allocator::xmlFreeImpl((*attr).elem as *mut c_void);
1170            }
1171            allocator::xmlFreeImpl(attr as *mut c_void);
1172            // Don't free tree - caller still owns it on failure
1173            return ptr::null_mut();
1174        }
1175
1176        // Add to element's linked list
1177        if !elem.is_null() {
1178            (*attr).nexth = (*elem).attributes;
1179            (*elem).attributes = attr;
1180        }
1181
1182        // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl "Link it to the
1183        // DTD"): the attribute decl is a child node of the DTD.
1184        if (*dtd).last.is_null() {
1185            (*dtd).children = attr as *mut _xmlNode;
1186            (*dtd).last = attr as *mut _xmlNode;
1187        } else {
1188            (*(*dtd).last).next = attr as *mut _xmlNode;
1189            (*attr).prev = (*dtd).last;
1190            (*dtd).last = attr as *mut _xmlNode;
1191        }
1192
1193        attr
1194    }
1195}
1196
1197/// Look up an attribute declaration by element name and attribute name.
1198///
1199/// # UPSTREAM-PARITY
1200///
1201/// ```c
1202/// xmlAttributePtr xmlGetAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
1203///                                     const xmlChar *name, int namePrefix);
1204/// ```
1205///
1206/// The `namePrefix` parameter is ignored in this implementation
1207/// (it's a legacy parameter in libxml2).
1208///
1209/// # SAFETY
1210///
1211/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1212/// - `name` must be a valid null-terminated string.
1213/// - `elem` may be NULL.
1214pub unsafe fn get_attribute_decl(
1215    dtd: *mut _xmlDtd,
1216    elem: *mut _xmlElement,
1217    name: *const xmlChar,
1218    _namePrefix: c_int,
1219) -> *mut _xmlAttribute {
1220    if dtd.is_null() || name.is_null() {
1221        return ptr::null_mut();
1222    }
1223
1224    unsafe {
1225        let d = &*dtd;
1226        let elem_name = if elem.is_null() {
1227            ptr::null()
1228        } else {
1229            (*elem).name
1230        };
1231
1232        // UPSTREAM-PARITY (valid.c xmlGetDtdQAttrDesc): keyed by
1233        // (name, prefix, elem).
1234        let payload = hash::hash_lookup3(
1235            d.attributes as *mut hash::HashTable,
1236            name,
1237            ptr::null(),
1238            elem_name,
1239        );
1240        payload as *mut _xmlAttribute
1241    }
1242}
1243
1244/// Deep copy an attribute declaration.
1245///
1246/// # UPSTREAM-PARITY
1247///
1248/// ```c
1249/// xmlAttributePtr xmlCopyAttribute(xmlAttributePtr attr);
1250/// ```
1251///
1252/// # SAFETY
1253///
1254/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1255pub unsafe fn copy_attribute_decl(attr: *mut _xmlAttribute) -> *mut _xmlAttribute {
1256    if attr.is_null() {
1257        return ptr::null_mut();
1258    }
1259
1260    unsafe {
1261        let a = &*attr;
1262
1263        // SAFETY: Allocate zero-initialized memory for the copy.
1264        let copy =
1265            allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1266        if copy.is_null() {
1267            return ptr::null_mut();
1268        }
1269
1270        (*copy).type_ = a.type_;
1271        (*copy).name = string::xml_strdup(a.name);
1272        (*copy).parent = a.parent;
1273        (*copy).doc = a.doc;
1274        (*copy).nexth = ptr::null_mut();
1275        (*copy).atype = a.atype;
1276        (*copy).def = a.def;
1277        (*copy).defaultValue = string::xml_strdup(a.defaultValue);
1278        (*copy).tree = copy_enumeration(a.tree);
1279        (*copy).prefix = string::xml_strdup(a.prefix);
1280        (*copy).elem = string::xml_strdup(a.elem);
1281
1282        copy
1283    }
1284}
1285
1286/// Free an attribute declaration.
1287///
1288/// # UPSTREAM-PARITY
1289///
1290/// ```c
1291/// void xmlFreeAttribute(xmlAttributePtr attr);
1292/// ```
1293///
1294/// # SAFETY
1295///
1296/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1297pub unsafe fn free_attribute(attr: *mut _xmlAttribute) {
1298    if attr.is_null() {
1299        return;
1300    }
1301
1302    unsafe {
1303        let a = &*attr;
1304
1305        if !a.name.is_null() {
1306            allocator::xmlFreeImpl(a.name as *mut c_void);
1307        }
1308        if !a.defaultValue.is_null() {
1309            allocator::xmlFreeImpl(a.defaultValue as *mut c_void);
1310        }
1311        if !a.prefix.is_null() {
1312            allocator::xmlFreeImpl(a.prefix as *mut c_void);
1313        }
1314        if !a.elem.is_null() {
1315            allocator::xmlFreeImpl(a.elem as *mut c_void);
1316        }
1317        if !a.tree.is_null() {
1318            free_enumeration(a.tree);
1319        }
1320
1321        allocator::xmlFreeImpl(attr as *mut c_void);
1322    }
1323}
1324
1325// ═══════════════════════════════════════════════════════════════════════════════
1326// Content Model Validation (Automata-based)
1327// ═══════════════════════════════════════════════════════════════════════════════
1328
1329/// Result of content model validation.
1330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1331pub enum ContentModelResult {
1332    /// Content is valid.
1333    Valid,
1334    /// Content is invalid.
1335    Invalid,
1336    /// Content model is indeterminate (mixed content with PCDATA).
1337    Indeterminate,
1338}
1339
1340/// Validate content (a list of element names) against a content model,
1341/// taking occurrence indicators into account.
1342///
1343/// # UPSTREAM-PARITY
1344///
1345/// ```c
1346/// int xmlValidContentModel(xmlElementContentPtr model, ...)
1347/// ```
1348///
1349/// This implements a simple recursive descent validator for content models.
1350/// For simple content models (EMPTY, ANY, PCDATA), the check is direct.
1351/// For sequence/choice models, it recursively validates.
1352///
1353/// Returns `ContentModelResult::Valid` if the content matches the model,
1354/// `ContentModelResult::Invalid` otherwise.
1355///
1356/// # SAFETY
1357///
1358/// - `model` must be a valid pointer to an _xmlElementContent, or NULL.
1359/// - `names` must be a slice of element names (null-terminated xmlChar strings).
1360pub unsafe fn valid_content_model(
1361    model: *mut _xmlElementContent,
1362    names: &[*const xmlChar],
1363) -> ContentModelResult {
1364    if model.is_null() {
1365        return ContentModelResult::Invalid;
1366    }
1367
1368    unsafe {
1369        let m = &*model;
1370
1371        // Handle occurrence indicators at this level first
1372        match m.ocur as u32 {
1373            o if o == XML_ELEMENT_CONTENT_OPT as u32 => {
1374                // Optional: zero or one occurrence
1375                if names.is_empty() {
1376                    return ContentModelResult::Valid;
1377                }
1378                return valid_content_model_inner(model, names);
1379            }
1380            o if o == XML_ELEMENT_CONTENT_MULT as u32 => {
1381                // Zero or more
1382                if names.is_empty() {
1383                    return ContentModelResult::Valid;
1384                }
1385                return valid_content_model_zero_or_more(model, names);
1386            }
1387            o if o == XML_ELEMENT_CONTENT_PLUS as u32 => {
1388                // One or more
1389                if names.is_empty() {
1390                    return ContentModelResult::Invalid;
1391                }
1392                return valid_content_model_one_or_more(model, names);
1393            }
1394            _ => {}
1395        }
1396
1397        valid_content_model_inner(model, names)
1398    }
1399}
1400
1401/// Validate content against a content model without considering occurrence.
1402unsafe fn valid_content_model_inner(
1403    model: *mut _xmlElementContent,
1404    names: &[*const xmlChar],
1405) -> ContentModelResult {
1406    unsafe {
1407        let m = &*model;
1408
1409        match m.type_ as u32 {
1410            t if t == XML_ELEMENT_CONTENT_PCDATA as u32 => {
1411                // PCDATA: content must be empty (just text)
1412                if names.is_empty() {
1413                    ContentModelResult::Valid
1414                } else {
1415                    ContentModelResult::Invalid
1416                }
1417            }
1418            t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
1419                // Single element: must match exactly one element
1420                if names.len() != 1 {
1421                    return ContentModelResult::Invalid;
1422                }
1423                if names[0].is_null() {
1424                    return ContentModelResult::Invalid;
1425                }
1426                // Compare with model name
1427                if string::xml_strcmp(names[0], m.name) != 0 {
1428                    return ContentModelResult::Invalid;
1429                }
1430                ContentModelResult::Valid
1431            }
1432            t if t == XML_ELEMENT_CONTENT_SEQ as u32 => {
1433                // Sequence: validate children in order
1434                valid_content_model_seq(m, names)
1435            }
1436            t if t == XML_ELEMENT_CONTENT_OR as u32 => {
1437                // Choice: one of the alternatives must match all names
1438                valid_content_model_or(m, names)
1439            }
1440            _ => ContentModelResult::Invalid,
1441        }
1442    }
1443}
1444
1445/// Validate content for zero-or-more occurrence.
1446unsafe fn valid_content_model_zero_or_more(
1447    model: *mut _xmlElementContent,
1448    names: &[*const xmlChar],
1449) -> ContentModelResult {
1450    // Zero or more: try each possible split
1451    let mut i = 0;
1452    while i <= names.len() {
1453        let consumed = &names[..i];
1454        let remaining = &names[i..];
1455
1456        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1457        if consumed_valid == ContentModelResult::Valid {
1458            if remaining.is_empty() {
1459                return ContentModelResult::Valid;
1460            }
1461            // Try to match remaining with same model
1462            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1463            if remaining_valid == ContentModelResult::Valid {
1464                return ContentModelResult::Valid;
1465            }
1466        }
1467
1468        i += 1;
1469    }
1470    ContentModelResult::Invalid
1471}
1472
1473/// Validate content for one-or-more occurrence.
1474unsafe fn valid_content_model_one_or_more(
1475    model: *mut _xmlElementContent,
1476    names: &[*const xmlChar],
1477) -> ContentModelResult {
1478    // One or more: must match at least once
1479    let mut i = 1;
1480    while i <= names.len() {
1481        let consumed = &names[..i];
1482        let remaining = &names[i..];
1483
1484        let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1485        if consumed_valid == ContentModelResult::Valid {
1486            if remaining.is_empty() {
1487                return ContentModelResult::Valid;
1488            }
1489            let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1490            if remaining_valid == ContentModelResult::Valid {
1491                return ContentModelResult::Valid;
1492            }
1493        }
1494
1495        i += 1;
1496    }
1497    ContentModelResult::Invalid
1498}
1499
1500/// Validate content against a sequence content model.
1501unsafe fn valid_content_model_seq(
1502    model: &_xmlElementContent,
1503    names: &[*const xmlChar],
1504) -> ContentModelResult {
1505    // For a sequence, we need to split the names between c1 and c2
1506    // This is a simplified validation - full automata-based validation
1507    // would be more complex.
1508
1509    let c1 = model.c1;
1510    let c2 = model.c2;
1511
1512    if c1.is_null() && c2.is_null() {
1513        return ContentModelResult::Valid;
1514    }
1515
1516    if c1.is_null() {
1517        return unsafe { valid_content_model(c2, names) };
1518    }
1519
1520    if c2.is_null() {
1521        return unsafe { valid_content_model(c1, names) };
1522    }
1523
1524    // Try to split the names at each possible position
1525    // This implements a simple backtracking validator
1526    for split in 0..=names.len() {
1527        let left = &names[..split];
1528        let right = &names[split..];
1529
1530        let left_valid = unsafe { valid_content_model(c1, left) };
1531        if left_valid != ContentModelResult::Valid {
1532            continue;
1533        }
1534
1535        let right_valid = unsafe { valid_content_model(c2, right) };
1536        if right_valid == ContentModelResult::Valid {
1537            return ContentModelResult::Valid;
1538        }
1539    }
1540
1541    ContentModelResult::Invalid
1542}
1543
1544/// Validate content against a choice content model.
1545unsafe fn valid_content_model_or(
1546    model: &_xmlElementContent,
1547    names: &[*const xmlChar],
1548) -> ContentModelResult {
1549    let c1 = model.c1;
1550    let c2 = model.c2;
1551
1552    if c1.is_null() && c2.is_null() {
1553        return ContentModelResult::Invalid;
1554    }
1555
1556    if !c1.is_null() {
1557        let result = unsafe { valid_content_model(c1, names) };
1558        if result == ContentModelResult::Valid {
1559            return ContentModelResult::Valid;
1560        }
1561    }
1562
1563    if !c2.is_null() {
1564        let result = unsafe { valid_content_model(c2, names) };
1565        if result == ContentModelResult::Valid {
1566            return ContentModelResult::Valid;
1567        }
1568    }
1569
1570    ContentModelResult::Invalid
1571}
1572
1573// ═══════════════════════════════════════════════════════════════════════════════
1574// Tests
1575// ═══════════════════════════════════════════════════════════════════════════════
1576
1577#[cfg(test)]
1578mod tests {
1579    use super::*;
1580
1581    use core::ffi::c_void;
1582    use core::ptr;
1583
1584    // ── Helpers ──────────────────────────────────────────────────────────
1585
1586    unsafe fn c_str(s: &[u8]) -> *const xmlChar {
1587        // Create a null-terminated xmlChar string
1588        let len = s.len();
1589        let buf = allocator::xmlMallocImpl(len + 1) as *mut xmlChar;
1590        assert!(!buf.is_null());
1591        ptr::copy_nonoverlapping(s.as_ptr(), buf, len);
1592        *buf.add(len) = 0;
1593        buf as *const xmlChar
1594    }
1595
1596    unsafe fn make_doc_and_dtd() -> (*mut _xmlDoc, *mut _xmlDtd) {
1597        let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1598        assert!(!doc.is_null());
1599        (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1600        (*doc).doc = doc;
1601        let dtd = create_int_subset(doc, c_str(b"root"), ptr::null(), ptr::null());
1602        assert!(!dtd.is_null());
1603        (doc, dtd)
1604    }
1605
1606    // ── DTD Access Tests ────────────────────────────────────────────────
1607
1608    #[test]
1609    fn test_get_int_subset_null() {
1610        {
1611            assert!(get_int_subset(ptr::null()).is_null());
1612        }
1613    }
1614
1615    #[test]
1616    fn test_create_int_subset() {
1617        unsafe {
1618            let (doc, dtd) = make_doc_and_dtd();
1619            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1620            assert!(!(*dtd).name.is_null());
1621            assert_eq!((*doc).intSubset, dtd);
1622
1623            // Cleanup
1624            free_dtd(dtd);
1625            allocator::xmlFreeImpl(doc as *mut c_void);
1626        }
1627    }
1628
1629    #[test]
1630    fn test_create_int_subset_null_doc() {
1631        unsafe {
1632            let dtd = create_int_subset(ptr::null_mut(), c_str(b"root"), ptr::null(), ptr::null());
1633            assert!(dtd.is_null());
1634        }
1635    }
1636
1637    #[test]
1638    fn test_new_dtd() {
1639        unsafe {
1640            let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1641            assert!(!doc.is_null());
1642            (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1643            (*doc).doc = doc;
1644
1645            let dtd = new_dtd(doc, c_str(b"test"), c_str(b"-//TEST//"), c_str(b"test.dtd"));
1646            assert!(!dtd.is_null());
1647            assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1648            assert_eq!((*doc).intSubset, dtd);
1649
1650            free_dtd(dtd);
1651            allocator::xmlFreeImpl(doc as *mut c_void);
1652        }
1653    }
1654
1655    #[test]
1656    fn test_new_dtd_no_doc() {
1657        unsafe {
1658            let dtd = new_dtd(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1659            assert!(!dtd.is_null());
1660            free_dtd(dtd);
1661        }
1662    }
1663
1664    // ── Notation Tests ──────────────────────────────────────────────────
1665
1666    #[test]
1667    fn test_add_get_notation() {
1668        unsafe {
1669            let (doc, dtd) = make_doc_and_dtd();
1670            let name = c_str(b"note");
1671            let pubid = c_str(b"-//TEST//NOTATION");
1672            let sysid = c_str(b"note.ent");
1673
1674            let n = add_notation_decl(dtd, name, pubid, sysid);
1675            assert!(!n.is_null());
1676            assert_eq!(string::xml_strcmp((*n).name, name), 0);
1677
1678            // Lookup
1679            let found = get_notation_decl(dtd, name);
1680            assert_eq!(found, n);
1681
1682            // Lookup non-existent
1683            let not_found = get_notation_decl(dtd, c_str(b"nonexistent"));
1684            assert!(not_found.is_null());
1685
1686            free_dtd(dtd);
1687            allocator::xmlFreeImpl(doc as *mut c_void);
1688        }
1689    }
1690
1691    #[test]
1692    fn test_add_notation_null_dtd() {
1693        unsafe {
1694            let n = add_notation_decl(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1695            assert!(n.is_null());
1696        }
1697    }
1698
1699    #[test]
1700    fn test_copy_notation() {
1701        unsafe {
1702            let (doc, dtd) = make_doc_and_dtd();
1703            let name = c_str(b"note1");
1704            let pubid = c_str(b"public");
1705            let sysid = c_str(b"system");
1706
1707            let n = add_notation_decl(dtd, name, pubid, sysid);
1708            assert!(!n.is_null());
1709
1710            let copy = copy_notation(n);
1711            assert!(!copy.is_null());
1712            assert_ne!(copy, n);
1713            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1714            assert_eq!(string::xml_strcmp((*copy).PublicID, pubid), 0);
1715            assert_eq!(string::xml_strcmp((*copy).SystemID, sysid), 0);
1716
1717            free_notation(copy);
1718            free_dtd(dtd);
1719            allocator::xmlFreeImpl(doc as *mut c_void);
1720        }
1721    }
1722
1723    #[test]
1724    fn test_copy_notation_null() {
1725        unsafe {
1726            assert!(copy_notation(ptr::null_mut()).is_null());
1727        }
1728    }
1729
1730    #[test]
1731    fn test_free_notation_null() {
1732        unsafe {
1733            free_notation(ptr::null_mut()); // Should not crash
1734        }
1735    }
1736
1737    // ── Content Model Tests ─────────────────────────────────────────────
1738
1739    #[test]
1740    fn test_create_free_content_model() {
1741        unsafe {
1742            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1743            assert!(!cm.is_null());
1744            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1745            assert_eq!((*cm).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1746
1747            free_content_model(cm);
1748        }
1749    }
1750
1751    #[test]
1752    fn test_create_content_model_pcdata() {
1753        unsafe {
1754            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
1755            assert!(!cm.is_null());
1756            assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_PCDATA as c_int);
1757            free_content_model(cm);
1758        }
1759    }
1760
1761    #[test]
1762    fn test_copy_content_model() {
1763        unsafe {
1764            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1765            assert!(!cm.is_null());
1766
1767            let copy = copy_content_model(cm);
1768            assert!(!copy.is_null());
1769            assert_ne!(copy, cm);
1770            assert_eq!((*copy).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1771            assert_eq!((*copy).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1772            assert_eq!(string::xml_strcmp((*copy).name, (*cm).name), 0);
1773
1774            free_content_model(cm);
1775            free_content_model(copy);
1776        }
1777    }
1778
1779    #[test]
1780    fn test_copy_content_model_null() {
1781        unsafe {
1782            assert!(copy_content_model(ptr::null_mut()).is_null());
1783        }
1784    }
1785
1786    #[test]
1787    fn test_free_content_model_null() {
1788        unsafe {
1789            free_content_model(ptr::null_mut()); // Should not crash
1790        }
1791    }
1792
1793    #[test]
1794    fn test_create_sequence_content_model() {
1795        unsafe {
1796            let c1 = create_content_model(c_str(b"a"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1797            let c2 = create_content_model(c_str(b"b"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1798            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
1799            assert!(!seq.is_null());
1800            (*seq).c1 = c1;
1801            (*seq).c2 = c2;
1802            (*c1).parent = seq;
1803            (*c2).parent = seq;
1804
1805            free_content_model(seq);
1806        }
1807    }
1808
1809    // ── Element Declaration Tests ───────────────────────────────────────
1810
1811    #[test]
1812    fn test_add_get_element() {
1813        unsafe {
1814            let (doc, dtd) = make_doc_and_dtd();
1815            let name = c_str(b"myElement");
1816
1817            let elem =
1818                add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
1819            assert!(!elem.is_null());
1820            assert_eq!((*elem).etype, XML_ELEMENT_TYPE_EMPTY as c_int);
1821
1822            let found = get_element_decl(dtd, name);
1823            assert_eq!(found, elem);
1824
1825            let not_found = get_element_decl(dtd, c_str(b"nonexistent"));
1826            assert!(not_found.is_null());
1827
1828            free_dtd(dtd);
1829            allocator::xmlFreeImpl(doc as *mut c_void);
1830        }
1831    }
1832
1833    #[test]
1834    fn test_add_element_duplicate() {
1835        unsafe {
1836            let (doc, dtd) = make_doc_and_dtd();
1837            let name = c_str(b"dup");
1838
1839            let e1 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
1840            assert!(!e1.is_null());
1841
1842            let e2 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ANY as c_int, ptr::null_mut());
1843            assert_eq!(e1, e2); // Same pointer returned
1844            assert_eq!((*e2).type_, XML_ELEMENT_DECL as c_int); // node type
1845            assert_eq!((*e2).etype, XML_ELEMENT_TYPE_EMPTY as c_int); // Still empty
1846
1847            free_dtd(dtd);
1848            allocator::xmlFreeImpl(doc as *mut c_void);
1849        }
1850    }
1851
1852    #[test]
1853    fn test_add_element_null_dtd() {
1854        unsafe {
1855            let elem = add_element_decl(
1856                ptr::null_mut(),
1857                c_str(b"test"),
1858                XML_ELEMENT_TYPE_EMPTY as c_int,
1859                ptr::null_mut(),
1860            );
1861            assert!(elem.is_null());
1862        }
1863    }
1864
1865    #[test]
1866    fn test_copy_element() {
1867        unsafe {
1868            let (doc, dtd) = make_doc_and_dtd();
1869            let name = c_str(b"source");
1870            let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1871
1872            let elem = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ELEMENT as c_int, cm);
1873            assert!(!elem.is_null());
1874
1875            let copy = copy_element(elem);
1876            assert!(!copy.is_null());
1877            assert_ne!(copy, elem);
1878            assert_eq!((*copy).type_, XML_ELEMENT_DECL as c_int);
1879            assert_eq!((*copy).etype, XML_ELEMENT_TYPE_ELEMENT as c_int);
1880            assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1881            assert!(!(*copy).content.is_null());
1882            assert_ne!((*copy).content, cm);
1883
1884            free_element(copy);
1885            free_dtd(dtd);
1886            allocator::xmlFreeImpl(doc as *mut c_void);
1887        }
1888    }
1889
1890    #[test]
1891    fn test_free_element_null() {
1892        unsafe {
1893            free_element(ptr::null_mut()); // Should not crash
1894        }
1895    }
1896
1897    // ── Attribute Declaration Tests ─────────────────────────────────────
1898
1899    #[test]
1900    fn test_add_get_attribute() {
1901        unsafe {
1902            let (doc, dtd) = make_doc_and_dtd();
1903            let elem_name = c_str(b"elem");
1904            let attr_name = c_str(b"attr1");
1905
1906            let elem = add_element_decl(
1907                dtd,
1908                elem_name,
1909                XML_ELEMENT_TYPE_EMPTY as c_int,
1910                ptr::null_mut(),
1911            );
1912            assert!(!elem.is_null());
1913
1914            let attr = add_attribute_decl(
1915                dtd,
1916                elem,
1917                attr_name,
1918                XML_ATTRIBUTE_CDATA as c_int,
1919                XML_ATTRIBUTE_IMPLIED as c_int,
1920                ptr::null(),
1921                ptr::null_mut(),
1922            );
1923            assert!(!attr.is_null());
1924            assert_eq!((*attr).atype, XML_ATTRIBUTE_CDATA as c_int);
1925            assert_eq!((*attr).def, XML_ATTRIBUTE_IMPLIED as c_int);
1926
1927            // Lookup by element + attribute name
1928            let found = get_attribute_decl(dtd, elem, attr_name, 0);
1929            assert_eq!(found, attr);
1930
1931            // Lookup non-existent
1932            let not_found = get_attribute_decl(dtd, elem, c_str(b"nonexistent"), 0);
1933            assert!(not_found.is_null());
1934
1935            free_dtd(dtd);
1936            allocator::xmlFreeImpl(doc as *mut c_void);
1937        }
1938    }
1939
1940    #[test]
1941    fn test_add_attribute_with_default() {
1942        unsafe {
1943            let (doc, dtd) = make_doc_and_dtd();
1944            let elem_name = c_str(b"elem");
1945            let attr_name = c_str(b"color");
1946            let default_val = c_str(b"red");
1947
1948            let elem = add_element_decl(
1949                dtd,
1950                elem_name,
1951                XML_ELEMENT_TYPE_EMPTY as c_int,
1952                ptr::null_mut(),
1953            );
1954
1955            let attr = add_attribute_decl(
1956                dtd,
1957                elem,
1958                attr_name,
1959                XML_ATTRIBUTE_CDATA as c_int,
1960                XML_ATTRIBUTE_FIXED as c_int,
1961                default_val,
1962                ptr::null_mut(),
1963            );
1964            assert!(!attr.is_null());
1965            assert_eq!((*attr).def, XML_ATTRIBUTE_FIXED as c_int);
1966            assert_eq!(string::xml_strcmp((*attr).defaultValue, default_val), 0);
1967
1968            free_dtd(dtd);
1969            allocator::xmlFreeImpl(doc as *mut c_void);
1970        }
1971    }
1972
1973    #[test]
1974    fn test_add_attribute_enumeration() {
1975        unsafe {
1976            let (doc, dtd) = make_doc_and_dtd();
1977            let elem_name = c_str(b"elem");
1978            let attr_name = c_str(b"size");
1979
1980            // Build enumeration: small, medium, large
1981            let v3 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1982                as *mut _xmlEnumeration;
1983            (*v3).name = string::xml_strdup(c_str(b"large"));
1984            let v2 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1985                as *mut _xmlEnumeration;
1986            (*v2).name = string::xml_strdup(c_str(b"medium"));
1987            (*v2).next = v3;
1988            let v1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1989                as *mut _xmlEnumeration;
1990            (*v1).name = string::xml_strdup(c_str(b"small"));
1991            (*v1).next = v2;
1992
1993            let elem = add_element_decl(
1994                dtd,
1995                elem_name,
1996                XML_ELEMENT_TYPE_EMPTY as c_int,
1997                ptr::null_mut(),
1998            );
1999            let attr = add_attribute_decl(
2000                dtd,
2001                elem,
2002                attr_name,
2003                XML_ATTRIBUTE_ENUMERATION as c_int,
2004                XML_ATTRIBUTE_REQUIRED as c_int,
2005                ptr::null(),
2006                v1,
2007            );
2008            assert!(!attr.is_null());
2009            assert_eq!((*attr).atype, XML_ATTRIBUTE_ENUMERATION as c_int);
2010
2011            free_dtd(dtd);
2012            allocator::xmlFreeImpl(doc as *mut c_void);
2013        }
2014    }
2015
2016    #[test]
2017    fn test_add_attribute_null_dtd() {
2018        unsafe {
2019            let attr = add_attribute_decl(
2020                ptr::null_mut(),
2021                ptr::null_mut(),
2022                c_str(b"test"),
2023                XML_ATTRIBUTE_CDATA as c_int,
2024                XML_ATTRIBUTE_IMPLIED as c_int,
2025                ptr::null(),
2026                ptr::null_mut(),
2027            );
2028            assert!(attr.is_null());
2029        }
2030    }
2031
2032    #[test]
2033    fn test_copy_attribute() {
2034        unsafe {
2035            let (doc, dtd) = make_doc_and_dtd();
2036            let elem_name = c_str(b"elem");
2037            let attr_name = c_str(b"id");
2038            let default_val = c_str(b"default");
2039
2040            let elem = add_element_decl(
2041                dtd,
2042                elem_name,
2043                XML_ELEMENT_TYPE_EMPTY as c_int,
2044                ptr::null_mut(),
2045            );
2046            let attr = add_attribute_decl(
2047                dtd,
2048                elem,
2049                attr_name,
2050                XML_ATTRIBUTE_ID as c_int,
2051                XML_ATTRIBUTE_IMPLIED as c_int,
2052                default_val,
2053                ptr::null_mut(),
2054            );
2055            assert!(!attr.is_null());
2056
2057            let copy = copy_attribute_decl(attr);
2058            assert!(!copy.is_null());
2059            assert_ne!(copy, attr);
2060            assert_eq!((*copy).atype, XML_ATTRIBUTE_ID as c_int);
2061            assert_eq!(string::xml_strcmp((*copy).name, attr_name), 0);
2062
2063            free_attribute(copy);
2064            free_dtd(dtd);
2065            allocator::xmlFreeImpl(doc as *mut c_void);
2066        }
2067    }
2068
2069    #[test]
2070    fn test_free_attribute_null() {
2071        unsafe {
2072            free_attribute(ptr::null_mut()); // Should not crash
2073        }
2074    }
2075
2076    // ── Content Model Validation Tests ──────────────────────────────────
2077
2078    #[test]
2079    fn test_valid_content_model_null() {
2080        unsafe {
2081            assert_eq!(
2082                valid_content_model(ptr::null_mut(), &[]),
2083                ContentModelResult::Invalid
2084            );
2085        }
2086    }
2087
2088    #[test]
2089    fn test_valid_content_model_pcdata() {
2090        unsafe {
2091            let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
2092            assert!(!cm.is_null());
2093
2094            // Empty content is valid for PCDATA
2095            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Valid);
2096
2097            // Non-empty content is invalid for PCDATA
2098            let name = c_str(b"child");
2099            assert_eq!(
2100                valid_content_model(cm, &[name]),
2101                ContentModelResult::Invalid
2102            );
2103
2104            free_content_model(cm);
2105        }
2106    }
2107
2108    #[test]
2109    fn test_valid_content_model_element() {
2110        unsafe {
2111            let child_name = c_str(b"child");
2112            let cm = create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2113            assert!(!cm.is_null());
2114
2115            // Correct element
2116            assert_eq!(
2117                valid_content_model(cm, &[child_name]),
2118                ContentModelResult::Valid
2119            );
2120
2121            // Wrong element
2122            let other = c_str(b"other");
2123            assert_eq!(
2124                valid_content_model(cm, &[other]),
2125                ContentModelResult::Invalid
2126            );
2127
2128            // Too many elements
2129            assert_eq!(
2130                valid_content_model(cm, &[child_name, child_name]),
2131                ContentModelResult::Invalid
2132            );
2133
2134            // Empty
2135            assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Invalid);
2136
2137            free_content_model(cm);
2138        }
2139    }
2140
2141    #[test]
2142    fn test_valid_content_model_seq() {
2143        unsafe {
2144            let a_name = c_str(b"a");
2145            let b_name = c_str(b"b");
2146
2147            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2148            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2149            let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
2150            (*seq).c1 = c1;
2151            (*seq).c2 = c2;
2152            (*c1).parent = seq;
2153            (*c2).parent = seq;
2154
2155            // Correct sequence
2156            assert_eq!(
2157                valid_content_model(seq, &[a_name, b_name]),
2158                ContentModelResult::Valid
2159            );
2160
2161            // Wrong order
2162            assert_eq!(
2163                valid_content_model(seq, &[b_name, a_name]),
2164                ContentModelResult::Invalid
2165            );
2166
2167            // Missing element
2168            assert_eq!(
2169                valid_content_model(seq, &[a_name]),
2170                ContentModelResult::Invalid
2171            );
2172
2173            free_content_model(seq);
2174        }
2175    }
2176
2177    #[test]
2178    fn test_valid_content_model_or() {
2179        unsafe {
2180            let a_name = c_str(b"a");
2181            let b_name = c_str(b"b");
2182
2183            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2184            let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2185            let choice = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_OR as c_int);
2186            (*choice).c1 = c1;
2187            (*choice).c2 = c2;
2188            (*c1).parent = choice;
2189            (*c2).parent = choice;
2190
2191            // First alternative
2192            assert_eq!(
2193                valid_content_model(choice, &[a_name]),
2194                ContentModelResult::Valid
2195            );
2196
2197            // Second alternative
2198            assert_eq!(
2199                valid_content_model(choice, &[b_name]),
2200                ContentModelResult::Valid
2201            );
2202
2203            // Neither
2204            let other = c_str(b"other");
2205            assert_eq!(
2206                valid_content_model(choice, &[other]),
2207                ContentModelResult::Invalid
2208            );
2209
2210            free_content_model(choice);
2211        }
2212    }
2213
2214    #[test]
2215    fn test_valid_content_model_optional() {
2216        unsafe {
2217            let a_name = c_str(b"a");
2218
2219            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2220            (*c1).ocur = XML_ELEMENT_CONTENT_OPT as c_int;
2221
2222            // Empty is valid for optional
2223            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2224
2225            // One is valid
2226            assert_eq!(
2227                valid_content_model(c1, &[a_name]),
2228                ContentModelResult::Valid
2229            );
2230
2231            free_content_model(c1);
2232        }
2233    }
2234
2235    #[test]
2236    fn test_valid_content_model_zero_or_more() {
2237        unsafe {
2238            let a_name = c_str(b"a");
2239
2240            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2241            (*c1).ocur = XML_ELEMENT_CONTENT_MULT as c_int;
2242
2243            // Empty is valid
2244            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2245
2246            // One is valid
2247            assert_eq!(
2248                valid_content_model(c1, &[a_name]),
2249                ContentModelResult::Valid
2250            );
2251
2252            // Multiple is valid
2253            assert_eq!(
2254                valid_content_model(c1, &[a_name, a_name, a_name]),
2255                ContentModelResult::Valid
2256            );
2257
2258            free_content_model(c1);
2259        }
2260    }
2261
2262    #[test]
2263    fn test_valid_content_model_one_or_more() {
2264        unsafe {
2265            let a_name = c_str(b"a");
2266
2267            let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2268            (*c1).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
2269
2270            // Empty is invalid
2271            assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Invalid);
2272
2273            // One is valid
2274            assert_eq!(
2275                valid_content_model(c1, &[a_name]),
2276                ContentModelResult::Valid
2277            );
2278
2279            // Multiple is valid
2280            assert_eq!(
2281                valid_content_model(c1, &[a_name, a_name]),
2282                ContentModelResult::Valid
2283            );
2284
2285            free_content_model(c1);
2286        }
2287    }
2288}