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