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