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