Skip to main content

libxml_rs/xml/dtd/
mod.rs

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