Skip to main content

libxml_rs/xml/dtd/
mod.rs

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