libxml_rs/xml/entities/mod.rs
1//! Entity handling (§24, §85 Phase 6).
2//!
3//! General entities, parameter entities, external entities, entity
4//! substitution, entity references, security limits, recursive entities,
5//! expansion limits.
6//!
7//! # Upstream contract
8//!
9//! Mirrors upstream entities.c and the entity paths of parser.c
10//! (SRC-LIBXML2-2.15.0, oracle tree `oracle/historical/src/libxml2-2.15.0/`):
11//! xmlAddEntity, xmlGetEntity, xmlGetPredefinedEntity, xmlNewReference,
12//! entity content caching and the expansion limits. Parity target: the system
13//! libxml2 2.15.3 oracle.
14//!
15//! # Conceptual behavior
16//!
17//! General entities, parameter entities, external entities, entity
18//! substitution, entity references, security limits, recursive entities and
19//! expansion limits. The entity model parses a referenced entity content once
20//! into ent->children (XML_ENT_PARSED / XML_ENT_EXPANDING flags) and reuses
21//! it — structural re-expansion is impossible by construction.
22//!
23//! # Ownership & safety invariants
24//!
25//! Ownership: entity declarations are owned by the DTD hash tables
26//! (entities/pentities); ent->children nodes are owned by the declaration and
27//! freed with the DTD; entity-ref nodes share content with the entity
28//! (xmlNewReference semantics) and must never be freed separately (R-000164).
29//! SAFETY: the expansion guards make entity processing loop-free.
30//!
31//! # Historical quirks & epochs
32//!
33//! Security epochs: CVE-2014-3660 (billion laughs) fix be2a7eda and its
34//! regression fix 72a46a51 (SEC-0006) bounded expansion; CVE-2013-2877
35//! (SEC-0004) added loop detection; the 2015 batch (SEC-0008: 69030714,
36//! f1063fdb) fixed entity-boundary bugs; the recursion-depth increments came
37//! from commit 8f30bdff (2016, SEC-0009). XML_ENTITY_CONTENT_DEPTH_MAX = 32
38//! and XML_ENTITY_CONTENT_EXPANSION_MAX = 1,000,000 follow upstream.
39//!
40//! # Deliberate oddities
41//!
42//! Deliberate oddities: the amplification guard fires unconditionally with no
43//! XML_PARSE_HUGE bypass; unloadable external entities fail silently
44//! (xmlCtxtParseEntity) rather than raising undeclared-entity errors;
45//! predefined entities (amp/lt/gt/quot/apos) substitute unconditionally
46//! regardless of XML_PARSE_NOENT.
47//!
48//! # Proving courts
49//!
50//! PARSER-ENTITY-* court family, SECURITY-LIMITS probe (amplification sweep
51//! L4..L9 x 10 matches the oracle on every boundary), TREE-001, CLI-XMLLINT-
52//! 0033/0034 and `cargo test --lib`.
53//!
54//! # Tempting simplifications that would break parity
55//!
56//! Not caching entity content would re-parse per reference (exponential
57//! blowup — the vulnerable pre-CVE behavior); skipping the XML_ENT_EXPANDING
58//! re-entry check would loop on a self-referential entity (CVE-2013-2877). Do
59//! not drop the silent-failure path for unloadable external entities — the
60//! NONET oracle behavior depends on it (SD-004).
61//!
62//! # Safety
63//!
64//! - The module-level `unsafe impl Sync for SyncPtr` is only instantiated in
65//! this module as the `static PREDEFINED_ENTITIES` array of `_xmlEntity`
66//! values. That static and the byte-string literals it points to are
67//! immutable after initialization and are only ever read (never mutated or
68//! freed), so sharing references to it across threads cannot cause data
69//! races or use-after-free.
70
71use core::ffi::c_void;
72use core::mem::size_of;
73use core::ptr;
74use std::os::raw::{c_char, c_int, c_uint, c_ulong};
75
76use crate::abi::allocator;
77use crate::abi::structs::*;
78use crate::abi::types::xmlElementType::*;
79use crate::abi::types::xmlEntityType::*;
80use crate::abi::types::*;
81use crate::xml::hash;
82use crate::xml::string;
83
84// ═══════════════════════════════════════════════════════════════════════════════
85// Constants
86// ═══════════════════════════════════════════════════════════════════════════════
87
88/// Maximum entity recursion depth.
89///
90/// # UPSTREAM-PARITY
91///
92/// libxml2 uses XML_ENTITY_CONTENT_DEPTH_MAX (default 32).
93pub const XML_ENTITY_CONTENT_DEPTH_MAX: c_int = 32;
94
95/// Maximum entity expansion size (in bytes).
96///
97/// # UPSTREAM-PARITY
98///
99/// libxml2 uses XML_ENTITY_CONTENT_EXPANSION_MAX (default 1,000,000).
100pub const XML_ENTITY_CONTENT_EXPANSION_MAX: c_uint = 1_000_000;
101
102// ═══════════════════════════════════════════════════════════════════════════════
103// Entity Declaration Functions
104// ═══════════════════════════════════════════════════════════════════════════════
105
106/// Add an entity declaration to a DTD.
107///
108/// # UPSTREAM-PARITY
109///
110/// Add an entity declaration to a DTD's hash table (DTD-level helper).
111///
112/// # UPSTREAM-PARITY
113///
114/// This is the historical tree.c `xmlAddEntity` core (dtd-level add) and
115/// backs the candidate's `xmlAddDocEntity`/`xmlAddDtdEntity` and the parser
116/// entity-declaration path. The exported `xmlAddEntity` (entities.h, 2.15)
117/// is the document-level `add_entity_doc` with the int/error-code contract
118/// (R-000176).
119///
120/// Adds an entity to the appropriate hash table in the DTD based on the
121/// entity type (general entities go to `entities`, parameter entities to
122/// `pentities`).
123///
124/// If an entity with the same name already exists, the existing entity
125/// is returned and no new entity is created.
126///
127/// # SAFETY
128///
129/// - `dtd` must be a valid pointer to an _xmlDtd, or NULL.
130/// - `name` must be a valid null-terminated string.
131/// - `ExternalID`, `SystemID`, `content` may be NULL.
132pub unsafe fn add_entity(
133 dtd: *mut _xmlDtd,
134 name: *const xmlChar,
135 etype: c_int,
136 ExternalID: *const xmlChar,
137 SystemID: *const xmlChar,
138 content: *const xmlChar,
139) -> *mut _xmlEntity {
140 unsafe { add_entity_impl(dtd, name, etype, ExternalID, SystemID, content, ptr::null()) }
141}
142
143/// Like `add_entity`, but also records the ORIGINAL declaration text on the
144/// entity (`orig`). UPSTREAM-PARITY (parser.c xmlParseEntityDecl): internal
145/// entities parsed from a DTD keep the raw source text of their value in
146/// `ent->orig`, and xmlsave.c `xmlBufDumpEntityDecl` prints `orig` verbatim
147/// (only `"` escaped) when present — falling back to the content path (which
148/// additionally escapes `%` to `%`) only for entities without `orig`
149/// (php bug67081: `<!ENTITY % attrs "%coreattrs;">` must round-trip with a
150/// raw `%`).
151///
152/// # SAFETY
153///
154/// - Same as `add_entity`; `orig` may be NULL.
155pub unsafe fn add_entity_with_orig(
156 dtd: *mut _xmlDtd,
157 name: *const xmlChar,
158 etype: c_int,
159 ExternalID: *const xmlChar,
160 SystemID: *const xmlChar,
161 content: *const xmlChar,
162 orig: *const xmlChar,
163) -> *mut _xmlEntity {
164 unsafe { add_entity_impl(dtd, name, etype, ExternalID, SystemID, content, orig) }
165}
166
167#[allow(clippy::too_many_arguments)]
168unsafe fn add_entity_impl(
169 dtd: *mut _xmlDtd,
170 name: *const xmlChar,
171 etype: c_int,
172 ExternalID: *const xmlChar,
173 SystemID: *const xmlChar,
174 content: *const xmlChar,
175 orig: *const xmlChar,
176) -> *mut _xmlEntity {
177 if dtd.is_null() || name.is_null() {
178 return ptr::null_mut();
179 }
180
181 unsafe {
182 let d = &*dtd;
183
184 // Determine which hash table to use
185 let is_param = is_parameter_entity(etype);
186 // UPSTREAM-PARITY (entities.c xmlAddEntity): the table is created
187 // lazily on first use.
188 if is_param {
189 if (*dtd).pentities.is_null() {
190 (*dtd).pentities = hash::hash_create(8) as *mut c_void;
191 }
192 } else if (*dtd).entities.is_null() {
193 (*dtd).entities = hash::hash_create(8) as *mut c_void;
194 }
195 let hash_table = if is_param {
196 d.pentities as *mut hash::HashTable
197 } else {
198 d.entities as *mut hash::HashTable
199 };
200
201 // Check if entity already exists
202 let existing = hash::hash_lookup(hash_table, name);
203 if !existing.is_null() {
204 return existing as *mut _xmlEntity;
205 }
206
207 // SAFETY: Allocate zero-initialized memory for the entity.
208 let entity = allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
209 if entity.is_null() {
210 return ptr::null_mut();
211 }
212
213 (*entity).type_ = XML_ENTITY_DECL as c_int;
214 (*entity).name = string::xml_strdup(name);
215 (*entity).etype = etype;
216 (*entity).ExternalID = string::xml_strdup(ExternalID);
217 (*entity).SystemID = string::xml_strdup(SystemID);
218 (*entity).content = string::xml_strdup(content);
219 (*entity).orig = if orig.is_null() {
220 ptr::null_mut()
221 } else {
222 string::xml_strdup(orig)
223 };
224 (*entity).length = if content.is_null() {
225 0
226 } else {
227 string::xml_strlen(content) as c_int
228 };
229 (*entity).flags = 0;
230 (*entity).expandedSize = 0;
231 (*entity).URI = ptr::null_mut();
232 (*entity).parent = dtd;
233 (*entity).doc = d.doc;
234 (*entity).nexte = ptr::null_mut();
235 (*entity).owner = 0;
236
237 // Add to hash table
238 let ret = hash::hash_add_entry(hash_table, name, entity as *mut c_void);
239 if ret != 0 {
240 // Failed to add
241 free_entity_internal(entity, false);
242 return ptr::null_mut();
243 }
244
245 // UPSTREAM-PARITY (entities.c xmlAddDocEntity/xmlAddDtdEntity
246 // "Link it to the DTD"): the entity decl is a child node of the DTD.
247 if (*dtd).last.is_null() {
248 (*dtd).children = entity as *mut _xmlNode;
249 (*dtd).last = entity as *mut _xmlNode;
250 } else {
251 (*(*dtd).last).next = entity as *mut _xmlNode;
252 (*entity).prev = (*dtd).last;
253 (*dtd).last = entity as *mut _xmlNode;
254 }
255
256 entity
257 }
258}
259
260/// Add an entity to a document's DTD (upstream entities.c `xmlAddEntity`, 2.15).
261///
262/// # UPSTREAM-PARITY
263///
264/// ```c
265/// int xmlAddEntity(xmlDoc *doc, int extSubset, const xmlChar *name, int type,
266/// const xmlChar *publicId, const xmlChar *systemId,
267/// const xmlChar *content, xmlEntity **out);
268/// ```
269///
270/// Mirrors the upstream control flow exactly (entities.c 2.15.0):
271///
272/// - `*out` is set to NULL on entry (upstream line: `if (out != NULL)
273/// *out = NULL;`);
274/// - NULL `doc`/`name` → `XML_ERR_ARGUMENT`;
275/// - missing DTD (extSubset selects the external subset, else internal) →
276/// `XML_DTD_NO_DTD`;
277/// - a predefined entity (lt/gt/amp/apos/quot) may only be redeclared with
278/// the exact replacement-content form upstream accepts (XML 1.0 §4.6),
279/// otherwise → `XML_ERR_REDECL_PREDEF_ENTITY`;
280/// - an unknown entity type → `XML_ERR_ARGUMENT`;
281/// - allocation failure → `XML_ERR_NO_MEMORY`;
282/// - a name already present in the selected table → `XML_WAR_ENTITY_REDEFINED`
283/// (the freshly created entity is freed);
284/// - success → `*out = entity` and returns 0.
285///
286/// The entity is created without going through the exported hooks, exactly
287/// as upstream `xmlCreateEntity` allocates directly with `xmlMalloc`.
288///
289/// # SAFETY
290///
291/// - `doc` must be a valid `_xmlDoc` pointer (or NULL), `name` a valid
292/// NUL-terminated string, `publicId`/`systemId`/`content` NUL-terminated
293/// or NULL, and `out` a writable `xmlEntity*` slot or NULL.
294#[allow(clippy::too_many_arguments)]
295pub unsafe fn add_entity_doc(
296 doc: *mut _xmlDoc,
297 ext_subset: c_int,
298 name: *const xmlChar,
299 etype: c_int,
300 public_id: *const xmlChar,
301 system_id: *const xmlChar,
302 content: *const xmlChar,
303 out: *mut *mut _xmlEntity,
304) -> c_int {
305 use crate::abi::types::{
306 XML_DTD_NO_DTD, XML_ERR_ARGUMENT, XML_ERR_NO_MEMORY, XML_ERR_REDECL_PREDEF_ENTITY,
307 XML_WAR_ENTITY_REDEFINED,
308 };
309 unsafe {
310 if !out.is_null() {
311 *out = ptr::null_mut();
312 }
313 if doc.is_null() || name.is_null() {
314 return XML_ERR_ARGUMENT;
315 }
316 let dtd = if ext_subset != 0 {
317 (*doc).extSubset
318 } else {
319 (*doc).intSubset
320 };
321 if dtd.is_null() {
322 return XML_DTD_NO_DTD;
323 }
324
325 // Select the table by entity type (upstream switch), creating it
326 // lazily with the candidate's dictionary-backed hash (hash_create).
327 let general = etype == XML_INTERNAL_GENERAL_ENTITY as c_int
328 || etype == XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int
329 || etype == XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int;
330 let parameter = etype == XML_INTERNAL_PARAMETER_ENTITY as c_int
331 || etype == XML_EXTERNAL_PARAMETER_ENTITY as c_int;
332 let table_field: *mut hash::HashTable = if general {
333 // XML 1.0 §4.6 predefined-entity redeclaration check.
334 let predef = lookup_predefined_entity(name);
335 if !predef.is_null() {
336 let mut valid = 0;
337 if etype == XML_INTERNAL_GENERAL_ENTITY as c_int && !content.is_null() {
338 let c = *(*predef).content;
339 if (*content == c)
340 && (*(content.add(1)) == 0)
341 && (c == b'>' || c == b'\'' || c == b'"')
342 {
343 valid = 1;
344 } else if (*content == b'&') && (*(content.add(1)) == b'#') {
345 if *(content.add(2)) == b'x' as xmlChar {
346 let hex = b"0123456789ABCDEF";
347 let mut ref_: [u8; 3] = [0, 0, b';'];
348 ref_[0] = hex[(c / 16 % 16) as usize];
349 ref_[1] = hex[(c % 16) as usize];
350 if libc::strcasecmp(
351 content.add(3) as *const c_char,
352 ref_.as_ptr() as *const c_char,
353 ) == 0
354 {
355 valid = 1;
356 }
357 } else {
358 let mut ref_: [u8; 3] = [0, 0, b';'];
359 ref_[0] = b'0' + c / 10 % 10;
360 ref_[1] = b'0' + c % 10;
361 if libc::strcmp(
362 content.add(2) as *const c_char,
363 ref_.as_ptr() as *const c_char,
364 ) == 0
365 {
366 valid = 1;
367 }
368 }
369 }
370 }
371 if valid == 0 {
372 return XML_ERR_REDECL_PREDEF_ENTITY;
373 }
374 }
375 if (*dtd).entities.is_null() {
376 (*dtd).entities = hash::hash_create(8) as *mut c_void;
377 }
378 (*dtd).entities as *mut hash::HashTable
379 } else if parameter {
380 if (*dtd).pentities.is_null() {
381 (*dtd).pentities = hash::hash_create(8) as *mut c_void;
382 }
383 (*dtd).pentities as *mut hash::HashTable
384 } else {
385 return XML_ERR_ARGUMENT;
386 };
387
388 // Upstream xmlCreateEntity: allocate zeroed, fill fields, strdup.
389 let entity = allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
390 if entity.is_null() {
391 return XML_ERR_NO_MEMORY;
392 }
393 (*entity).doc = doc;
394 (*entity).type_ = XML_ENTITY_DECL as c_int;
395 (*entity).etype = etype;
396 (*entity).name = string::xml_strdup(name);
397 if (*entity).name.is_null() {
398 free_entity_internal(entity, false);
399 return XML_ERR_NO_MEMORY;
400 }
401 if !public_id.is_null() {
402 (*entity).ExternalID = string::xml_strdup(public_id);
403 if (*entity).ExternalID.is_null() {
404 free_entity_internal(entity, false);
405 return XML_ERR_NO_MEMORY;
406 }
407 }
408 if !system_id.is_null() {
409 (*entity).SystemID = string::xml_strdup(system_id);
410 if (*entity).SystemID.is_null() {
411 free_entity_internal(entity, false);
412 return XML_ERR_NO_MEMORY;
413 }
414 }
415 if !content.is_null() {
416 (*entity).length = string::xml_strlen(content) as c_int;
417 (*entity).content = string::xml_strdup(content);
418 if (*entity).content.is_null() {
419 free_entity_internal(entity, false);
420 return XML_ERR_NO_MEMORY;
421 }
422 } else {
423 (*entity).length = 0;
424 (*entity).content = ptr::null_mut();
425 }
426 (*entity).URI = ptr::null();
427 (*entity).orig = ptr::null_mut();
428 (*entity).flags = 0;
429 (*entity).expandedSize = 0;
430 (*entity).nexte = ptr::null_mut();
431 (*entity).owner = 0;
432
433 // Upstream xmlHashAdd on the selected table: a name already present
434 // frees the fresh entity and reports the redefinition warning; any
435 // other add failure is treated as OOM (upstream res < 0).
436 let existing = hash::hash_lookup(table_field, name);
437 if !existing.is_null() {
438 free_entity_internal(entity, false);
439 return XML_WAR_ENTITY_REDEFINED;
440 }
441 let res = hash::hash_add_entry(table_field, name, entity as *mut c_void);
442 if res != 0 {
443 free_entity_internal(entity, false);
444 return XML_ERR_NO_MEMORY;
445 }
446
447 // Link it to the DTD (upstream "Link it to the DTD" block).
448 (*entity).parent = dtd;
449 (*entity).doc = (*dtd).doc;
450 if (*dtd).last.is_null() {
451 (*dtd).children = entity as *mut _xmlNode;
452 (*dtd).last = entity as *mut _xmlNode;
453 } else {
454 (*(*dtd).last).next = entity as *mut _xmlNode;
455 (*entity).prev = (*dtd).last;
456 (*dtd).last = entity as *mut _xmlNode;
457 }
458
459 if !out.is_null() {
460 *out = entity;
461 }
462 0
463 }
464}
465
466/// Get a general entity by name.
467///
468/// # UPSTREAM-PARITY
469///
470/// ```c
471/// xmlEntityPtr xmlGetEntity(xmlDocPtr doc, const xmlChar *name);
472/// ```
473///
474/// Searches the document's DTD for a general entity with the given name.
475/// Checks both internal and external subsets.
476///
477/// # SAFETY
478///
479/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
480/// - `name` must be a valid null-terminated string.
481pub unsafe fn get_entity(doc: *mut _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
482 // UPSTREAM-PARITY (entities.c xmlGetDocEntity): a NULL document resolves
483 // through the PREDEFINED entities only — a doc-less
484 // `new DOMEntityReference("amp")` (xmlNewReference with doc NULL) still
485 // binds the predefined declaration as its children/last/content, and
486 // php's `dom_entity_reference_fetch_and_sync_declaration` re-syncs the
487 // same way via xmlGetDocEntity(NULL, ...).
488 if doc.is_null() {
489 return unsafe { crate::abi::exports_misc::xmlGetPredefinedEntity(name) };
490 }
491 if name.is_null() {
492 return ptr::null_mut();
493 }
494
495 unsafe {
496 let d = &*doc;
497
498 // Check internal subset
499 if !d.intSubset.is_null() {
500 let entities = (*d.intSubset).entities as *mut hash::HashTable;
501 let found = hash::hash_lookup(entities, name);
502 if !found.is_null() {
503 return found as *mut _xmlEntity;
504 }
505 }
506
507 // Check external subset
508 if !d.extSubset.is_null() {
509 let entities = (*d.extSubset).entities as *mut hash::HashTable;
510 let found = hash::hash_lookup(entities, name);
511 if !found.is_null() {
512 return found as *mut _xmlEntity;
513 }
514 }
515
516 // UPSTREAM-PARITY (entities.c xmlGetDocEntity): an unregistered name
517 // still resolves to the predefined entity (amp/lt/gt/quot/apos).
518 unsafe { crate::abi::exports_misc::xmlGetPredefinedEntity(name) }
519 }
520}
521
522/// Look up an entity in a DTD's entity hash table (upstream
523/// `xmlGetDtdEntity` core).
524///
525/// # SAFETY
526///
527/// - `dtd` must be a valid DTD pointer or NULL.
528/// - `name` must be a valid null-terminated string.
529pub unsafe fn get_entity_from_dtd(dtd: *mut _xmlDtd, name: *const xmlChar) -> *mut _xmlEntity {
530 if dtd.is_null() || name.is_null() {
531 return ptr::null_mut();
532 }
533 unsafe {
534 let entities = (*dtd).entities as *mut hash::HashTable;
535 let found = hash::hash_lookup(entities, name);
536 if found.is_null() {
537 ptr::null_mut()
538 } else {
539 found as *mut _xmlEntity
540 }
541 }
542}
543
544/// Get a parameter entity by name.
545///
546/// # UPSTREAM-PARITY
547///
548/// ```c
549/// xmlEntityPtr xmlGetParameterEntity(xmlDocPtr doc, const xmlChar *name);
550/// ```
551///
552/// Searches the document's DTD for a parameter entity with the given name.
553///
554/// # SAFETY
555///
556/// - `doc` must be a valid pointer to an _xmlDoc, or NULL.
557/// - `name` must be a valid null-terminated string.
558pub unsafe fn get_parameter_entity(doc: *mut _xmlDoc, name: *const xmlChar) -> *mut _xmlEntity {
559 if doc.is_null() || name.is_null() {
560 return ptr::null_mut();
561 }
562
563 unsafe {
564 let d = &*doc;
565
566 // Check internal subset
567 if !d.intSubset.is_null() {
568 let pentities = (*d.intSubset).pentities as *mut hash::HashTable;
569 let found = hash::hash_lookup(pentities, name);
570 if !found.is_null() {
571 return found as *mut _xmlEntity;
572 }
573 }
574
575 // Check external subset
576 if !d.extSubset.is_null() {
577 let pentities = (*d.extSubset).pentities as *mut hash::HashTable;
578 let found = hash::hash_lookup(pentities, name);
579 if !found.is_null() {
580 return found as *mut _xmlEntity;
581 }
582 }
583
584 ptr::null_mut()
585 }
586}
587
588/// Deep copy an entity declaration.
589///
590/// # UPSTREAM-PARITY
591///
592/// ```c
593/// xmlEntityPtr xmlCopyEntity(xmlEntityPtr entity);
594/// ```
595///
596/// # SAFETY
597///
598/// - `entity` must be a valid pointer to an _xmlEntity, or NULL.
599pub unsafe fn copy_entity(entity: *mut _xmlEntity) -> *mut _xmlEntity {
600 if entity.is_null() {
601 return ptr::null_mut();
602 }
603
604 unsafe {
605 let e = &*entity;
606
607 // SAFETY: Allocate zero-initialized memory for the copy.
608 let copy = allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
609 if copy.is_null() {
610 return ptr::null_mut();
611 }
612
613 (*copy).type_ = XML_ENTITY_DECL as c_int;
614 (*copy).name = string::xml_strdup(e.name);
615 (*copy).etype = e.etype;
616 (*copy).ExternalID = string::xml_strdup(e.ExternalID);
617 (*copy).SystemID = string::xml_strdup(e.SystemID);
618 (*copy).content = string::xml_strdup(e.content);
619 (*copy).orig = string::xml_strdup(e.orig);
620 (*copy).length = e.length;
621 (*copy).flags = e.flags;
622 (*copy).expandedSize = e.expandedSize;
623 (*copy).URI = string::xml_strdup(e.URI);
624 (*copy).parent = e.parent;
625 (*copy).doc = e.doc;
626 (*copy).nexte = ptr::null_mut();
627 (*copy).owner = e.owner;
628
629 copy
630 }
631}
632
633/// Free an entity declaration.
634///
635/// # UPSTREAM-PARITY
636///
637/// ```c
638/// void xmlFreeEntity(xmlEntityPtr entity);
639/// ```
640///
641/// # SAFETY
642///
643/// - `entity` must be a valid pointer to an _xmlEntity, or NULL.
644pub unsafe fn free_entity(entity: *mut _xmlEntity) {
645 free_entity_internal(entity, true);
646}
647
648/// Internal entity free function.
649///
650/// If `free_children` is true, also frees the entity's children tree nodes.
651/// This is separated because the hash deallocator should not free children
652/// (they are owned by the document tree), but `xmlFreeEntity` from user code
653/// should free everything.
654///
655/// # Safety
656///
657/// - `entity` must be NULL or a pointer to a heap-allocated `_xmlEntity` whose
658/// `name`, `content`, `orig`, `ExternalID`, `SystemID`, and `URI` fields are
659/// each either NULL or pointers to allocator-owned allocations. The call
660/// frees every non-NULL field, then frees the entity itself, so afterwards
661/// neither the entity nor any of its fields may be dereferenced or freed
662/// again by the caller.
663unsafe fn free_entity_internal(entity: *mut _xmlEntity, free_children: bool) {
664 if entity.is_null() {
665 return;
666 }
667
668 unsafe {
669 let e = &*entity;
670
671 if !e.name.is_null() {
672 allocator::xmlFreeImpl(e.name as *mut c_void);
673 }
674 if !e.content.is_null() {
675 allocator::xmlFreeImpl(e.content as *mut c_void);
676 }
677 if !e.orig.is_null() {
678 allocator::xmlFreeImpl(e.orig as *mut c_void);
679 }
680 if !e.ExternalID.is_null() {
681 allocator::xmlFreeImpl(e.ExternalID as *mut c_void);
682 }
683 if !e.SystemID.is_null() {
684 allocator::xmlFreeImpl(e.SystemID as *mut c_void);
685 }
686 if !e.URI.is_null() {
687 allocator::xmlFreeImpl(e.URI as *mut c_void);
688 }
689
690 // Free children tree nodes if requested. The materialized replacement
691 // tree (upstream xmlNodeParseAttValue fills `ent->children` from
692 // `ent->content` on first reference) is owned by the declaration and
693 // freed with it (text/cdata nodes and entity-REF nodes; the REFs' own
694 // `children` point at declarations, which free_node never descends
695 // into).
696 if free_children && !e.children.is_null() {
697 crate::xml::tree::free_node_list(e.children);
698 }
699
700 allocator::xmlFreeImpl(entity as *mut c_void);
701 }
702}
703
704// ═══════════════════════════════════════════════════════════════════════════════
705// Entity Type Helpers
706// ═══════════════════════════════════════════════════════════════════════════════
707
708/// Check if an entity type is a parameter entity.
709#[inline]
710pub const fn is_parameter_entity(etype: c_int) -> bool {
711 etype == XML_INTERNAL_PARAMETER_ENTITY as c_int
712 || etype == XML_EXTERNAL_PARAMETER_ENTITY as c_int
713}
714
715/// Check if an entity type is an external entity.
716#[inline]
717pub const fn is_external_entity(etype: c_int) -> bool {
718 etype == XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int
719 || etype == XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int
720 || etype == XML_EXTERNAL_PARAMETER_ENTITY as c_int
721}
722
723/// Check if an entity type is a predefined entity.
724#[inline]
725pub const fn is_predefined_entity(etype: c_int) -> bool {
726 etype == XML_INTERNAL_PREDEFINED_ENTITY as c_int
727}
728
729// ═══════════════════════════════════════════════════════════════════════════════
730// Entity Substitution & Encoding
731// ═══════════════════════════════════════════════════════════════════════════════
732
733/// Encode special XML characters in a string for output.
734///
735/// # UPSTREAM-PARITY
736///
737/// ```c
738/// xmlChar *xmlEncodeEntitiesReentrant(xmlDocPtr doc, const xmlChar *input);
739/// ```
740///
741/// Encodes `<`, `>`, `&`, `"`, `'` as their corresponding XML entities.
742/// The returned string must be freed with `xmlFree`.
743///
744/// # SAFETY
745///
746/// - `input` must be a valid null-terminated xmlChar string, or NULL.
747pub unsafe fn encode_entities_reentrant(_doc: *mut _xmlDoc, input: *const xmlChar) -> *mut xmlChar {
748 if input.is_null() {
749 return ptr::null_mut();
750 }
751
752 unsafe {
753 // First pass: calculate output size
754 let len = string::xml_strlen(input);
755 let mut out_len: usize = 0;
756
757 for i in 0..len {
758 match *input.add(i) {
759 b'<' => out_len += 4, // <
760 b'>' => out_len += 4, // >
761 b'&' => out_len += 5, // &
762 b'"' => out_len += 6, // "
763 b'\'' => out_len += 6, // '
764 _c => out_len += 1,
765 }
766 }
767
768 // Allocate output buffer
769 let output = allocator::xmlMallocImpl(out_len + 1) as *mut xmlChar;
770 if output.is_null() {
771 return ptr::null_mut();
772 }
773
774 // Second pass: encode
775 let mut j: usize = 0;
776 for i in 0..len {
777 let c = *input.add(i);
778 match c {
779 b'<' => {
780 ptr::copy_nonoverlapping(b"<" as *const u8, output.add(j), 4);
781 j += 4;
782 }
783 b'>' => {
784 ptr::copy_nonoverlapping(b">" as *const u8, output.add(j), 4);
785 j += 4;
786 }
787 b'&' => {
788 ptr::copy_nonoverlapping(b"&" as *const u8, output.add(j), 5);
789 j += 5;
790 }
791 b'"' => {
792 ptr::copy_nonoverlapping(b""" as *const u8, output.add(j), 6);
793 j += 6;
794 }
795 b'\'' => {
796 ptr::copy_nonoverlapping(b"'" as *const u8, output.add(j), 6);
797 j += 6;
798 }
799 _ => {
800 *output.add(j) = c;
801 j += 1;
802 }
803 }
804 }
805
806 *output.add(out_len) = 0; // null-terminate
807 output
808 }
809}
810
811/// Decode entity references in a string.
812///
813/// # UPSTREAM-PARITY
814///
815/// ```c
816/// xmlChar *xmlStringDecodeEntities(xmlDocPtr doc, const xmlChar *input,
817/// int what, int end, int end2, int end3);
818/// ```
819///
820/// Replaces entity references (`&name;`) with their content from the
821/// document's entity declarations. Also handles numeric character
822/// references (`&#NNN;` and `&#xHHH;`).
823///
824/// The `what` parameter specifies which entities to substitute:
825/// - 0: substitute all
826/// - 1: substitute only predefined
827/// - 2: substitute only general
828///
829/// `end`, `end2`, `end3` specify terminating characters (0 if none).
830///
831/// Returns the decoded string (must be freed with `xmlFree`), or NULL on error.
832///
833/// # SAFETY
834///
835/// - `doc` may be NULL (no entity lookup, only numeric refs).
836/// - `input` must be a valid null-terminated xmlChar string, or NULL.
837pub unsafe fn string_decode_entities(
838 doc: *mut _xmlDoc,
839 input: *const xmlChar,
840 what: c_int,
841 end: xmlChar,
842 end2: xmlChar,
843 end3: xmlChar,
844) -> *mut xmlChar {
845 if input.is_null() {
846 return ptr::null_mut();
847 }
848
849 unsafe {
850 let len = string::xml_strlen(input);
851 if len == 0 {
852 // Return empty string
853 let empty = allocator::xmlMallocImpl(1) as *mut xmlChar;
854 if !empty.is_null() {
855 *empty = 0;
856 }
857 return empty;
858 }
859
860 // Allocate a generous output buffer (input length + expansion)
861 let max_out = len * 4 + 1; // Allow for some expansion
862 let output = allocator::xmlMallocImpl(max_out) as *mut xmlChar;
863 if output.is_null() {
864 return ptr::null_mut();
865 }
866
867 let mut out_pos: usize = 0;
868 let mut i: usize = 0;
869
870 while i < len {
871 let c = *input.add(i);
872
873 // Check for terminating characters
874 if (end != 0 && c == end) || (end2 != 0 && c == end2) || (end3 != 0 && c == end3) {
875 break;
876 }
877
878 if c == b'&' {
879 // Entity reference
880 i += 1;
881
882 // Check for numeric character reference: &#NNN; or &#xHHH;
883 if i < len && *input.add(i) == b'#' {
884 i += 1;
885 let (decoded_char, _consumed) = decode_numeric_ref(input, &mut i, len);
886 if decoded_char != 0 {
887 // Encode the decoded character as UTF-8
888 if decoded_char < 0x80 {
889 if out_pos < max_out - 1 {
890 *output.add(out_pos) = decoded_char as u8;
891 out_pos += 1;
892 }
893 } else if decoded_char < 0x800 {
894 if out_pos < max_out - 2 {
895 *output.add(out_pos) = 0xC0 | ((decoded_char >> 6) as u8);
896 *output.add(out_pos + 1) = 0x80 | ((decoded_char & 0x3F) as u8);
897 out_pos += 2;
898 }
899 } else {
900 if out_pos < max_out - 3 {
901 *output.add(out_pos) = 0xE0 | ((decoded_char >> 12) as u8);
902 *output.add(out_pos + 1) =
903 0x80 | (((decoded_char >> 6) & 0x3F) as u8);
904 *output.add(out_pos + 2) = 0x80 | ((decoded_char & 0x3F) as u8);
905 out_pos += 3;
906 }
907 }
908 }
909 // decode_numeric_ref already advanced i past ';'
910 // The continue skips the i += 1 at the bottom of the loop
911 continue;
912 }
913
914 // General entity reference: &name;
915 let entity_name_start = i;
916 while i < len
917 && *input.add(i) != b';'
918 && *input.add(i) != b'&'
919 && *input.add(i) != 0
920 {
921 i += 1;
922 }
923
924 if i < len && *input.add(i) == b';' {
925 // We have a complete entity reference
926 let name_len = i - entity_name_start;
927 if name_len > 0 {
928 // Create a null-terminated name
929 let name_buf = allocator::xmlMallocImpl(name_len + 1) as *mut xmlChar;
930 if !name_buf.is_null() {
931 ptr::copy_nonoverlapping(
932 input.add(entity_name_start),
933 name_buf,
934 name_len,
935 );
936 *name_buf.add(name_len) = 0;
937
938 // Try to find the entity
939 let mut entity: *mut _xmlEntity = ptr::null_mut();
940 if what != 1 {
941 // Not just predefined
942 if !doc.is_null() {
943 entity = get_entity(doc, name_buf as *const xmlChar);
944 }
945 }
946
947 if entity.is_null() {
948 // Try predefined entities
949 entity = lookup_predefined_entity(name_buf as *const xmlChar);
950 }
951
952 if !entity.is_null() && !(*entity).content.is_null() {
953 // Copy entity content to output
954 let content = (*entity).content;
955 let content_len = string::xml_strlen(content);
956 for j in 0..content_len {
957 if out_pos < max_out - 1 {
958 *output.add(out_pos) = *content.add(j);
959 out_pos += 1;
960 }
961 }
962 } else {
963 // Entity not found — output the reference as-is
964 if out_pos < max_out - 2 {
965 *output.add(out_pos) = b'&';
966 out_pos += 1;
967 }
968 for j in 0..name_len {
969 if out_pos < max_out - 2 {
970 *output.add(out_pos) = *input.add(entity_name_start + j);
971 out_pos += 1;
972 }
973 }
974 if out_pos < max_out - 1 {
975 *output.add(out_pos) = b';';
976 out_pos += 1;
977 }
978 }
979
980 allocator::xmlFreeImpl(name_buf as *mut c_void);
981 }
982 }
983 // Skip past the semicolon
984 // i is already at the semicolon, loop increment will skip it
985 } else {
986 // Malformed reference — output as-is
987 if out_pos < max_out - 1 {
988 *output.add(out_pos) = b'&';
989 out_pos += 1;
990 }
991 // Back up to include all characters we scanned
992 // The loop increment will advance i
993 }
994 } else {
995 // Regular character
996 if out_pos < max_out - 1 {
997 *output.add(out_pos) = c;
998 out_pos += 1;
999 }
1000 }
1001
1002 i += 1;
1003 }
1004
1005 *output.add(out_pos) = 0; // null-terminate
1006 output
1007 }
1008}
1009
1010/// Decode a numeric character reference (`&#NNN;` or `&#xHHH;`).
1011///
1012/// Returns the decoded character and the number of additional characters consumed.
1013///
1014/// # Safety
1015///
1016/// - `input` must be a valid pointer to a buffer of at least `len` readable
1017/// bytes; `pos` must be a valid `&mut usize` in `0..=len` — the read at
1018/// `input[pos]` is bounds-checked against `len` before every access.
1019const unsafe fn decode_numeric_ref(
1020 input: *const xmlChar,
1021 pos: &mut usize,
1022 len: usize,
1023) -> (u32, usize) {
1024 unsafe {
1025 let mut consumed: usize = 0;
1026
1027 if *pos >= len {
1028 return (0, 0);
1029 }
1030
1031 if *input.add(*pos) == b'x' || *input.add(*pos) == b'X' {
1032 // Hexadecimal: &#xHHH;
1033 *pos += 1;
1034 consumed += 1;
1035
1036 let mut value: u32 = 0;
1037 while *pos < len {
1038 let c = *input.add(*pos);
1039 if c == b';' {
1040 *pos += 1;
1041 consumed += 1;
1042 return (value, consumed);
1043 }
1044 let digit = match c {
1045 b'0'..=b'9' => c - b'0',
1046 b'a'..=b'f' => c - b'a' + 10,
1047 b'A'..=b'F' => c - b'A' + 10,
1048 _ => break,
1049 };
1050 value = value.wrapping_mul(16).wrapping_add(digit as u32);
1051 *pos += 1;
1052 consumed += 1;
1053 }
1054 (value, consumed)
1055 } else {
1056 // Decimal: &#NNN;
1057 let mut value: u32 = 0;
1058 while *pos < len {
1059 let c = *input.add(*pos);
1060 if c == b';' {
1061 *pos += 1;
1062 consumed += 1;
1063 return (value, consumed);
1064 }
1065 if !c.is_ascii_digit() {
1066 break;
1067 }
1068 value = value.wrapping_mul(10).wrapping_add((c - b'0') as u32);
1069 *pos += 1;
1070 consumed += 1;
1071 }
1072 (value, consumed)
1073 }
1074 }
1075}
1076
1077/// A wrapper for static data that implements Sync for raw pointer types.
1078struct SyncPtr<T>(pub T);
1079unsafe impl<T> Sync for SyncPtr<T> {}
1080
1081/// Look up a predefined XML entity by name.
1082///
1083/// Returns the entity pointer (to a static entity) or NULL.
1084///
1085/// # Safety
1086///
1087/// - `name` must be NULL or a pointer to a NUL-terminated `xmlChar` string
1088/// that stays readable for the duration of the call: the first byte is read
1089/// via `name.add(0)` and the full string is compared with
1090/// `string::xml_strcmp`. The returned pointer aliases the immutable static
1091/// `PREDEFINED_ENTITIES` array, lives for the whole program, and must not be
1092/// freed or mutated by the caller.
1093unsafe fn lookup_predefined_entity(name: *const xmlChar) -> *mut _xmlEntity {
1094 if name.is_null() {
1095 return ptr::null_mut();
1096 }
1097
1098 // UPSTREAM-PARITY: Predefined entities are: lt, gt, amp, quot, apos
1099 static PREDEFINED_ENTITIES: SyncPtr<[_xmlEntity; 5]> = SyncPtr([
1100 _xmlEntity {
1101 _private: ptr::null_mut(),
1102 type_: XML_ENTITY_DECL as c_int,
1103 name: b"lt\0" as *const u8 as *const xmlChar,
1104 children: ptr::null_mut(),
1105 last: ptr::null_mut(),
1106 parent: ptr::null_mut(),
1107 next: ptr::null_mut(),
1108 prev: ptr::null_mut(),
1109 doc: ptr::null_mut(),
1110 orig: ptr::null_mut(),
1111 content: b"<\0" as *const u8 as *mut xmlChar,
1112 length: 1,
1113 etype: XML_INTERNAL_PREDEFINED_ENTITY as c_int,
1114 ExternalID: ptr::null(),
1115 SystemID: ptr::null(),
1116 nexte: ptr::null_mut(),
1117 URI: ptr::null(),
1118 owner: 0,
1119 flags: 0,
1120 expandedSize: 0,
1121 },
1122 _xmlEntity {
1123 _private: ptr::null_mut(),
1124 type_: XML_ENTITY_DECL as c_int,
1125 name: b"gt\0" as *const u8 as *const xmlChar,
1126 children: ptr::null_mut(),
1127 last: ptr::null_mut(),
1128 parent: ptr::null_mut(),
1129 next: ptr::null_mut(),
1130 prev: ptr::null_mut(),
1131 doc: ptr::null_mut(),
1132 orig: ptr::null_mut(),
1133 content: b">\0" as *const u8 as *mut xmlChar,
1134 length: 1,
1135 etype: XML_INTERNAL_PREDEFINED_ENTITY as c_int,
1136 ExternalID: ptr::null(),
1137 SystemID: ptr::null(),
1138 nexte: ptr::null_mut(),
1139 URI: ptr::null(),
1140 owner: 0,
1141 flags: 0,
1142 expandedSize: 0,
1143 },
1144 _xmlEntity {
1145 _private: ptr::null_mut(),
1146 type_: XML_ENTITY_DECL as c_int,
1147 name: b"amp\0" as *const u8 as *const xmlChar,
1148 children: ptr::null_mut(),
1149 last: ptr::null_mut(),
1150 parent: ptr::null_mut(),
1151 next: ptr::null_mut(),
1152 prev: ptr::null_mut(),
1153 doc: ptr::null_mut(),
1154 orig: ptr::null_mut(),
1155 content: b"&\0" as *const u8 as *mut xmlChar,
1156 length: 1,
1157 etype: XML_INTERNAL_PREDEFINED_ENTITY as c_int,
1158 ExternalID: ptr::null(),
1159 SystemID: ptr::null(),
1160 nexte: ptr::null_mut(),
1161 URI: ptr::null(),
1162 owner: 0,
1163 flags: 0,
1164 expandedSize: 0,
1165 },
1166 _xmlEntity {
1167 _private: ptr::null_mut(),
1168 type_: XML_ENTITY_DECL as c_int,
1169 name: b"quot\0" as *const u8 as *const xmlChar,
1170 children: ptr::null_mut(),
1171 last: ptr::null_mut(),
1172 parent: ptr::null_mut(),
1173 next: ptr::null_mut(),
1174 prev: ptr::null_mut(),
1175 doc: ptr::null_mut(),
1176 orig: ptr::null_mut(),
1177 content: b"\"\0" as *const u8 as *mut xmlChar,
1178 length: 1,
1179 etype: XML_INTERNAL_PREDEFINED_ENTITY as c_int,
1180 ExternalID: ptr::null(),
1181 SystemID: ptr::null(),
1182 nexte: ptr::null_mut(),
1183 URI: ptr::null(),
1184 owner: 0,
1185 flags: 0,
1186 expandedSize: 0,
1187 },
1188 _xmlEntity {
1189 _private: ptr::null_mut(),
1190 type_: XML_ENTITY_DECL as c_int,
1191 name: b"apos\0" as *const u8 as *const xmlChar,
1192 children: ptr::null_mut(),
1193 last: ptr::null_mut(),
1194 parent: ptr::null_mut(),
1195 next: ptr::null_mut(),
1196 prev: ptr::null_mut(),
1197 doc: ptr::null_mut(),
1198 orig: ptr::null_mut(),
1199 content: b"'\0" as *const u8 as *mut xmlChar,
1200 length: 1,
1201 etype: XML_INTERNAL_PREDEFINED_ENTITY as c_int,
1202 ExternalID: ptr::null(),
1203 SystemID: ptr::null(),
1204 nexte: ptr::null_mut(),
1205 URI: ptr::null(),
1206 owner: 0,
1207 flags: 0,
1208 expandedSize: 0,
1209 },
1210 ]);
1211
1212 unsafe {
1213 // Match which entity
1214 let idx = match *name.add(0) as char {
1215 'l' => {
1216 if string::xml_strcmp(name, b"lt\0" as *const u8 as *const xmlChar) == 0 {
1217 0
1218 } else {
1219 return ptr::null_mut();
1220 }
1221 }
1222 'g' => {
1223 if string::xml_strcmp(name, b"gt\0" as *const u8 as *const xmlChar) == 0 {
1224 1
1225 } else {
1226 return ptr::null_mut();
1227 }
1228 }
1229 'a' => {
1230 if string::xml_strcmp(name, b"amp\0" as *const u8 as *const xmlChar) == 0 {
1231 2
1232 } else if string::xml_strcmp(name, b"apos\0" as *const u8 as *const xmlChar) == 0 {
1233 4
1234 } else {
1235 return ptr::null_mut();
1236 }
1237 }
1238 'q' if string::xml_strcmp(name, b"quot\0" as *const u8 as *const xmlChar) == 0 => 3,
1239 _ => return ptr::null_mut(),
1240 };
1241
1242 &PREDEFINED_ENTITIES.0[idx] as *const _xmlEntity as *mut _xmlEntity
1243 }
1244}
1245
1246// ═══════════════════════════════════════════════════════════════════════════════
1247// Entity Content Retrieval
1248// ═══════════════════════════════════════════════════════════════════════════════
1249
1250/// Get the content of an entity as a string.
1251///
1252/// # SAFETY
1253///
1254/// - `entity` must be a valid pointer to an _xmlEntity, or NULL.
1255pub unsafe fn get_entity_content(entity: *mut _xmlEntity) -> *mut xmlChar {
1256 if entity.is_null() {
1257 return ptr::null_mut();
1258 }
1259
1260 unsafe {
1261 let e = &*entity;
1262 if e.content.is_null() {
1263 return ptr::null_mut();
1264 }
1265 string::xml_strdup(e.content)
1266 }
1267}
1268
1269// ═══════════════════════════════════════════════════════════════════════════════
1270// Security/Limit Functions
1271// ═══════════════════════════════════════════════════════════════════════════════
1272
1273/// Check if entity expansion exceeds limits.
1274///
1275/// Returns 0 if within limits, -1 if exceeded.
1276pub const fn check_entity_expansion_limit(expanded_size: c_ulong) -> c_int {
1277 if expanded_size > XML_ENTITY_CONTENT_EXPANSION_MAX as c_ulong {
1278 -1
1279 } else {
1280 0
1281 }
1282}
1283
1284/// Check if entity recursion depth exceeds limits.
1285///
1286/// Returns 0 if within limits, -1 if exceeded.
1287pub const fn check_entity_recursion_depth(depth: c_int) -> c_int {
1288 if depth > XML_ENTITY_CONTENT_DEPTH_MAX {
1289 -1
1290 } else {
1291 0
1292 }
1293}
1294
1295// ═══════════════════════════════════════════════════════════════════════════════
1296// Tests
1297// ═══════════════════════════════════════════════════════════════════════════════
1298
1299#[cfg(test)]
1300mod tests {
1301 use super::*;
1302
1303 use core::ffi::c_void;
1304 use core::ptr;
1305
1306 unsafe fn c_str(s: &[u8]) -> *const xmlChar {
1307 let len = s.len();
1308 let buf = allocator::xmlMallocImpl(len + 1) as *mut xmlChar;
1309 assert!(!buf.is_null());
1310 ptr::copy_nonoverlapping(s.as_ptr(), buf, len);
1311 *buf.add(len) = 0;
1312 buf as *const xmlChar
1313 }
1314
1315 unsafe fn make_doc_and_dtd() -> (*mut _xmlDoc, *mut _xmlDtd) {
1316 let doc = allocator::xmlMallocZero(size_of::<_xmlDoc>() as usize) as *mut _xmlDoc;
1317 assert!(!doc.is_null());
1318 (*doc).type_ = XML_DOCUMENT_NODE as c_int;
1319 (*doc).doc = doc;
1320 let dtd = allocator::xmlMallocZero(size_of::<_xmlDtd>() as usize) as *mut _xmlDtd;
1321 assert!(!dtd.is_null());
1322 (*dtd).type_ = XML_DTD_NODE as c_int;
1323 (*dtd).parent = doc;
1324 (*dtd).doc = doc;
1325 (*dtd).entities = hash::hash_create(8) as *mut c_void;
1326 (*dtd).pentities = hash::hash_create(8) as *mut c_void;
1327 (*doc).intSubset = dtd;
1328 (doc, dtd)
1329 }
1330
1331 // ── Entity Declaration Tests ────────────────────────────────────────
1332
1333 #[test]
1334 /// Tests that a general entity can be added to a DTD and looked back up.
1335 ///
1336 /// # Safety
1337 ///
1338 /// - The test dereferences only raw pointers it allocates itself: `doc`
1339 /// and `dtd` are zero-initialized by `make_doc_and_dtd` and stay alive
1340 /// until the final `xmlFreeImpl` calls; `name` and `content` are
1341 /// NUL-terminated buffers from `c_str`; `entity` is asserted non-NULL
1342 /// before `(*entity)` is read. The hash tables are freed with
1343 /// `entity_deallocator` before `dtd` and `doc` are freed, so no access
1344 /// touches freed memory.
1345 fn test_add_entity_general() {
1346 unsafe {
1347 let (doc, dtd) = make_doc_and_dtd();
1348 let name = c_str(b"myEntity");
1349 let content = c_str(b"Hello, World!");
1350
1351 let entity = add_entity(
1352 dtd,
1353 name,
1354 XML_INTERNAL_GENERAL_ENTITY as c_int,
1355 ptr::null(),
1356 ptr::null(),
1357 content,
1358 );
1359 assert!(!entity.is_null());
1360 assert_eq!((*entity).etype, XML_INTERNAL_GENERAL_ENTITY as c_int);
1361 assert_eq!((*entity).length, 13);
1362
1363 // Lookup
1364 let found = get_entity(doc, name);
1365 assert_eq!(found, entity);
1366
1367 // Cleanup
1368 // We need to free the hash tables manually since we don't have free_dtd available
1369 hash::hash_free(
1370 (*dtd).entities as *mut hash::HashTable,
1371 Some(entity_deallocator),
1372 );
1373 hash::hash_free(
1374 (*dtd).pentities as *mut hash::HashTable,
1375 Some(entity_deallocator),
1376 );
1377 allocator::xmlFreeImpl(dtd as *mut c_void);
1378 allocator::xmlFreeImpl(doc as *mut c_void);
1379 }
1380 }
1381
1382 /// Deallocator passed to `hash::hash_free` to release entity payloads.
1383 ///
1384 /// # Safety
1385 ///
1386 /// - `payload` must be NULL or a pointer to a heap-allocated `_xmlEntity`
1387 /// that has not yet been freed, because it is forwarded to
1388 /// `free_entity`; `_name` is ignored by this function.
1389 unsafe extern "C" fn entity_deallocator(payload: *mut c_void, _name: *mut u8) {
1390 if !payload.is_null() {
1391 free_entity(payload as *mut _xmlEntity);
1392 }
1393 }
1394
1395 #[test]
1396 /// Tests that a parameter entity is stored in the `pentities` table.
1397 ///
1398 /// # Safety
1399 ///
1400 /// - `doc` and `dtd` are zero-initialized by `make_doc_and_dtd` and remain
1401 /// live until the trailing frees; `name` and `content` are NUL-terminated
1402 /// `c_str` allocations valid for the whole test; the `entity` pointer is
1403 /// asserted non-NULL before `(*entity)` is read, and the hash tables are
1404 /// freed with `entity_deallocator` before `dtd` and `doc` are freed.
1405 fn test_add_entity_parameter() {
1406 unsafe {
1407 let (doc, dtd) = make_doc_and_dtd();
1408 let name = c_str(b"myParam");
1409 let content = c_str(b"parameter content");
1410
1411 let entity = add_entity(
1412 dtd,
1413 name,
1414 XML_INTERNAL_PARAMETER_ENTITY as c_int,
1415 ptr::null(),
1416 ptr::null(),
1417 content,
1418 );
1419 assert!(!entity.is_null());
1420 assert_eq!((*entity).etype, XML_INTERNAL_PARAMETER_ENTITY as c_int);
1421
1422 // Parameter entity should be in pentities
1423 let found = hash::hash_lookup((*dtd).pentities as *mut hash::HashTable, name);
1424 assert_eq!(found, entity as *mut c_void);
1425
1426 // General entity lookup should NOT find it
1427 let not_found = hash::hash_lookup((*dtd).entities as *mut hash::HashTable, name);
1428 assert!(not_found.is_null());
1429
1430 hash::hash_free(
1431 (*dtd).entities as *mut hash::HashTable,
1432 Some(entity_deallocator),
1433 );
1434 hash::hash_free(
1435 (*dtd).pentities as *mut hash::HashTable,
1436 Some(entity_deallocator),
1437 );
1438 allocator::xmlFreeImpl(dtd as *mut c_void);
1439 allocator::xmlFreeImpl(doc as *mut c_void);
1440 }
1441 }
1442
1443 #[test]
1444 /// Tests that `add_entity` returns NULL when the DTD is NULL.
1445 ///
1446 /// # Safety
1447 ///
1448 /// - `add_entity` checks `dtd` for NULL and returns early without
1449 /// dereferencing it, and the `name` argument is a NUL-terminated `c_str`
1450 /// allocation that stays alive for the duration of the call.
1451 fn test_add_entity_null_dtd() {
1452 unsafe {
1453 let entity = add_entity(
1454 ptr::null_mut(),
1455 c_str(b"test"),
1456 XML_INTERNAL_GENERAL_ENTITY as c_int,
1457 ptr::null(),
1458 ptr::null(),
1459 ptr::null(),
1460 );
1461 assert!(entity.is_null());
1462 }
1463 }
1464
1465 #[test]
1466 /// Tests that re-adding an entity with the same name reuses the existing
1467 /// declaration.
1468 ///
1469 /// # Safety
1470 ///
1471 /// - `doc` and `dtd` are zero-initialized by `make_doc_and_dtd` and outlive
1472 /// every dereference; `name` and `content` are NUL-terminated `c_str`
1473 /// allocations; `e1` is asserted non-NULL before `(*e1)` is read, and
1474 /// the hash tables are freed with `entity_deallocator` before `dtd` and
1475 /// `doc` are freed.
1476 fn test_add_entity_duplicate() {
1477 unsafe {
1478 let (doc, dtd) = make_doc_and_dtd();
1479 let name = c_str(b"dup");
1480 let content = c_str(b"original");
1481
1482 let e1 = add_entity(
1483 dtd,
1484 name,
1485 XML_INTERNAL_GENERAL_ENTITY as c_int,
1486 ptr::null(),
1487 ptr::null(),
1488 content,
1489 );
1490 assert!(!e1.is_null());
1491
1492 let e2 = add_entity(
1493 dtd,
1494 name,
1495 XML_INTERNAL_GENERAL_ENTITY as c_int,
1496 ptr::null(),
1497 ptr::null(),
1498 c_str(b"replacement"),
1499 );
1500 assert_eq!(e1, e2); // Same pointer
1501
1502 hash::hash_free(
1503 (*dtd).entities as *mut hash::HashTable,
1504 Some(entity_deallocator),
1505 );
1506 hash::hash_free(
1507 (*dtd).pentities as *mut hash::HashTable,
1508 Some(entity_deallocator),
1509 );
1510 allocator::xmlFreeImpl(dtd as *mut c_void);
1511 allocator::xmlFreeImpl(doc as *mut c_void);
1512 }
1513 }
1514
1515 #[test]
1516 /// Tests that an external parsed entity keeps its SystemID.
1517 ///
1518 /// # Safety
1519 ///
1520 /// - `doc` and `dtd` come from `make_doc_and_dtd` and stay live until the
1521 /// trailing frees; `name` and `sysid` are NUL-terminated `c_str`
1522 /// allocations; `entity` is asserted non-NULL before `(*entity).SystemID`
1523 /// is passed to `string::xml_strcmp`, and the hash tables are freed with
1524 /// `entity_deallocator` before `dtd` and `doc` are freed.
1525 fn test_add_entity_external() {
1526 unsafe {
1527 let (doc, dtd) = make_doc_and_dtd();
1528 let name = c_str(b"extEntity");
1529 let sysid = c_str(b"http://example.com/entity.xml");
1530
1531 let entity = add_entity(
1532 dtd,
1533 name,
1534 XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int,
1535 ptr::null(),
1536 sysid,
1537 ptr::null(),
1538 );
1539 assert!(!entity.is_null());
1540 assert_eq!((*entity).etype, XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int);
1541 assert_eq!(string::xml_strcmp((*entity).SystemID, sysid), 0);
1542
1543 hash::hash_free(
1544 (*dtd).entities as *mut hash::HashTable,
1545 Some(entity_deallocator),
1546 );
1547 hash::hash_free(
1548 (*dtd).pentities as *mut hash::HashTable,
1549 Some(entity_deallocator),
1550 );
1551 allocator::xmlFreeImpl(dtd as *mut c_void);
1552 allocator::xmlFreeImpl(doc as *mut c_void);
1553 }
1554 }
1555
1556 #[test]
1557 /// Tests that `get_entity` returns NULL when the document is NULL.
1558 ///
1559 /// # Safety
1560 ///
1561 /// - `get_entity` checks `doc` for NULL before dereferencing it, and the
1562 /// `name` argument is a NUL-terminated `c_str` allocation that stays
1563 /// alive for the duration of the call.
1564 fn test_get_entity_null_doc() {
1565 unsafe {
1566 let found = get_entity(ptr::null_mut(), c_str(b"test"));
1567 assert!(found.is_null());
1568 }
1569 }
1570
1571 #[test]
1572 /// Tests that `get_entity` returns NULL for an undeclared name.
1573 ///
1574 /// # Safety
1575 ///
1576 /// - `doc` and `dtd` are zero-initialized by `make_doc_and_dtd` and stay
1577 /// live until the trailing frees; the lookup name is a NUL-terminated
1578 /// `c_str` allocation; the hash tables are freed with
1579 /// `entity_deallocator` before `dtd` and `doc` are freed.
1580 fn test_get_entity_not_found() {
1581 unsafe {
1582 let (doc, dtd) = make_doc_and_dtd();
1583 let found = get_entity(doc, c_str(b"nonexistent"));
1584 assert!(found.is_null());
1585
1586 hash::hash_free(
1587 (*dtd).entities as *mut hash::HashTable,
1588 Some(entity_deallocator),
1589 );
1590 hash::hash_free(
1591 (*dtd).pentities as *mut hash::HashTable,
1592 Some(entity_deallocator),
1593 );
1594 allocator::xmlFreeImpl(dtd as *mut c_void);
1595 allocator::xmlFreeImpl(doc as *mut c_void);
1596 }
1597 }
1598
1599 #[test]
1600 /// Tests that `get_parameter_entity` finds a stored parameter entity.
1601 ///
1602 /// # Safety
1603 ///
1604 /// - `doc` and `dtd` come from `make_doc_and_dtd` and stay live until the
1605 /// trailing frees; `name` and `content` are NUL-terminated `c_str`
1606 /// allocations; `entity` is asserted non-NULL before the equality check,
1607 /// and the hash tables are freed with `entity_deallocator` before `dtd`
1608 /// and `doc` are freed.
1609 fn test_get_parameter_entity() {
1610 unsafe {
1611 let (doc, dtd) = make_doc_and_dtd();
1612 let name = c_str(b"param1");
1613 let content = c_str(b"param content");
1614
1615 let entity = add_entity(
1616 dtd,
1617 name,
1618 XML_INTERNAL_PARAMETER_ENTITY as c_int,
1619 ptr::null(),
1620 ptr::null(),
1621 content,
1622 );
1623 assert!(!entity.is_null());
1624
1625 let found = get_parameter_entity(doc, name);
1626 assert_eq!(found, entity);
1627
1628 hash::hash_free(
1629 (*dtd).entities as *mut hash::HashTable,
1630 Some(entity_deallocator),
1631 );
1632 hash::hash_free(
1633 (*dtd).pentities as *mut hash::HashTable,
1634 Some(entity_deallocator),
1635 );
1636 allocator::xmlFreeImpl(dtd as *mut c_void);
1637 allocator::xmlFreeImpl(doc as *mut c_void);
1638 }
1639 }
1640
1641 #[test]
1642 /// Tests that `copy_entity` produces an independent deep copy.
1643 ///
1644 /// # Safety
1645 ///
1646 /// - `entity` is a zero-initialized `xmlMallocZero` allocation whose
1647 /// `name` and `content` fields are fresh `xml_strdup` allocations; the
1648 /// original and the copy are both freed with `free_entity` before the
1649 /// test returns, and every dereference of these raw pointers happens
1650 /// while the allocations are still live.
1651 fn test_copy_entity() {
1652 unsafe {
1653 let name = c_str(b"srcEntity");
1654 let content = c_str(b"source content");
1655
1656 // Create an entity without a DTD
1657 let entity =
1658 allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
1659 assert!(!entity.is_null());
1660 (*entity).type_ = XML_ENTITY_DECL as c_int;
1661 (*entity).name = string::xml_strdup(name);
1662 (*entity).content = string::xml_strdup(content);
1663 (*entity).length = 14;
1664 (*entity).etype = XML_INTERNAL_GENERAL_ENTITY as c_int;
1665
1666 let copy = copy_entity(entity);
1667 assert!(!copy.is_null());
1668 assert_ne!(copy, entity);
1669 assert_eq!((*copy).etype, XML_INTERNAL_GENERAL_ENTITY as c_int);
1670 assert_eq!((*copy).length, 14);
1671 assert_eq!(string::xml_strcmp((*copy).name, name), 0);
1672 assert_eq!(string::xml_strcmp((*copy).content, content), 0);
1673
1674 free_entity(copy);
1675 free_entity(entity);
1676 }
1677 }
1678
1679 #[test]
1680 /// Tests that `copy_entity` returns NULL for a NULL input.
1681 ///
1682 /// # Safety
1683 ///
1684 /// - `copy_entity` checks its argument for NULL and returns early without
1685 /// dereferencing it, so passing `ptr::null_mut()` is sound.
1686 fn test_copy_entity_null() {
1687 unsafe {
1688 assert!(copy_entity(ptr::null_mut()).is_null());
1689 }
1690 }
1691
1692 #[test]
1693 /// Tests that `free_entity` tolerates a NULL pointer without crashing.
1694 ///
1695 /// # Safety
1696 ///
1697 /// - `free_entity` returns early when the pointer is NULL and never
1698 /// dereferences it, so the call is sound.
1699 fn test_free_entity_null() {
1700 unsafe {
1701 free_entity(ptr::null_mut()); // Should not crash
1702 }
1703 }
1704
1705 // ── Entity Type Tests ───────────────────────────────────────────────
1706
1707 #[test]
1708 fn test_is_parameter_entity() {
1709 assert!(is_parameter_entity(XML_INTERNAL_PARAMETER_ENTITY as c_int));
1710 assert!(is_parameter_entity(XML_EXTERNAL_PARAMETER_ENTITY as c_int));
1711 assert!(!is_parameter_entity(XML_INTERNAL_GENERAL_ENTITY as c_int));
1712 assert!(!is_parameter_entity(
1713 XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int
1714 ));
1715 }
1716
1717 #[test]
1718 fn test_is_external_entity() {
1719 assert!(is_external_entity(
1720 XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int
1721 ));
1722 assert!(is_external_entity(
1723 XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int
1724 ));
1725 assert!(is_external_entity(XML_EXTERNAL_PARAMETER_ENTITY as c_int));
1726 assert!(!is_external_entity(XML_INTERNAL_GENERAL_ENTITY as c_int));
1727 assert!(!is_external_entity(XML_INTERNAL_PARAMETER_ENTITY as c_int));
1728 }
1729
1730 #[test]
1731 fn test_is_predefined_entity() {
1732 assert!(is_predefined_entity(
1733 XML_INTERNAL_PREDEFINED_ENTITY as c_int
1734 ));
1735 assert!(!is_predefined_entity(XML_INTERNAL_GENERAL_ENTITY as c_int));
1736 }
1737
1738 // ── Entity Encoding Tests ───────────────────────────────────────────
1739
1740 #[test]
1741 /// Tests that `encode_entities_reentrant` returns NULL for NULL input.
1742 ///
1743 /// # Safety
1744 ///
1745 /// - `encode_entities_reentrant` checks `input` for NULL and returns
1746 /// without dereferencing it, so passing null pointers is sound.
1747 fn test_encode_entities_reentrant_null() {
1748 unsafe {
1749 let result = encode_entities_reentrant(ptr::null_mut(), ptr::null());
1750 assert!(result.is_null());
1751 }
1752 }
1753
1754 #[test]
1755 /// Tests that text without special characters is encoded unchanged.
1756 ///
1757 /// # Safety
1758 ///
1759 /// - `input` is a NUL-terminated `c_str` allocation that stays alive until
1760 /// the comparison; `result` is the freshly allocated output of
1761 /// `encode_entities_reentrant`, asserted non-NULL, and is freed with
1762 /// `xmlFreeImpl` after the comparison.
1763 fn test_encode_entities_reentrant_no_special() {
1764 unsafe {
1765 let input = c_str(b"Hello, World!");
1766 let result = encode_entities_reentrant(ptr::null_mut(), input);
1767 assert!(!result.is_null());
1768 assert_eq!(string::xml_strcmp(result, input), 0);
1769 allocator::xmlFreeImpl(result as *mut c_void);
1770 }
1771 }
1772
1773 #[test]
1774 /// Tests that the `lt` and `gt` characters are encoded to entities.
1775 ///
1776 /// # Safety
1777 ///
1778 /// - `input` and `expected` are NUL-terminated `c_str` allocations that
1779 /// stay alive until the comparison; `result` is the freshly allocated
1780 /// output of `encode_entities_reentrant`, asserted non-NULL, and is
1781 /// freed with `xmlFreeImpl` after the comparison.
1782 fn test_encode_entities_reentrant_lt_gt() {
1783 unsafe {
1784 let input = c_str(b"a < b > c");
1785 let result = encode_entities_reentrant(ptr::null_mut(), input);
1786 assert!(!result.is_null());
1787 let expected = c_str(b"a < b > c");
1788 assert_eq!(string::xml_strcmp(result, expected), 0);
1789 allocator::xmlFreeImpl(result as *mut c_void);
1790 }
1791 }
1792
1793 #[test]
1794 /// Tests that the `amp` character is encoded to an entity.
1795 ///
1796 /// # Safety
1797 ///
1798 /// - `input` and `expected` are NUL-terminated `c_str` allocations that
1799 /// stay alive until the comparison; `result` is the freshly allocated
1800 /// output of `encode_entities_reentrant`, asserted non-NULL, and is
1801 /// freed with `xmlFreeImpl` after the comparison.
1802 fn test_encode_entities_reentrant_amp() {
1803 unsafe {
1804 let input = c_str(b"a & b");
1805 let result = encode_entities_reentrant(ptr::null_mut(), input);
1806 assert!(!result.is_null());
1807 let expected = c_str(b"a & b");
1808 assert_eq!(string::xml_strcmp(result, expected), 0);
1809 allocator::xmlFreeImpl(result as *mut c_void);
1810 }
1811 }
1812
1813 #[test]
1814 /// Tests that quotes and apostrophes are encoded to entities.
1815 ///
1816 /// # Safety
1817 ///
1818 /// - `input` and `expected` are NUL-terminated `c_str` allocations that
1819 /// stay alive until the comparison; `result` is the freshly allocated
1820 /// output of `encode_entities_reentrant`, asserted non-NULL, and is
1821 /// freed with `xmlFreeImpl` after the comparison.
1822 fn test_encode_entities_reentrant_quotes() {
1823 unsafe {
1824 let input = c_str(b"\"hello\" 'world'");
1825 let result = encode_entities_reentrant(ptr::null_mut(), input);
1826 assert!(!result.is_null());
1827 let expected = c_str(b""hello" 'world'");
1828 assert_eq!(string::xml_strcmp(result, expected), 0);
1829 allocator::xmlFreeImpl(result as *mut c_void);
1830 }
1831 }
1832
1833 #[test]
1834 /// Tests that a mixed string is fully encoded in one pass.
1835 ///
1836 /// # Safety
1837 ///
1838 /// - `input` and `expected` are NUL-terminated `c_str` allocations that
1839 /// stay alive until the comparison; `result` is the freshly allocated
1840 /// output of `encode_entities_reentrant`, asserted non-NULL, and is
1841 /// freed with `xmlFreeImpl` after the comparison.
1842 fn test_encode_entities_reentrant_all() {
1843 unsafe {
1844 let input = c_str(b"<tag attr=\"value\">&'more'</tag>");
1845 let result = encode_entities_reentrant(ptr::null_mut(), input);
1846 assert!(!result.is_null());
1847 let expected =
1848 c_str(b"<tag attr="value">&'more'</tag>");
1849 assert_eq!(string::xml_strcmp(result, expected), 0);
1850 allocator::xmlFreeImpl(result as *mut c_void);
1851 }
1852 }
1853
1854 // ── Entity Decoding Tests ───────────────────────────────────────────
1855
1856 #[test]
1857 /// Tests that `string_decode_entities` returns NULL for a NULL input.
1858 ///
1859 /// # Safety
1860 ///
1861 /// - `string_decode_entities` returns early when `input` is NULL, and the
1862 /// `doc` argument is only dereferenced for entity lookup during
1863 /// decoding, so passing null pointers here is sound.
1864 fn test_decode_entities_null_input() {
1865 unsafe {
1866 let result = string_decode_entities(ptr::null_mut(), ptr::null(), 0, 0, 0, 0);
1867 assert!(result.is_null());
1868 }
1869 }
1870
1871 #[test]
1872 /// Tests that decoding an empty string yields an empty NUL-terminated
1873 /// result.
1874 ///
1875 /// # Safety
1876 ///
1877 /// - `input` is a NUL-terminated `c_str` allocation valid for the call;
1878 /// `result` is asserted non-NULL before the `*result` dereference and is
1879 /// freed with `xmlFreeImpl` before the test ends.
1880 fn test_decode_entities_empty() {
1881 unsafe {
1882 let input = c_str(b"");
1883 let result = string_decode_entities(ptr::null_mut(), input, 0, 0, 0, 0);
1884 assert!(!result.is_null());
1885 assert_eq!(*result, 0);
1886 allocator::xmlFreeImpl(result as *mut c_void);
1887 }
1888 }
1889
1890 #[test]
1891 /// Tests that input without references is decoded unchanged.
1892 ///
1893 /// # Safety
1894 ///
1895 /// - `input` is a NUL-terminated `c_str` allocation that stays alive until
1896 /// the comparison; `result` is asserted non-NULL before being passed to
1897 /// `string::xml_strcmp` and is freed with `xmlFreeImpl` afterwards.
1898 fn test_decode_entities_no_refs() {
1899 unsafe {
1900 let input = c_str(b"Hello, World!");
1901 let result = string_decode_entities(ptr::null_mut(), input, 0, 0, 0, 0);
1902 assert!(!result.is_null());
1903 assert_eq!(string::xml_strcmp(result, input), 0);
1904 allocator::xmlFreeImpl(result as *mut c_void);
1905 }
1906 }
1907
1908 #[test]
1909 /// Tests that a decimal numeric character reference is decoded.
1910 ///
1911 /// # Safety
1912 ///
1913 /// - `input` and `expected` are NUL-terminated `c_str` allocations that
1914 /// stay alive until the comparison; `result` is asserted non-NULL before
1915 /// being passed to `string::xml_strcmp` and is freed with `xmlFreeImpl`
1916 /// afterwards.
1917 fn test_decode_numeric_decimal() {
1918 unsafe {
1919 // A = 'A'
1920 let input = c_str(b"A");
1921 let result = string_decode_entities(ptr::null_mut(), input, 0, 0, 0, 0);
1922 assert!(!result.is_null());
1923 let expected = c_str(b"A");
1924 assert_eq!(string::xml_strcmp(result, expected), 0);
1925 allocator::xmlFreeImpl(result as *mut c_void);
1926 }
1927 }
1928
1929 #[test]
1930 /// Tests that a hexadecimal numeric character reference is decoded.
1931 ///
1932 /// # Safety
1933 ///
1934 /// - `input` and `expected` are NUL-terminated `c_str` allocations that
1935 /// stay alive until the comparison; `result` is asserted non-NULL before
1936 /// being passed to `string::xml_strcmp` and is freed with `xmlFreeImpl`
1937 /// afterwards.
1938 fn test_decode_numeric_hex() {
1939 unsafe {
1940 // A = 'A'
1941 let input = c_str(b"A");
1942 let result = string_decode_entities(ptr::null_mut(), input, 0, 0, 0, 0);
1943 assert!(!result.is_null());
1944 let expected = c_str(b"A");
1945 assert_eq!(string::xml_strcmp(result, expected), 0);
1946 allocator::xmlFreeImpl(result as *mut c_void);
1947 }
1948 }
1949
1950 #[test]
1951 /// Tests that mixed decimal and hexadecimal references are decoded.
1952 ///
1953 /// # Safety
1954 ///
1955 /// - `input` and `expected` are NUL-terminated `c_str` allocations that
1956 /// stay alive until the comparison; `result` is asserted non-NULL before
1957 /// being passed to `string::xml_strcmp` and is freed with `xmlFreeImpl`
1958 /// afterwards.
1959 fn test_decode_numeric_mixed() {
1960 unsafe {
1961 let input = c_str(b"Hello World!"); // Hello World!
1962 let result = string_decode_entities(ptr::null_mut(), input, 0, 0, 0, 0);
1963 assert!(!result.is_null());
1964 let expected = c_str(b"Hello World!");
1965 assert_eq!(string::xml_strcmp(result, expected), 0);
1966 allocator::xmlFreeImpl(result as *mut c_void);
1967 }
1968 }
1969
1970 #[test]
1971 /// Tests that predefined general entities in the DTD are substituted.
1972 ///
1973 /// # Safety
1974 ///
1975 /// - `doc` and `dtd` are zero-initialized by `make_doc_and_dtd` and stay
1976 /// live until the trailing frees; the `input` and `expected` buffers are
1977 /// NUL-terminated `c_str` allocations; `result` is asserted non-NULL
1978 /// before the comparison and freed with `xmlFreeImpl`; the hash tables
1979 /// are freed with `entity_deallocator` before `dtd` and `doc` are freed.
1980 fn test_decode_predefined_entities() {
1981 unsafe {
1982 let (doc, dtd) = make_doc_and_dtd();
1983 let input = c_str(b"a < b > c & d");
1984
1985 // Add the entities to the DTD so get_entity can find them
1986 add_entity(
1987 dtd,
1988 c_str(b"lt"),
1989 XML_INTERNAL_GENERAL_ENTITY as c_int,
1990 ptr::null(),
1991 ptr::null(),
1992 c_str(b"<"),
1993 );
1994 add_entity(
1995 dtd,
1996 c_str(b"gt"),
1997 XML_INTERNAL_GENERAL_ENTITY as c_int,
1998 ptr::null(),
1999 ptr::null(),
2000 c_str(b">"),
2001 );
2002 add_entity(
2003 dtd,
2004 c_str(b"amp"),
2005 XML_INTERNAL_GENERAL_ENTITY as c_int,
2006 ptr::null(),
2007 ptr::null(),
2008 c_str(b"&"),
2009 );
2010
2011 let result = string_decode_entities(doc, input, 0, 0, 0, 0);
2012 assert!(!result.is_null());
2013 let expected = c_str(b"a < b > c & d");
2014 assert_eq!(string::xml_strcmp(result, expected), 0);
2015
2016 allocator::xmlFreeImpl(result as *mut c_void);
2017
2018 hash::hash_free(
2019 (*dtd).entities as *mut hash::HashTable,
2020 Some(entity_deallocator),
2021 );
2022 hash::hash_free(
2023 (*dtd).pentities as *mut hash::HashTable,
2024 Some(entity_deallocator),
2025 );
2026 allocator::xmlFreeImpl(dtd as *mut c_void);
2027 allocator::xmlFreeImpl(doc as *mut c_void);
2028 }
2029 }
2030
2031 // ── Security/Limit Tests ────────────────────────────────────────────
2032
2033 #[test]
2034 fn test_check_entity_expansion_limit() {
2035 assert_eq!(check_entity_expansion_limit(100), 0);
2036 assert_eq!(check_entity_expansion_limit(1_000_000), 0);
2037 assert_eq!(check_entity_expansion_limit(1_000_001), -1);
2038 }
2039
2040 #[test]
2041 fn test_check_entity_recursion_depth() {
2042 assert_eq!(check_entity_recursion_depth(10), 0);
2043 assert_eq!(check_entity_recursion_depth(32), 0);
2044 assert_eq!(check_entity_recursion_depth(33), -1);
2045 }
2046
2047 // ── Entity Content Tests ────────────────────────────────────────────
2048
2049 #[test]
2050 /// Tests that `get_entity_content` returns a copy of the entity content.
2051 ///
2052 /// # Safety
2053 ///
2054 /// - `entity` is a zero-initialized `xmlMallocZero` allocation whose
2055 /// `content` field is a fresh `xml_strdup` allocation; `retrieved` is
2056 /// asserted non-NULL before the comparison and freed with `xmlFreeImpl`,
2057 /// and `entity` is freed with `free_entity` before the test returns.
2058 fn test_get_entity_content() {
2059 unsafe {
2060 let content = c_str(b"entity content");
2061 let entity =
2062 allocator::xmlMallocZero(size_of::<_xmlEntity>() as usize) as *mut _xmlEntity;
2063 assert!(!entity.is_null());
2064 (*entity).content = string::xml_strdup(content);
2065
2066 let retrieved = get_entity_content(entity);
2067 assert!(!retrieved.is_null());
2068 assert_eq!(string::xml_strcmp(retrieved, content), 0);
2069
2070 allocator::xmlFreeImpl(retrieved as *mut c_void);
2071 free_entity(entity);
2072 }
2073 }
2074
2075 #[test]
2076 /// Tests that `get_entity_content` returns NULL for a NULL entity.
2077 ///
2078 /// # Safety
2079 ///
2080 /// - `get_entity_content` checks its argument for NULL and returns early
2081 /// without dereferencing it, so passing `ptr::null_mut()` is sound.
2082 fn test_get_entity_content_null() {
2083 unsafe {
2084 assert!(get_entity_content(ptr::null_mut()).is_null());
2085 }
2086 }
2087}