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 // Placeholder upgrade (add_element_decl carries the attribute chain
797 // of a removed UNDEFINED placeholder to the real declaration).
798 let mut elem_attrs: *mut _xmlAttribute = ptr::null_mut();
799 // UPSTREAM-PARITY (valid.c xmlAddElementDecl): the table is created
800 // lazily on first use.
801 if (*dtd).elements.is_null() {
802 (*dtd).elements = hash::hash_create(8) as *mut c_void;
803 }
804 let d = &*dtd;
805
806 // Check if element already exists
807 let existing = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
808 if !existing.is_null() {
809 let ex = existing as *mut _xmlElement;
810 if (*ex).etype != XML_ELEMENT_TYPE_UNDEFINED as c_int {
811 // UPSTREAM-PARITY (valid.c xmlAddElementDecl): when a real
812 // declaration already exists the incoming content model is
813 // NOT attached — upstream frees it before returning the
814 // existing declaration (the caller handed ownership over).
815 // Dropping it here leaked the freshly built tree
816 // (ASan fuzz: malformed-DTD docs whose decls were first
817 // registered by the SAX layer).
818 if !content.is_null() {
819 free_content_model(content);
820 }
821 return ex;
822 }
823 // UPSTREAM-PARITY (valid.c xmlAddElementDecl): an UNDEFINED
824 // placeholder (created when an ATTLIST named an as-yet
825 // undeclared element) is REMOVED and freed; its attribute
826 // declarations are carried over to the real declaration that
827 // follows ("lookup old attributes inserted on an undefined
828 // element in the internal subset"). The placeholder was never
829 // linked into the DTD child list (get_element_decl_created), so
830 // only the hash entry is removed.
831 let old_attributes = (*ex).attributes;
832 (*ex).attributes = ptr::null_mut();
833 hash::hash_remove_entry(d.elements as *mut hash::HashTable, name, None);
834 free_element(ex);
835 elem_attrs = old_attributes;
836 }
837
838 // SAFETY: Allocate zero-initialized memory for the element.
839 let elem = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
840 if elem.is_null() {
841 return ptr::null_mut();
842 }
843
844 // UPSTREAM-PARITY (valid.c xmlAddElementDecl): the node type is
845 // XML_ELEMENT_DECL; the element type (EMPTY/ANY/MIXED/ELEMENT) is
846 // stored in `etype` only. The caller passes the element type as
847 // `type_` (matching the upstream parameter list).
848 (*elem).name = string::xml_strdup(name);
849 (*elem).type_ = XML_ELEMENT_DECL as c_int;
850 (*elem).etype = type_; // xmlElementTypeVal mirrors xmlElementType here
851 (*elem).content = content; // Takes ownership of the content model
852 (*elem).attributes = elem_attrs;
853 (*elem).prefix = ptr::null_mut();
854 (*elem).children = ptr::null_mut();
855 (*elem).last = ptr::null_mut();
856 (*elem).parent = dtd;
857 (*elem).next = ptr::null_mut();
858 (*elem).prev = ptr::null_mut();
859 (*elem).doc = (*dtd).doc;
860 (*elem).cont_model = ptr::null_mut();
861
862 // Add to hash table
863 let ret = hash::hash_add_entry(
864 d.elements as *mut hash::HashTable,
865 name,
866 elem as *mut c_void,
867 );
868 if ret != 0 {
869 // Failed to add
870 free_element(elem);
871 return ptr::null_mut();
872 }
873
874 // UPSTREAM-PARITY (valid.c xmlAddElementDecl "Link it to the DTD"):
875 // the element decl is a child node of the DTD.
876 if (*dtd).last.is_null() {
877 (*dtd).children = elem as *mut _xmlNode;
878 (*dtd).last = elem as *mut _xmlNode;
879 } else {
880 (*(*dtd).last).next = elem as *mut _xmlNode;
881 (*elem).prev = (*dtd).last;
882 (*dtd).last = elem as *mut _xmlNode;
883 }
884
885 elem
886 }
887}
888
889/// Look up an element declaration by name.
890///
891/// # UPSTREAM-PARITY
892///
893/// ```c
894/// xmlElementPtr xmlGetElementDecl(xmlDtdPtr dtd, const xmlChar *name);
895/// ```
896///
897/// # SAFETY
898///
899/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
900/// - `name` must be a valid null-terminated string.
901pub unsafe fn get_element_decl(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlElement {
902 if dtd.is_null() || name.is_null() {
903 return ptr::null_mut();
904 }
905
906 unsafe {
907 let d = &*dtd;
908 if d.elements.is_null() {
909 return ptr::null_mut();
910 }
911 let payload = hash::hash_lookup(d.elements as *mut hash::HashTable, name);
912 payload as *mut _xmlElement
913 }
914}
915
916/// UPSTREAM-PARITY (valid.c xmlGetDtdElementDesc2): lookup an element
917/// declaration, creating an UNDEFINED placeholder when missing. The
918/// placeholder is registered in the elements table but NOT linked into the
919/// DTD's child list (the attribute-decl path that triggers this leaves the
920/// element undeclared).
921///
922/// # SAFETY
923///
924/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
925/// - `name` must be a valid null-terminated string.
926pub unsafe fn get_element_decl_created(
927 dtd: *mut _xmlDtd,
928 name: *const xmlChar,
929) -> *mut _xmlElement {
930 if dtd.is_null() || name.is_null() {
931 return ptr::null_mut();
932 }
933
934 unsafe {
935 if (*dtd).elements.is_null() {
936 (*dtd).elements = hash::hash_create(8) as *mut c_void;
937 }
938 let existing = hash::hash_lookup((*dtd).elements as *mut hash::HashTable, name);
939 if !existing.is_null() {
940 return existing as *mut _xmlElement;
941 }
942
943 let elem = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
944 if elem.is_null() {
945 return ptr::null_mut();
946 }
947 (*elem).type_ = XML_ELEMENT_DECL as c_int;
948 (*elem).name = string::xml_strdup(name);
949 (*elem).etype = XML_ELEMENT_TYPE_UNDEFINED as c_int;
950 (*elem).doc = (*dtd).doc;
951 (*elem).parent = dtd;
952 if hash::hash_add_entry(
953 (*dtd).elements as *mut hash::HashTable,
954 name,
955 elem as *mut c_void,
956 ) != 0
957 {
958 free_element(elem);
959 return ptr::null_mut();
960 }
961 elem
962 }
963}
964
965/// Deep copy an element declaration.
966///
967/// # UPSTREAM-PARITY
968///
969/// ```c
970/// xmlElementPtr xmlCopyElement(xmlElementPtr elem);
971/// ```
972///
973/// # SAFETY
974///
975/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
976pub unsafe fn copy_element(elem: *mut _xmlElement) -> *mut _xmlElement {
977 if elem.is_null() {
978 return ptr::null_mut();
979 }
980
981 unsafe {
982 let e = &*elem;
983
984 // SAFETY: Allocate zero-initialized memory for the copy.
985 let copy = allocator::xmlMallocZero(size_of::<_xmlElement>() as usize) as *mut _xmlElement;
986 if copy.is_null() {
987 return ptr::null_mut();
988 }
989
990 (*copy).name = string::xml_strdup(e.name);
991 (*copy).type_ = e.type_;
992 (*copy).etype = e.etype;
993 (*copy).content = copy_content_model(e.content);
994 (*copy).prefix = string::xml_strdup(e.prefix);
995 (*copy)._private = e._private;
996 (*copy).parent = e.parent;
997 (*copy).doc = e.doc;
998
999 // Copy attribute declarations (linked list)
1000 if !e.attributes.is_null() {
1001 // UPSTREAM-PARITY: We copy the attribute linked list by
1002 // iterating and copying each attribute.
1003 let mut src_attr = e.attributes;
1004 let mut prev_copy: *mut _xmlAttribute = ptr::null_mut();
1005 let mut first_copy: *mut _xmlAttribute = ptr::null_mut();
1006
1007 while !src_attr.is_null() {
1008 let attr_copy = copy_attribute_decl(src_attr);
1009 if attr_copy.is_null() {
1010 // Free what we've copied so far
1011 let mut to_free = first_copy;
1012 while !to_free.is_null() {
1013 let next = (*to_free).nexth;
1014 free_attribute(to_free);
1015 to_free = next;
1016 }
1017 allocator::xmlFreeImpl(copy as *mut c_void);
1018 return ptr::null_mut();
1019 }
1020
1021 if prev_copy.is_null() {
1022 first_copy = attr_copy;
1023 } else {
1024 (*prev_copy).nexth = attr_copy;
1025 }
1026 prev_copy = attr_copy;
1027 src_attr = (*src_attr).nexth;
1028 }
1029
1030 (*copy).attributes = first_copy;
1031 }
1032
1033 copy
1034 }
1035}
1036
1037/// Free an element declaration and its content model.
1038///
1039/// # UPSTREAM-PARITY
1040///
1041/// ```c
1042/// void xmlFreeElement(xmlElementPtr elem);
1043/// ```
1044///
1045/// Frees the element declaration and its content model, but NOT the
1046/// attribute declarations (which are owned by the DTD's attribute hash).
1047///
1048/// # SAFETY
1049///
1050/// - `elem` must be a valid pointer to an _xmlElement, or NULL.
1051pub unsafe fn free_element(elem: *mut _xmlElement) {
1052 if elem.is_null() {
1053 return;
1054 }
1055
1056 unsafe {
1057 // Free name
1058 if !(*elem).name.is_null() {
1059 allocator::xmlFreeImpl((*elem).name as *mut c_void);
1060 }
1061
1062 // Free prefix
1063 if !(*elem).prefix.is_null() {
1064 allocator::xmlFreeImpl((*elem).prefix as *mut c_void);
1065 }
1066
1067 // Free content model
1068 if !(*elem).content.is_null() {
1069 free_content_model((*elem).content);
1070 }
1071
1072 // Free the compiled content-model NFA (xmlValidBuildContentModel)
1073 // UPSTREAM-PARITY: xmlFreeElement releases contModel via xmlRegFreeRegexp.
1074 if !(*elem).cont_model.is_null() {
1075 crate::xml::validation::free_content_model_nfa(
1076 (*elem).cont_model as *mut crate::xml::validation::ContentModelNfa,
1077 );
1078 }
1079
1080 // UPSTREAM-PARITY: The attributes linked list on the element
1081 // declaration is NOT owned by the element. The DTD's attribute
1082 // hash table is the sole owner. When the DTD is freed, the
1083 // hash table's deallocator frees all attributes.
1084 // Therefore, we do NOT free the attributes list here.
1085 (*elem).attributes = ptr::null_mut();
1086
1087 allocator::xmlFreeImpl(elem as *mut c_void);
1088 }
1089}
1090
1091// ═══════════════════════════════════════════════════════════════════════════════
1092// Attribute Declarations
1093// ═══════════════════════════════════════════════════════════════════════════════
1094
1095/// Free an enumeration value tree.
1096///
1097/// # SAFETY
1098///
1099/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
1100pub(crate) unsafe fn free_enumeration(tree: *mut _xmlEnumeration) {
1101 if tree.is_null() {
1102 return;
1103 }
1104
1105 unsafe {
1106 let mut cur = tree;
1107 while !cur.is_null() {
1108 let next = (*cur).next;
1109 if !(*cur).name.is_null() {
1110 allocator::xmlFreeImpl((*cur).name as *mut c_void);
1111 }
1112 allocator::xmlFreeImpl(cur as *mut c_void);
1113 cur = next;
1114 }
1115 }
1116}
1117
1118/// Deep copy an enumeration value tree.
1119///
1120/// # SAFETY
1121///
1122/// - `tree` must be a valid pointer to an _xmlEnumeration, or NULL.
1123unsafe fn copy_enumeration(tree: *mut _xmlEnumeration) -> *mut _xmlEnumeration {
1124 if tree.is_null() {
1125 return ptr::null_mut();
1126 }
1127
1128 unsafe {
1129 let mut src = tree;
1130 let mut first_copy: *mut _xmlEnumeration = ptr::null_mut();
1131 let mut prev_copy: *mut _xmlEnumeration = ptr::null_mut();
1132
1133 while !src.is_null() {
1134 let copy = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
1135 as *mut _xmlEnumeration;
1136 if copy.is_null() {
1137 // Free what we've allocated so far
1138 let mut to_free = first_copy;
1139 while !to_free.is_null() {
1140 let next = (*to_free).next;
1141 if !(*to_free).name.is_null() {
1142 allocator::xmlFreeImpl((*to_free).name as *mut c_void);
1143 }
1144 allocator::xmlFreeImpl(to_free as *mut c_void);
1145 to_free = next;
1146 }
1147 return ptr::null_mut();
1148 }
1149
1150 (*copy).name = string::xml_strdup((*src).name);
1151 (*copy).next = ptr::null_mut();
1152
1153 if prev_copy.is_null() {
1154 first_copy = copy;
1155 } else {
1156 (*prev_copy).next = copy;
1157 }
1158 prev_copy = copy;
1159 src = (*src).next;
1160 }
1161
1162 first_copy
1163 }
1164}
1165
1166/// Add an attribute declaration to a DTD.
1167///
1168/// # UPSTREAM-PARITY
1169///
1170/// ```c
1171/// xmlAttributePtr xmlAddAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
1172/// const xmlChar *name, int type, int def,
1173/// const xmlChar *defaultValue,
1174/// xmlEnumerationPtr tree);
1175/// ```
1176///
1177/// Adds an attribute declaration to both the DTD's attribute hash table
1178/// (keyed by element name + attribute name) and the element's linked list.
1179/// If an attribute with the same name already exists for this element,
1180/// the existing declaration is returned.
1181///
1182/// # SAFETY
1183///
1184/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1185/// - `name` must be a valid null-terminated string.
1186/// - `elem`, `defaultValue`, `tree` may be NULL.
1187#[allow(clippy::too_many_arguments)]
1188pub unsafe fn add_attribute_decl(
1189 dtd: *mut _xmlDtd,
1190 elem: *mut _xmlElement,
1191 name: *const xmlChar,
1192 ns: *const xmlChar,
1193 type_: c_int,
1194 def: c_int,
1195 defaultValue: *const xmlChar,
1196 tree: *mut _xmlEnumeration,
1197) -> *mut _xmlAttribute {
1198 if dtd.is_null() || name.is_null() {
1199 return ptr::null_mut();
1200 }
1201
1202 unsafe {
1203 // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): the table is
1204 // created lazily on first use.
1205 if (*dtd).attributes.is_null() {
1206 (*dtd).attributes = hash::hash_create(8) as *mut c_void;
1207 }
1208 let d = &*dtd;
1209 let elem_name = if elem.is_null() {
1210 ptr::null()
1211 } else {
1212 (*elem).name
1213 };
1214
1215 // Check if attribute already exists for this element
1216 // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): the attribute table
1217 // is keyed by (name, ns, elem) — xmlHashAdd3/xmlHashLookup3 with the
1218 // namespace as the middle key (R-000176: the pre-2.10 candidate
1219 // signature dropped the ns key).
1220 let existing =
1221 hash::hash_lookup3(d.attributes as *mut hash::HashTable, name, ns, elem_name);
1222 if !existing.is_null() {
1223 // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): a duplicate
1224 // declaration frees the incoming enumeration tree before
1225 // returning the existing one (the caller handed ownership over).
1226 if !tree.is_null() {
1227 free_enumeration(tree);
1228 }
1229 return existing as *mut _xmlAttribute;
1230 }
1231
1232 // SAFETY: Allocate zero-initialized memory for the attribute.
1233 let attr =
1234 allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1235 if attr.is_null() {
1236 return ptr::null_mut();
1237 }
1238
1239 (*attr).type_ = XML_ATTRIBUTE_DECL as c_int;
1240 (*attr).name = string::xml_strdup(name);
1241 (*attr).parent = dtd;
1242 (*attr).doc = d.doc;
1243 (*attr).nexth = ptr::null_mut();
1244 (*attr).atype = type_;
1245 (*attr).def = def;
1246 (*attr).defaultValue = string::xml_strdup(defaultValue);
1247 (*attr).tree = tree; // Takes ownership of the enumeration tree
1248 // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl): `prefix` mirrors the
1249 // ns argument; NULL ns leaves it NULL.
1250 (*attr).prefix = if ns.is_null() {
1251 ptr::null_mut()
1252 } else {
1253 string::xml_strdup(ns)
1254 };
1255 (*attr).elem = string::xml_strdup(elem_name);
1256
1257 // Add to DTD's attribute hash table (keyed by attribute name,
1258 // namespace, element name — upstream xmlHashAdd3).
1259 let ret = hash::hash_add_entry3(
1260 d.attributes as *mut hash::HashTable,
1261 name,
1262 ns,
1263 elem_name,
1264 attr as *mut c_void,
1265 );
1266 if ret != 0 {
1267 // Failed to add
1268 if !(*attr).defaultValue.is_null() {
1269 allocator::xmlFreeImpl((*attr).defaultValue as *mut c_void);
1270 }
1271 if !(*attr).name.is_null() {
1272 allocator::xmlFreeImpl((*attr).name as *mut c_void);
1273 }
1274 if !(*attr).elem.is_null() {
1275 allocator::xmlFreeImpl((*attr).elem as *mut c_void);
1276 }
1277 if !(*attr).prefix.is_null() {
1278 allocator::xmlFreeImpl((*attr).prefix as *mut c_void);
1279 }
1280 if !(*attr).tree.is_null() {
1281 free_enumeration((*attr).tree);
1282 }
1283 allocator::xmlFreeImpl(attr as *mut c_void);
1284 return ptr::null_mut();
1285 }
1286
1287 // Add to element's linked list
1288 if !elem.is_null() {
1289 (*attr).nexth = (*elem).attributes;
1290 (*elem).attributes = attr;
1291 }
1292
1293 // UPSTREAM-PARITY (valid.c xmlAddAttributeDecl "Link it to the
1294 // DTD"): the attribute decl is a child node of the DTD.
1295 if (*dtd).last.is_null() {
1296 (*dtd).children = attr as *mut _xmlNode;
1297 (*dtd).last = attr as *mut _xmlNode;
1298 } else {
1299 (*(*dtd).last).next = attr as *mut _xmlNode;
1300 (*attr).prev = (*dtd).last;
1301 (*dtd).last = attr as *mut _xmlNode;
1302 }
1303
1304 attr
1305 }
1306}
1307
1308/// Look up an attribute declaration by element name and attribute name.
1309///
1310/// # UPSTREAM-PARITY
1311///
1312/// ```c
1313/// xmlAttributePtr xmlGetAttributeDecl(xmlDtdPtr dtd, xmlElementPtr elem,
1314/// const xmlChar *name, int namePrefix);
1315/// ```
1316///
1317/// The `namePrefix` parameter is ignored in this implementation
1318/// (it's a legacy parameter in libxml2).
1319///
1320/// # SAFETY
1321///
1322/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
1323/// - `name` must be a valid null-terminated string.
1324/// - `elem` may be NULL.
1325pub unsafe fn get_attribute_decl(
1326 dtd: *mut _xmlDtd,
1327 elem: *mut _xmlElement,
1328 name: *const xmlChar,
1329 _namePrefix: c_int,
1330) -> *mut _xmlAttribute {
1331 if dtd.is_null() || name.is_null() {
1332 return ptr::null_mut();
1333 }
1334
1335 unsafe {
1336 let d = &*dtd;
1337 let elem_name = if elem.is_null() {
1338 ptr::null()
1339 } else {
1340 (*elem).name
1341 };
1342
1343 // UPSTREAM-PARITY (valid.c xmlGetDtdQAttrDesc): keyed by
1344 // (name, prefix, elem).
1345 let payload = hash::hash_lookup3(
1346 d.attributes as *mut hash::HashTable,
1347 name,
1348 ptr::null(),
1349 elem_name,
1350 );
1351 payload as *mut _xmlAttribute
1352 }
1353}
1354
1355/// Deep copy an attribute declaration.
1356///
1357/// # UPSTREAM-PARITY
1358///
1359/// ```c
1360/// xmlAttributePtr xmlCopyAttribute(xmlAttributePtr attr);
1361/// ```
1362///
1363/// # SAFETY
1364///
1365/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1366pub unsafe fn copy_attribute_decl(attr: *mut _xmlAttribute) -> *mut _xmlAttribute {
1367 if attr.is_null() {
1368 return ptr::null_mut();
1369 }
1370
1371 unsafe {
1372 let a = &*attr;
1373
1374 // SAFETY: Allocate zero-initialized memory for the copy.
1375 let copy =
1376 allocator::xmlMallocZero(size_of::<_xmlAttribute>() as usize) as *mut _xmlAttribute;
1377 if copy.is_null() {
1378 return ptr::null_mut();
1379 }
1380
1381 (*copy).type_ = a.type_;
1382 (*copy).name = string::xml_strdup(a.name);
1383 (*copy).parent = a.parent;
1384 (*copy).doc = a.doc;
1385 (*copy).nexth = ptr::null_mut();
1386 (*copy).atype = a.atype;
1387 (*copy).def = a.def;
1388 (*copy).defaultValue = string::xml_strdup(a.defaultValue);
1389 (*copy).tree = copy_enumeration(a.tree);
1390 (*copy).prefix = string::xml_strdup(a.prefix);
1391 (*copy).elem = string::xml_strdup(a.elem);
1392
1393 copy
1394 }
1395}
1396
1397/// Free an attribute declaration.
1398///
1399/// # UPSTREAM-PARITY
1400///
1401/// ```c
1402/// void xmlFreeAttribute(xmlAttributePtr attr);
1403/// ```
1404///
1405/// # SAFETY
1406///
1407/// - `attr` must be a valid pointer to an _xmlAttribute, or NULL.
1408pub unsafe fn free_attribute(attr: *mut _xmlAttribute) {
1409 if attr.is_null() {
1410 return;
1411 }
1412
1413 unsafe {
1414 let a = &*attr;
1415
1416 if !a.name.is_null() {
1417 allocator::xmlFreeImpl(a.name as *mut c_void);
1418 }
1419 if !a.defaultValue.is_null() {
1420 allocator::xmlFreeImpl(a.defaultValue as *mut c_void);
1421 }
1422 if !a.prefix.is_null() {
1423 allocator::xmlFreeImpl(a.prefix as *mut c_void);
1424 }
1425 if !a.elem.is_null() {
1426 allocator::xmlFreeImpl(a.elem as *mut c_void);
1427 }
1428 if !a.tree.is_null() {
1429 free_enumeration(a.tree);
1430 }
1431
1432 allocator::xmlFreeImpl(attr as *mut c_void);
1433 }
1434}
1435
1436// ═══════════════════════════════════════════════════════════════════════════════
1437// Content Model Validation (Automata-based)
1438// ═══════════════════════════════════════════════════════════════════════════════
1439
1440/// Result of content model validation.
1441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1442pub enum ContentModelResult {
1443 /// Content is valid.
1444 Valid,
1445 /// Content is invalid.
1446 Invalid,
1447 /// Content model is indeterminate (mixed content with PCDATA).
1448 Indeterminate,
1449}
1450
1451/// Validate content (a list of element names) against a content model,
1452/// taking occurrence indicators into account.
1453///
1454/// # UPSTREAM-PARITY
1455///
1456/// ```c
1457/// int xmlValidContentModel(xmlElementContentPtr model, ...)
1458/// ```
1459///
1460/// This implements a simple recursive descent validator for content models.
1461/// For simple content models (EMPTY, ANY, PCDATA), the check is direct.
1462/// For sequence/choice models, it recursively validates.
1463///
1464/// Returns `ContentModelResult::Valid` if the content matches the model,
1465/// `ContentModelResult::Invalid` otherwise.
1466///
1467/// # SAFETY
1468///
1469/// - `model` must be a valid pointer to an _xmlElementContent, or NULL.
1470/// - `names` must be a slice of element names (null-terminated xmlChar strings).
1471pub unsafe fn valid_content_model(
1472 model: *mut _xmlElementContent,
1473 names: &[*const xmlChar],
1474) -> ContentModelResult {
1475 if model.is_null() {
1476 return ContentModelResult::Invalid;
1477 }
1478
1479 unsafe {
1480 let m = &*model;
1481
1482 // Handle occurrence indicators at this level first
1483 match m.ocur as u32 {
1484 o if o == XML_ELEMENT_CONTENT_OPT as u32 => {
1485 // Optional: zero or one occurrence
1486 if names.is_empty() {
1487 return ContentModelResult::Valid;
1488 }
1489 return valid_content_model_inner(model, names);
1490 }
1491 o if o == XML_ELEMENT_CONTENT_MULT as u32 => {
1492 // Zero or more
1493 if names.is_empty() {
1494 return ContentModelResult::Valid;
1495 }
1496 return valid_content_model_zero_or_more(model, names);
1497 }
1498 o if o == XML_ELEMENT_CONTENT_PLUS as u32 => {
1499 // One or more
1500 if names.is_empty() {
1501 return ContentModelResult::Invalid;
1502 }
1503 return valid_content_model_one_or_more(model, names);
1504 }
1505 _ => {}
1506 }
1507
1508 valid_content_model_inner(model, names)
1509 }
1510}
1511
1512/// Validate content against a content model without considering occurrence.
1513///
1514/// # Safety
1515///
1516/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1517/// dereferenced to read `type_` and `name`, and its `c1`/`c2` children are
1518/// followed when non-NULL.
1519/// - Each entry in `names` must be NULL or a valid pointer to a
1520/// NUL-terminated `xmlChar` element name; non-NULL entries are compared
1521/// with `xml_strcmp`.
1522unsafe fn valid_content_model_inner(
1523 model: *mut _xmlElementContent,
1524 names: &[*const xmlChar],
1525) -> ContentModelResult {
1526 unsafe {
1527 let m = &*model;
1528
1529 match m.type_ as u32 {
1530 t if t == XML_ELEMENT_CONTENT_PCDATA as u32 => {
1531 // PCDATA: content must be empty (just text)
1532 if names.is_empty() {
1533 ContentModelResult::Valid
1534 } else {
1535 ContentModelResult::Invalid
1536 }
1537 }
1538 t if t == XML_ELEMENT_CONTENT_ELEMENT as u32 => {
1539 // Single element: must match exactly one element
1540 if names.len() != 1 {
1541 return ContentModelResult::Invalid;
1542 }
1543 if names[0].is_null() {
1544 return ContentModelResult::Invalid;
1545 }
1546 // Compare with model name
1547 if string::xml_strcmp(names[0], m.name) != 0 {
1548 return ContentModelResult::Invalid;
1549 }
1550 ContentModelResult::Valid
1551 }
1552 t if t == XML_ELEMENT_CONTENT_SEQ as u32 => {
1553 // Sequence: validate children in order
1554 valid_content_model_seq(m, names)
1555 }
1556 t if t == XML_ELEMENT_CONTENT_OR as u32 => {
1557 // Choice: one of the alternatives must match all names
1558 valid_content_model_or(m, names)
1559 }
1560 _ => ContentModelResult::Invalid,
1561 }
1562 }
1563}
1564
1565/// Validate content for zero-or-more occurrence.
1566///
1567/// # Safety
1568///
1569/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1570/// forwarded to `valid_content_model_inner`, which dereferences it.
1571/// - Each entry in `names` must be NULL or a valid pointer to a
1572/// NUL-terminated `xmlChar` element name.
1573unsafe fn valid_content_model_zero_or_more(
1574 model: *mut _xmlElementContent,
1575 names: &[*const xmlChar],
1576) -> ContentModelResult {
1577 // Zero or more: try each possible split
1578 let mut i = 0;
1579 while i <= names.len() {
1580 let consumed = &names[..i];
1581 let remaining = &names[i..];
1582
1583 let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1584 if consumed_valid == ContentModelResult::Valid {
1585 if remaining.is_empty() {
1586 return ContentModelResult::Valid;
1587 }
1588 // Try to match remaining with same model
1589 let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1590 if remaining_valid == ContentModelResult::Valid {
1591 return ContentModelResult::Valid;
1592 }
1593 }
1594
1595 i += 1;
1596 }
1597 ContentModelResult::Invalid
1598}
1599
1600/// Validate content for one-or-more occurrence.
1601///
1602/// # Safety
1603///
1604/// - `model` must be a valid pointer to an `_xmlElementContent`; it is
1605/// forwarded to `valid_content_model_inner`, which dereferences it.
1606/// - Each entry in `names` must be NULL or a valid pointer to a
1607/// NUL-terminated `xmlChar` element name.
1608unsafe fn valid_content_model_one_or_more(
1609 model: *mut _xmlElementContent,
1610 names: &[*const xmlChar],
1611) -> ContentModelResult {
1612 // One or more: must match at least once
1613 let mut i = 1;
1614 while i <= names.len() {
1615 let consumed = &names[..i];
1616 let remaining = &names[i..];
1617
1618 let consumed_valid = unsafe { valid_content_model_inner(model, consumed) };
1619 if consumed_valid == ContentModelResult::Valid {
1620 if remaining.is_empty() {
1621 return ContentModelResult::Valid;
1622 }
1623 let remaining_valid = unsafe { valid_content_model_zero_or_more(model, remaining) };
1624 if remaining_valid == ContentModelResult::Valid {
1625 return ContentModelResult::Valid;
1626 }
1627 }
1628
1629 i += 1;
1630 }
1631 ContentModelResult::Invalid
1632}
1633
1634/// Validate content against a sequence content model.
1635///
1636/// # Safety
1637///
1638/// - The `model` reference must point to a live `_xmlElementContent`.
1639/// - Non-NULL `c1`/`c2` children must be valid pointers to
1640/// `_xmlElementContent`; they are passed to `valid_content_model`.
1641/// - Each entry in `names` must be NULL or a valid pointer to a
1642/// NUL-terminated `xmlChar` element name.
1643unsafe fn valid_content_model_seq(
1644 model: &_xmlElementContent,
1645 names: &[*const xmlChar],
1646) -> ContentModelResult {
1647 // For a sequence, we need to split the names between c1 and c2
1648 // This is a simplified validation - full automata-based validation
1649 // would be more complex.
1650
1651 let c1 = model.c1;
1652 let c2 = model.c2;
1653
1654 if c1.is_null() && c2.is_null() {
1655 return ContentModelResult::Valid;
1656 }
1657
1658 if c1.is_null() {
1659 return unsafe { valid_content_model(c2, names) };
1660 }
1661
1662 if c2.is_null() {
1663 return unsafe { valid_content_model(c1, names) };
1664 }
1665
1666 // Try to split the names at each possible position
1667 // This implements a simple backtracking validator
1668 for split in 0..=names.len() {
1669 let left = &names[..split];
1670 let right = &names[split..];
1671
1672 let left_valid = unsafe { valid_content_model(c1, left) };
1673 if left_valid != ContentModelResult::Valid {
1674 continue;
1675 }
1676
1677 let right_valid = unsafe { valid_content_model(c2, right) };
1678 if right_valid == ContentModelResult::Valid {
1679 return ContentModelResult::Valid;
1680 }
1681 }
1682
1683 ContentModelResult::Invalid
1684}
1685
1686/// Validate content against a choice content model.
1687///
1688/// # Safety
1689///
1690/// - The `model` reference must point to a live `_xmlElementContent`.
1691/// - Non-NULL `c1`/`c2` children must be valid pointers to
1692/// `_xmlElementContent`; they are passed to `valid_content_model`.
1693/// - Each entry in `names` must be NULL or a valid pointer to a
1694/// NUL-terminated `xmlChar` element name.
1695unsafe fn valid_content_model_or(
1696 model: &_xmlElementContent,
1697 names: &[*const xmlChar],
1698) -> ContentModelResult {
1699 let c1 = model.c1;
1700 let c2 = model.c2;
1701
1702 if c1.is_null() && c2.is_null() {
1703 return ContentModelResult::Invalid;
1704 }
1705
1706 if !c1.is_null() {
1707 let result = unsafe { valid_content_model(c1, names) };
1708 if result == ContentModelResult::Valid {
1709 return ContentModelResult::Valid;
1710 }
1711 }
1712
1713 if !c2.is_null() {
1714 let result = unsafe { valid_content_model(c2, names) };
1715 if result == ContentModelResult::Valid {
1716 return ContentModelResult::Valid;
1717 }
1718 }
1719
1720 ContentModelResult::Invalid
1721}
1722
1723// ═══════════════════════════════════════════════════════════════════════════════
1724// Tests
1725// ═══════════════════════════════════════════════════════════════════════════════
1726
1727#[cfg(test)]
1728mod tests {
1729 use super::*;
1730
1731 use core::ffi::c_void;
1732 use core::ptr;
1733
1734 // ── Helpers ──────────────────────────────────────────────────────────
1735
1736 unsafe fn c_str(s: &[u8]) -> *const xmlChar {
1737 // Create a null-terminated xmlChar string
1738 let len = s.len();
1739 let buf = allocator::xmlMallocImpl(len + 1) as *mut xmlChar;
1740 assert!(!buf.is_null());
1741 ptr::copy_nonoverlapping(s.as_ptr(), buf, len);
1742 *buf.add(len) = 0;
1743 buf as *const xmlChar
1744 }
1745
1746 unsafe fn make_doc_and_dtd() -> (*mut _xmlDoc, *mut _xmlDtd) {
1747 let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1748 assert!(!doc.is_null());
1749 (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1750 (*doc).doc = doc;
1751 let dtd = create_int_subset(doc, c_str(b"root"), ptr::null(), ptr::null());
1752 assert!(!dtd.is_null());
1753 (doc, dtd)
1754 }
1755
1756 // ── DTD Access Tests ────────────────────────────────────────────────
1757
1758 /// Verify that `get_int_subset` accepts a NULL document.
1759 ///
1760 /// # Safety
1761 ///
1762 /// - NULL is allowed and is never dereferenced.
1763 #[test]
1764 fn test_get_int_subset_null() {
1765 {
1766 assert!(get_int_subset(ptr::null()).is_null());
1767 }
1768 }
1769
1770 /// Verify that `create_int_subset` attaches a DTD to a document.
1771 ///
1772 /// # Safety
1773 ///
1774 /// - `doc` and `dtd` from `make_doc_and_dtd` are heap-allocated structs
1775 /// that must be valid while their fields are read; the DTD is freed with
1776 /// `free_dtd` and the doc with `xmlFreeImpl`.
1777 #[test]
1778 fn test_create_int_subset() {
1779 unsafe {
1780 let (doc, dtd) = make_doc_and_dtd();
1781 assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1782 assert!(!(*dtd).name.is_null());
1783 assert_eq!((*doc).intSubset, dtd);
1784
1785 // Cleanup
1786 free_dtd(dtd);
1787 allocator::xmlFreeImpl(doc as *mut c_void);
1788 }
1789 }
1790
1791 /// Verify that `create_int_subset` with a NULL doc still allocates and
1792 /// returns an unattached DTD (upstream tree.c `xmlCreateIntSubset` —
1793 /// HOSTILE-ABI A37).
1794 ///
1795 /// # Safety
1796 ///
1797 /// - A NULL `doc` is allowed and is not dereferenced; the returned DTD
1798 /// is freed with `free_dtd`.
1799 #[test]
1800 fn test_create_int_subset_null_doc() {
1801 unsafe {
1802 let dtd = create_int_subset(ptr::null_mut(), c_str(b"root"), ptr::null(), ptr::null());
1803 assert!(!dtd.is_null());
1804 assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1805 assert!((*dtd).parent.is_null());
1806 assert!((*dtd).doc.is_null());
1807 free_dtd(dtd);
1808 }
1809 }
1810
1811 /// Verify that `new_dtd` creates a DTD node and attaches it to the doc.
1812 ///
1813 /// # Safety
1814 ///
1815 /// - `doc` is a heap-allocated `_xmlDoc` and the strings passed to
1816 /// `new_dtd` are NUL-terminated allocations that stay alive for the call.
1817 /// - `dtd` must be freed with `free_dtd` and the doc with `xmlFreeImpl`.
1818 #[test]
1819 fn test_new_dtd() {
1820 unsafe {
1821 let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1822 assert!(!doc.is_null());
1823 (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1824 (*doc).doc = doc;
1825
1826 let dtd = new_dtd(doc, c_str(b"test"), c_str(b"-//TEST//"), c_str(b"test.dtd"));
1827 assert!(!dtd.is_null());
1828 assert_eq!((*dtd).type_, XML_DTD_NODE as c_int);
1829 assert_eq!((*doc).intSubset, dtd);
1830
1831 free_dtd(dtd);
1832 allocator::xmlFreeImpl(doc as *mut c_void);
1833 }
1834 }
1835
1836 /// Verify that `new_dtd` works without a document.
1837 ///
1838 /// # Safety
1839 ///
1840 /// - The strings passed to `new_dtd` must be NUL-terminated and alive for
1841 /// the call; the returned DTD is freed with `free_dtd`.
1842 #[test]
1843 fn test_new_dtd_no_doc() {
1844 unsafe {
1845 let dtd = new_dtd(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1846 assert!(!dtd.is_null());
1847 free_dtd(dtd);
1848 }
1849 }
1850
1851 // ── Notation Tests ──────────────────────────────────────────────────
1852
1853 /// Verify adding and looking up a notation declaration.
1854 ///
1855 /// # Safety
1856 ///
1857 /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
1858 /// must be valid and alive for the calls; the DTD is freed with
1859 /// `free_dtd` and the doc with `xmlFreeImpl`.
1860 #[test]
1861 fn test_add_get_notation() {
1862 unsafe {
1863 let (doc, dtd) = make_doc_and_dtd();
1864 let name = c_str(b"note");
1865 let pubid = c_str(b"-//TEST//NOTATION");
1866 let sysid = c_str(b"note.ent");
1867
1868 let n = add_notation_decl(dtd, name, pubid, sysid);
1869 assert!(!n.is_null());
1870 assert_eq!(string::xml_strcmp((*n).name, name), 0);
1871
1872 // Lookup
1873 let found = get_notation_decl(dtd, name);
1874 assert_eq!(found, n);
1875
1876 // Lookup non-existent
1877 let not_found = get_notation_decl(dtd, c_str(b"nonexistent"));
1878 assert!(not_found.is_null());
1879
1880 free_dtd(dtd);
1881 allocator::xmlFreeImpl(doc as *mut c_void);
1882 }
1883 }
1884
1885 /// Verify that adding a notation to a NULL DTD returns NULL.
1886 ///
1887 /// # Safety
1888 ///
1889 /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
1890 /// buffer must be NUL-terminated.
1891 #[test]
1892 fn test_add_notation_null_dtd() {
1893 unsafe {
1894 let n = add_notation_decl(ptr::null_mut(), c_str(b"test"), ptr::null(), ptr::null());
1895 assert!(n.is_null());
1896 }
1897 }
1898
1899 /// Verify `copy_notation` deep-copies a notation declaration.
1900 ///
1901 /// # Safety
1902 ///
1903 /// - `n` from `add_notation_decl` must be a valid `_xmlNotation` while
1904 /// copied; the copy is freed with `free_notation`, the DTD with
1905 /// `free_dtd`, and the doc with `xmlFreeImpl`.
1906 #[test]
1907 fn test_copy_notation() {
1908 unsafe {
1909 let (doc, dtd) = make_doc_and_dtd();
1910 let name = c_str(b"note1");
1911 let pubid = c_str(b"public");
1912 let sysid = c_str(b"system");
1913
1914 let n = add_notation_decl(dtd, name, pubid, sysid);
1915 assert!(!n.is_null());
1916
1917 let copy = copy_notation(n);
1918 assert!(!copy.is_null());
1919 assert_ne!(copy, n);
1920 assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1921 assert_eq!(string::xml_strcmp((*copy).PublicID, pubid), 0);
1922 assert_eq!(string::xml_strcmp((*copy).SystemID, sysid), 0);
1923
1924 free_notation(copy);
1925 free_dtd(dtd);
1926 allocator::xmlFreeImpl(doc as *mut c_void);
1927 }
1928 }
1929
1930 /// Verify that `copy_notation` accepts a NULL pointer.
1931 ///
1932 /// # Safety
1933 ///
1934 /// - NULL is allowed and is not dereferenced.
1935 #[test]
1936 fn test_copy_notation_null() {
1937 unsafe {
1938 assert!(copy_notation(ptr::null_mut()).is_null());
1939 }
1940 }
1941
1942 /// Verify that `free_notation` accepts a NULL pointer.
1943 ///
1944 /// # Safety
1945 ///
1946 /// - NULL is allowed and is not dereferenced.
1947 #[test]
1948 fn test_free_notation_null() {
1949 unsafe {
1950 free_notation(ptr::null_mut()); // Should not crash
1951 }
1952 }
1953
1954 // ── Content Model Tests ─────────────────────────────────────────────
1955
1956 /// Verify creating and freeing a content model.
1957 ///
1958 /// # Safety
1959 ///
1960 /// - The name buffer passed to `create_content_model` must be
1961 /// NUL-terminated and alive for the call; the returned model is freed
1962 /// with `free_content_model`.
1963 #[test]
1964 fn test_create_free_content_model() {
1965 unsafe {
1966 let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
1967 assert!(!cm.is_null());
1968 assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
1969 assert_eq!((*cm).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
1970
1971 free_content_model(cm);
1972 }
1973 }
1974
1975 /// Verify creating a PCDATA content model.
1976 ///
1977 /// # Safety
1978 ///
1979 /// - The returned model must be valid while read and is freed with
1980 /// `free_content_model`.
1981 #[test]
1982 fn test_create_content_model_pcdata() {
1983 unsafe {
1984 let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
1985 assert!(!cm.is_null());
1986 assert_eq!((*cm).type_, XML_ELEMENT_CONTENT_PCDATA as c_int);
1987 free_content_model(cm);
1988 }
1989 }
1990
1991 /// Verify `copy_content_model` deep-copies a content model.
1992 ///
1993 /// # Safety
1994 ///
1995 /// - `cm` must be a valid `_xmlElementContent` while copied; both models
1996 /// are freed with `free_content_model`.
1997 #[test]
1998 fn test_copy_content_model() {
1999 unsafe {
2000 let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2001 assert!(!cm.is_null());
2002
2003 let copy = copy_content_model(cm);
2004 assert!(!copy.is_null());
2005 assert_ne!(copy, cm);
2006 assert_eq!((*copy).type_, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2007 assert_eq!((*copy).ocur, XML_ELEMENT_CONTENT_ONCE as c_int);
2008 assert_eq!(string::xml_strcmp((*copy).name, (*cm).name), 0);
2009
2010 free_content_model(cm);
2011 free_content_model(copy);
2012 }
2013 }
2014
2015 /// Verify that `copy_content_model` accepts a NULL pointer.
2016 ///
2017 /// # Safety
2018 ///
2019 /// - NULL is allowed and is not dereferenced.
2020 #[test]
2021 fn test_copy_content_model_null() {
2022 unsafe {
2023 assert!(copy_content_model(ptr::null_mut()).is_null());
2024 }
2025 }
2026
2027 /// Verify that `free_content_model` accepts a NULL pointer.
2028 ///
2029 /// # Safety
2030 ///
2031 /// - NULL is allowed and is not dereferenced.
2032 #[test]
2033 fn test_free_content_model_null() {
2034 unsafe {
2035 free_content_model(ptr::null_mut()); // Should not crash
2036 }
2037 }
2038
2039 /// Verify creating a sequence content model with two children.
2040 ///
2041 /// # Safety
2042 ///
2043 /// - `c1`, `c2`, and `seq` must be valid `_xmlElementContent` pointers
2044 /// whose `parent`/`c1`/`c2` links are consistent before `free_content_model`
2045 /// walks them.
2046 #[test]
2047 fn test_create_sequence_content_model() {
2048 unsafe {
2049 let c1 = create_content_model(c_str(b"a"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2050 let c2 = create_content_model(c_str(b"b"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2051 let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
2052 assert!(!seq.is_null());
2053 (*seq).c1 = c1;
2054 (*seq).c2 = c2;
2055 (*c1).parent = seq;
2056 (*c2).parent = seq;
2057
2058 free_content_model(seq);
2059 }
2060 }
2061
2062 // ── Element Declaration Tests ───────────────────────────────────────
2063
2064 /// Verify adding and looking up an element declaration.
2065 ///
2066 /// # Safety
2067 ///
2068 /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2069 /// must be valid and alive for the calls; the DTD is freed with
2070 /// `free_dtd` and the doc with `xmlFreeImpl`.
2071 #[test]
2072 fn test_add_get_element() {
2073 unsafe {
2074 let (doc, dtd) = make_doc_and_dtd();
2075 let name = c_str(b"myElement");
2076
2077 let elem =
2078 add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
2079 assert!(!elem.is_null());
2080 assert_eq!((*elem).etype, XML_ELEMENT_TYPE_EMPTY as c_int);
2081
2082 let found = get_element_decl(dtd, name);
2083 assert_eq!(found, elem);
2084
2085 let not_found = get_element_decl(dtd, c_str(b"nonexistent"));
2086 assert!(not_found.is_null());
2087
2088 free_dtd(dtd);
2089 allocator::xmlFreeImpl(doc as *mut c_void);
2090 }
2091 }
2092
2093 /// Verify that adding a duplicate element declaration returns the
2094 /// existing declaration unchanged.
2095 ///
2096 /// # Safety
2097 ///
2098 /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated name buffer must
2099 /// be valid and alive for the calls; the DTD is freed with `free_dtd`
2100 /// and the doc with `xmlFreeImpl`.
2101 #[test]
2102 fn test_add_element_duplicate() {
2103 unsafe {
2104 let (doc, dtd) = make_doc_and_dtd();
2105 let name = c_str(b"dup");
2106
2107 let e1 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_EMPTY as c_int, ptr::null_mut());
2108 assert!(!e1.is_null());
2109
2110 let e2 = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ANY as c_int, ptr::null_mut());
2111 assert_eq!(e1, e2); // Same pointer returned
2112 assert_eq!((*e2).type_, XML_ELEMENT_DECL as c_int); // node type
2113 assert_eq!((*e2).etype, XML_ELEMENT_TYPE_EMPTY as c_int); // Still empty
2114
2115 free_dtd(dtd);
2116 allocator::xmlFreeImpl(doc as *mut c_void);
2117 }
2118 }
2119
2120 /// Verify that adding an element to a NULL DTD returns NULL.
2121 ///
2122 /// # Safety
2123 ///
2124 /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
2125 /// buffer must be NUL-terminated.
2126 #[test]
2127 fn test_add_element_null_dtd() {
2128 unsafe {
2129 let elem = add_element_decl(
2130 ptr::null_mut(),
2131 c_str(b"test"),
2132 XML_ELEMENT_TYPE_EMPTY as c_int,
2133 ptr::null_mut(),
2134 );
2135 assert!(elem.is_null());
2136 }
2137 }
2138
2139 /// Verify `copy_element` deep-copies an element declaration.
2140 ///
2141 /// # Safety
2142 ///
2143 /// - `elem` and its content model must be valid while copied; the copy is
2144 /// freed with `free_element`, the DTD with `free_dtd`, and the doc with
2145 /// `xmlFreeImpl`.
2146 #[test]
2147 fn test_copy_element() {
2148 unsafe {
2149 let (doc, dtd) = make_doc_and_dtd();
2150 let name = c_str(b"source");
2151 let cm = create_content_model(c_str(b"child"), XML_ELEMENT_CONTENT_ELEMENT as c_int);
2152
2153 let elem = add_element_decl(dtd, name, XML_ELEMENT_TYPE_ELEMENT as c_int, cm);
2154 assert!(!elem.is_null());
2155
2156 let copy = copy_element(elem);
2157 assert!(!copy.is_null());
2158 assert_ne!(copy, elem);
2159 assert_eq!((*copy).type_, XML_ELEMENT_DECL as c_int);
2160 assert_eq!((*copy).etype, XML_ELEMENT_TYPE_ELEMENT as c_int);
2161 assert_eq!(string::xml_strcmp((*copy).name, name), 0);
2162 assert!(!(*copy).content.is_null());
2163 assert_ne!((*copy).content, cm);
2164
2165 free_element(copy);
2166 free_dtd(dtd);
2167 allocator::xmlFreeImpl(doc as *mut c_void);
2168 }
2169 }
2170
2171 /// Verify that `free_element` accepts a NULL pointer.
2172 ///
2173 /// # Safety
2174 ///
2175 /// - NULL is allowed and is not dereferenced.
2176 #[test]
2177 fn test_free_element_null() {
2178 unsafe {
2179 free_element(ptr::null_mut()); // Should not crash
2180 }
2181 }
2182
2183 // ── Attribute Declaration Tests ─────────────────────────────────────
2184
2185 /// Verify adding and looking up an attribute declaration.
2186 ///
2187 /// # Safety
2188 ///
2189 /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2190 /// must be valid and alive for the calls; the DTD is freed with
2191 /// `free_dtd` and the doc with `xmlFreeImpl`.
2192 #[test]
2193 fn test_add_get_attribute() {
2194 unsafe {
2195 let (doc, dtd) = make_doc_and_dtd();
2196 let elem_name = c_str(b"elem");
2197 let attr_name = c_str(b"attr1");
2198
2199 let elem = add_element_decl(
2200 dtd,
2201 elem_name,
2202 XML_ELEMENT_TYPE_EMPTY as c_int,
2203 ptr::null_mut(),
2204 );
2205 assert!(!elem.is_null());
2206
2207 let attr = add_attribute_decl(
2208 dtd,
2209 elem,
2210 attr_name,
2211 ptr::null(),
2212 XML_ATTRIBUTE_CDATA as c_int,
2213 XML_ATTRIBUTE_IMPLIED as c_int,
2214 ptr::null(),
2215 ptr::null_mut(),
2216 );
2217 assert!(!attr.is_null());
2218 assert_eq!((*attr).atype, XML_ATTRIBUTE_CDATA as c_int);
2219 assert_eq!((*attr).def, XML_ATTRIBUTE_IMPLIED as c_int);
2220
2221 // Lookup by element + attribute name
2222 let found = get_attribute_decl(dtd, elem, attr_name, 0);
2223 assert_eq!(found, attr);
2224
2225 // Lookup non-existent
2226 let not_found = get_attribute_decl(dtd, elem, c_str(b"nonexistent"), 0);
2227 assert!(not_found.is_null());
2228
2229 free_dtd(dtd);
2230 allocator::xmlFreeImpl(doc as *mut c_void);
2231 }
2232 }
2233
2234 /// Verify adding an attribute declaration with a default value.
2235 ///
2236 /// # Safety
2237 ///
2238 /// - `dtd` from `make_doc_and_dtd` and the NUL-terminated `c_str` buffers
2239 /// must be valid and alive for the calls; the DTD is freed with
2240 /// `free_dtd` and the doc with `xmlFreeImpl`.
2241 #[test]
2242 fn test_add_attribute_with_default() {
2243 unsafe {
2244 let (doc, dtd) = make_doc_and_dtd();
2245 let elem_name = c_str(b"elem");
2246 let attr_name = c_str(b"color");
2247 let default_val = c_str(b"red");
2248
2249 let elem = add_element_decl(
2250 dtd,
2251 elem_name,
2252 XML_ELEMENT_TYPE_EMPTY as c_int,
2253 ptr::null_mut(),
2254 );
2255
2256 let attr = add_attribute_decl(
2257 dtd,
2258 elem,
2259 attr_name,
2260 ptr::null(),
2261 XML_ATTRIBUTE_CDATA as c_int,
2262 XML_ATTRIBUTE_FIXED as c_int,
2263 default_val,
2264 ptr::null_mut(),
2265 );
2266 assert!(!attr.is_null());
2267 assert_eq!((*attr).def, XML_ATTRIBUTE_FIXED as c_int);
2268 assert_eq!(string::xml_strcmp((*attr).defaultValue, default_val), 0);
2269
2270 free_dtd(dtd);
2271 allocator::xmlFreeImpl(doc as *mut c_void);
2272 }
2273 }
2274
2275 /// Verify adding an attribute with an enumeration of values.
2276 ///
2277 /// # Safety
2278 ///
2279 /// - The `_xmlEnumeration` chain built with `xmlMallocZero` must consist
2280 /// of valid structs with NUL-terminated duplicated names before it is
2281 /// handed to `add_attribute_decl`; the DTD owns and frees it via
2282 /// `free_dtd`, and the doc is freed with `xmlFreeImpl`.
2283 #[test]
2284 fn test_add_attribute_enumeration() {
2285 unsafe {
2286 let (doc, dtd) = make_doc_and_dtd();
2287 let elem_name = c_str(b"elem");
2288 let attr_name = c_str(b"size");
2289
2290 // Build enumeration: small, medium, large
2291 let v3 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2292 as *mut _xmlEnumeration;
2293 (*v3).name = string::xml_strdup(c_str(b"large"));
2294 let v2 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2295 as *mut _xmlEnumeration;
2296 (*v2).name = string::xml_strdup(c_str(b"medium"));
2297 (*v2).next = v3;
2298 let v1 = allocator::xmlMallocZero(size_of::<_xmlEnumeration>() as usize)
2299 as *mut _xmlEnumeration;
2300 (*v1).name = string::xml_strdup(c_str(b"small"));
2301 (*v1).next = v2;
2302
2303 let elem = add_element_decl(
2304 dtd,
2305 elem_name,
2306 XML_ELEMENT_TYPE_EMPTY as c_int,
2307 ptr::null_mut(),
2308 );
2309 let attr = add_attribute_decl(
2310 dtd,
2311 elem,
2312 attr_name,
2313 ptr::null(),
2314 XML_ATTRIBUTE_ENUMERATION as c_int,
2315 XML_ATTRIBUTE_REQUIRED as c_int,
2316 ptr::null(),
2317 v1,
2318 );
2319 assert!(!attr.is_null());
2320 assert_eq!((*attr).atype, XML_ATTRIBUTE_ENUMERATION as c_int);
2321
2322 free_dtd(dtd);
2323 allocator::xmlFreeImpl(doc as *mut c_void);
2324 }
2325 }
2326
2327 /// Verify that adding an attribute to a NULL DTD returns NULL.
2328 ///
2329 /// # Safety
2330 ///
2331 /// - Passing a NULL `dtd` is allowed and is not dereferenced; the name
2332 /// buffer must be NUL-terminated.
2333 #[test]
2334 fn test_add_attribute_null_dtd() {
2335 unsafe {
2336 let attr = add_attribute_decl(
2337 ptr::null_mut(),
2338 ptr::null_mut(),
2339 c_str(b"test"),
2340 ptr::null(),
2341 XML_ATTRIBUTE_CDATA as c_int,
2342 XML_ATTRIBUTE_IMPLIED as c_int,
2343 ptr::null(),
2344 ptr::null_mut(),
2345 );
2346 assert!(attr.is_null());
2347 }
2348 }
2349
2350 /// Verify `copy_attribute_decl` deep-copies an attribute declaration.
2351 ///
2352 /// # Safety
2353 ///
2354 /// - `attr` must be a valid `_xmlAttribute` while copied; the copy is
2355 /// freed with `free_attribute`, the DTD with `free_dtd`, and the doc
2356 /// with `xmlFreeImpl`.
2357 #[test]
2358 fn test_copy_attribute() {
2359 unsafe {
2360 let (doc, dtd) = make_doc_and_dtd();
2361 let elem_name = c_str(b"elem");
2362 let attr_name = c_str(b"id");
2363 let default_val = c_str(b"default");
2364
2365 let elem = add_element_decl(
2366 dtd,
2367 elem_name,
2368 XML_ELEMENT_TYPE_EMPTY as c_int,
2369 ptr::null_mut(),
2370 );
2371 let attr = add_attribute_decl(
2372 dtd,
2373 elem,
2374 attr_name,
2375 ptr::null(),
2376 XML_ATTRIBUTE_ID as c_int,
2377 XML_ATTRIBUTE_IMPLIED as c_int,
2378 default_val,
2379 ptr::null_mut(),
2380 );
2381 assert!(!attr.is_null());
2382
2383 let copy = copy_attribute_decl(attr);
2384 assert!(!copy.is_null());
2385 assert_ne!(copy, attr);
2386 assert_eq!((*copy).atype, XML_ATTRIBUTE_ID as c_int);
2387 assert_eq!(string::xml_strcmp((*copy).name, attr_name), 0);
2388
2389 free_attribute(copy);
2390 free_dtd(dtd);
2391 allocator::xmlFreeImpl(doc as *mut c_void);
2392 }
2393 }
2394
2395 /// Verify that `free_attribute` accepts a NULL pointer.
2396 ///
2397 /// # Safety
2398 ///
2399 /// - NULL is allowed and is not dereferenced.
2400 #[test]
2401 fn test_free_attribute_null() {
2402 unsafe {
2403 free_attribute(ptr::null_mut()); // Should not crash
2404 }
2405 }
2406
2407 // ── Content Model Validation Tests ──────────────────────────────────
2408
2409 /// Verify that `valid_content_model` rejects a NULL model.
2410 ///
2411 /// # Safety
2412 ///
2413 /// - Passing a NULL `model` is allowed and is not dereferenced.
2414 #[test]
2415 fn test_valid_content_model_null() {
2416 unsafe {
2417 assert_eq!(
2418 valid_content_model(ptr::null_mut(), &[]),
2419 ContentModelResult::Invalid
2420 );
2421 }
2422 }
2423
2424 /// Verify PCDATA content-model validation.
2425 ///
2426 /// # Safety
2427 ///
2428 /// - `cm` must be a valid `_xmlElementContent` while validated; the name
2429 /// buffer must be NUL-terminated and alive for the call. The model is
2430 /// freed with `free_content_model`.
2431 #[test]
2432 fn test_valid_content_model_pcdata() {
2433 unsafe {
2434 let cm = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_PCDATA as c_int);
2435 assert!(!cm.is_null());
2436
2437 // Empty content is valid for PCDATA
2438 assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Valid);
2439
2440 // Non-empty content is invalid for PCDATA
2441 let name = c_str(b"child");
2442 assert_eq!(
2443 valid_content_model(cm, &[name]),
2444 ContentModelResult::Invalid
2445 );
2446
2447 free_content_model(cm);
2448 }
2449 }
2450
2451 /// Verify single-element content-model validation.
2452 ///
2453 /// # Safety
2454 ///
2455 /// - `cm` must be a valid `_xmlElementContent` and the name buffers
2456 /// NUL-terminated and alive for the calls; the model is freed with
2457 /// `free_content_model`.
2458 #[test]
2459 fn test_valid_content_model_element() {
2460 unsafe {
2461 let child_name = c_str(b"child");
2462 let cm = create_content_model(child_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2463 assert!(!cm.is_null());
2464
2465 // Correct element
2466 assert_eq!(
2467 valid_content_model(cm, &[child_name]),
2468 ContentModelResult::Valid
2469 );
2470
2471 // Wrong element
2472 let other = c_str(b"other");
2473 assert_eq!(
2474 valid_content_model(cm, &[other]),
2475 ContentModelResult::Invalid
2476 );
2477
2478 // Too many elements
2479 assert_eq!(
2480 valid_content_model(cm, &[child_name, child_name]),
2481 ContentModelResult::Invalid
2482 );
2483
2484 // Empty
2485 assert_eq!(valid_content_model(cm, &[]), ContentModelResult::Invalid);
2486
2487 free_content_model(cm);
2488 }
2489 }
2490
2491 /// Verify sequence content-model validation.
2492 ///
2493 /// # Safety
2494 ///
2495 /// - `c1`, `c2`, and `seq` must be valid, linked `_xmlElementContent`
2496 /// structs and the name buffers NUL-terminated for the calls; the
2497 /// sequence is freed with `free_content_model`.
2498 #[test]
2499 fn test_valid_content_model_seq() {
2500 unsafe {
2501 let a_name = c_str(b"a");
2502 let b_name = c_str(b"b");
2503
2504 let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2505 let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2506 let seq = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_SEQ as c_int);
2507 (*seq).c1 = c1;
2508 (*seq).c2 = c2;
2509 (*c1).parent = seq;
2510 (*c2).parent = seq;
2511
2512 // Correct sequence
2513 assert_eq!(
2514 valid_content_model(seq, &[a_name, b_name]),
2515 ContentModelResult::Valid
2516 );
2517
2518 // Wrong order
2519 assert_eq!(
2520 valid_content_model(seq, &[b_name, a_name]),
2521 ContentModelResult::Invalid
2522 );
2523
2524 // Missing element
2525 assert_eq!(
2526 valid_content_model(seq, &[a_name]),
2527 ContentModelResult::Invalid
2528 );
2529
2530 free_content_model(seq);
2531 }
2532 }
2533
2534 /// Verify choice content-model validation.
2535 ///
2536 /// # Safety
2537 ///
2538 /// - `c1`, `c2`, and `choice` must be valid, linked `_xmlElementContent`
2539 /// structs and the name buffers NUL-terminated for the calls; the
2540 /// choice is freed with `free_content_model`.
2541 #[test]
2542 fn test_valid_content_model_or() {
2543 unsafe {
2544 let a_name = c_str(b"a");
2545 let b_name = c_str(b"b");
2546
2547 let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2548 let c2 = create_content_model(b_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2549 let choice = create_content_model(ptr::null(), XML_ELEMENT_CONTENT_OR as c_int);
2550 (*choice).c1 = c1;
2551 (*choice).c2 = c2;
2552 (*c1).parent = choice;
2553 (*c2).parent = choice;
2554
2555 // First alternative
2556 assert_eq!(
2557 valid_content_model(choice, &[a_name]),
2558 ContentModelResult::Valid
2559 );
2560
2561 // Second alternative
2562 assert_eq!(
2563 valid_content_model(choice, &[b_name]),
2564 ContentModelResult::Valid
2565 );
2566
2567 // Neither
2568 let other = c_str(b"other");
2569 assert_eq!(
2570 valid_content_model(choice, &[other]),
2571 ContentModelResult::Invalid
2572 );
2573
2574 free_content_model(choice);
2575 }
2576 }
2577
2578 /// Verify optional-occurrence content-model validation.
2579 ///
2580 /// # Safety
2581 ///
2582 /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2583 /// NUL-terminated for the calls; the model is freed with
2584 /// `free_content_model`.
2585 #[test]
2586 fn test_valid_content_model_optional() {
2587 unsafe {
2588 let a_name = c_str(b"a");
2589
2590 let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2591 (*c1).ocur = XML_ELEMENT_CONTENT_OPT as c_int;
2592
2593 // Empty is valid for optional
2594 assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2595
2596 // One is valid
2597 assert_eq!(
2598 valid_content_model(c1, &[a_name]),
2599 ContentModelResult::Valid
2600 );
2601
2602 free_content_model(c1);
2603 }
2604 }
2605
2606 /// Verify zero-or-more content-model validation.
2607 ///
2608 /// # Safety
2609 ///
2610 /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2611 /// NUL-terminated for the calls; the model is freed with
2612 /// `free_content_model`.
2613 #[test]
2614 fn test_valid_content_model_zero_or_more() {
2615 unsafe {
2616 let a_name = c_str(b"a");
2617
2618 let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2619 (*c1).ocur = XML_ELEMENT_CONTENT_MULT as c_int;
2620
2621 // Empty is valid
2622 assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Valid);
2623
2624 // One is valid
2625 assert_eq!(
2626 valid_content_model(c1, &[a_name]),
2627 ContentModelResult::Valid
2628 );
2629
2630 // Multiple is valid
2631 assert_eq!(
2632 valid_content_model(c1, &[a_name, a_name, a_name]),
2633 ContentModelResult::Valid
2634 );
2635
2636 free_content_model(c1);
2637 }
2638 }
2639
2640 /// Verify one-or-more content-model validation.
2641 ///
2642 /// # Safety
2643 ///
2644 /// - `c1` must be a valid `_xmlElementContent` and the name buffer
2645 /// NUL-terminated for the calls; the model is freed with
2646 /// `free_content_model`.
2647 #[test]
2648 fn test_valid_content_model_one_or_more() {
2649 unsafe {
2650 let a_name = c_str(b"a");
2651
2652 let c1 = create_content_model(a_name, XML_ELEMENT_CONTENT_ELEMENT as c_int);
2653 (*c1).ocur = XML_ELEMENT_CONTENT_PLUS as c_int;
2654
2655 // Empty is invalid
2656 assert_eq!(valid_content_model(c1, &[]), ContentModelResult::Invalid);
2657
2658 // One is valid
2659 assert_eq!(
2660 valid_content_model(c1, &[a_name]),
2661 ContentModelResult::Valid
2662 );
2663
2664 // Multiple is valid
2665 assert_eq!(
2666 valid_content_model(c1, &[a_name, a_name]),
2667 ContentModelResult::Valid
2668 );
2669
2670 free_content_model(c1);
2671 }
2672 }
2673}