libxml_rs/xml/debug/mod.rs
1//! Debug/memory debugging infrastructure (§85 Phase 7).
2//!
3//! UPSTREAM-PARITY: Corresponds to `debugXML.c` / `debugXML.h` in libxml2.
4//!
5//! The libxml2 debug APIs for printing tree structure, XPath expressions, etc.
6//! These are used by `xmllint --debug` and other diagnostic tools.
7//!
8//! # Upstream contract
9//!
10//! Mirrors upstream `debugXML.c` / `debugXML.h`
11//! (`SRC-LIBXML2-2.15.0-DEBUGXML-C`, parity target libxml2 2.15.3 oracle):
12//! `xmlDebugDumpNode`, `xmlDebugDumpNodeList`, `xmlDebugDumpOneNode`,
13//! `xmlDebugDumpAttrList`, `xmlDebugDumpString` and the `xmllint --debug`
14//! tree dump format.
15//!
16//! # Conceptual behavior
17//!
18//! Implements the recursive debug dumper: nodes are printed with type,
19//! name, content, attributes, namespaces, line numbers and children at
20//! increasing indentation (`MAX_DEPTH` guard), plus the XPath expression
21//! and string dumps used by xmllint diagnostics.
22//!
23//! # Ownership & safety invariants
24//!
25//! The dump functions borrow the tree — they never free or detach nodes;
26//! output goes to the caller-provided FILE* (or a buffer) that the caller
27//! owns. Namespace strings are read via their href (bounded scans), never
28//! stored.
29//!
30//! # Historical quirks & epochs
31//!
32//! The dump format is one of the most stable surfaces: the debug-simple /
33//! debug-dtd / debug-nodes / debug-ns matrix cases are byte-identical
34//! across the whole 2.7.8 → 2.15.3 span (SEMANTIC_EPOCHS.md stable cases).
35//! One deliberate epoch join: entity content children dump as `TEXT compact`
36//! since 2.13.0 (E-004, commit 8d04f0ee 2024-03-11, R-000119) — the
37//! candidate matches the 2.13+ epoch, i.e. the current oracle.
38//!
39//! # Deliberate oddities
40//!
41//! The `TEXT compact` marker is reproduced exactly (including the
42//! 14/15-byte compact threshold that is unchanged in every tested version),
43//! rather than normalized to plain `TEXT`.
44//!
45//! # Proving courts
46//!
47//! The xmllint --debug CLI differential courts and the debug-* matrix cases
48//! compare dumps byte-identical against each built oracle (2.7.8 through
49//! 2.15.3); cargo test covers the unit dump suites.
50//!
51//! # Tempting simplifications that would break parity
52//!
53//! Do not normalize the dump format (e.g. always printing `TEXT` or
54//! collapsing attributes): xmllint --debug output is a byte-exact parity
55//! target and downstream tooling parses it. Do not drop the compact-text
56//! threshold logic — R-000119/E-004 depend on it.
57//!
58//! # Safety
59//!
60//! This module contains no top-level `unsafe` blocks, no `static mut` items
61//! and no module-level `unsafe` code; every `unsafe` operation lives inside
62//! the individual `unsafe fn` entry points and their tests below, each
63//! carrying its own `# Safety` contract. The module-level invariant for the
64//! exported dump surface is that all raw pointers (output `FILE*`, `_xmlDoc`,
65//! `_xmlNode`, `_xmlAttr`, `_xmlElement`, `_xmlEntity` and `xmlChar` strings)
66//! are borrowed for the duration of each call — never stored, freed or
67//! detached — and must point to live, unmutated objects; NULL is accepted
68//! wherever the upstream C API allows it.
69
70use crate::abi::structs::{_xmlAttr, _xmlDoc, _xmlNode};
71use core::ffi::{c_char, c_int, c_void};
72use core::ptr;
73
74/// Maximum indentation depth for debug output.
75const MAX_DEPTH: c_int = 100;
76
77/// Check if a node is an XInclude start node.
78///
79/// UPSTREAM-PARITY: `xmlDebugIsXInclude()` — internal check used by debug dumper.
80///
81/// # Safety
82///
83/// - `node` must be NULL or a valid pointer to a live `_xmlNode`.
84/// - If non-NULL, `(*node).ns` must be NULL or a valid `_xmlNs` whose `href`
85/// points to a region of at least 30 readable bytes, and `(*node).name`
86/// must be NULL or point to at least 8 readable bytes (both are scanned
87/// with `from_raw_parts` at fixed lengths).
88/// - The node and its namespace must not be mutated concurrently during the
89/// call.
90fn is_xinclude_node(node: *mut _xmlNode) -> bool {
91 if node.is_null() {
92 return false;
93 }
94 unsafe {
95 let ns = (*node).ns;
96 if ns.is_null() {
97 return false;
98 }
99 let ns_href = (*ns).href;
100 let _ns_prefix = (*ns).prefix;
101 if ns_href.is_null() {
102 return false;
103 }
104 // Check for XInclude namespace
105 let href = core::slice::from_raw_parts(ns_href, 30);
106 let xi_ns = b"http://www.w3.org/2001/XInclude\0";
107 let mut matches = true;
108 for i in 0..30 {
109 if i >= href.len() || href[i] != xi_ns[i] {
110 matches = false;
111 break;
112 }
113 }
114 if !matches {
115 return false;
116 }
117 // Check for xi:include element
118 let name_bytes = if !(*node).name.is_null() {
119 core::slice::from_raw_parts((*node).name, 8)
120 } else {
121 return false;
122 };
123 name_bytes.len() >= 7 && &name_bytes[..7] == b"include"
124 }
125}
126
127/// Convert a boolean to text.
128///
129/// UPSTREAM-PARITY: `xmlBoolToText()`
130///
131/// # SAFETY
132///
133/// The function touches crate-global state only; it is safe
134/// as long as the caller respects the library's global
135/// initialization/cleanup ordering (xmlInitParser before use,
136/// xmlCleanupParser only after all users are done).
137///
138/// Violating the global lifecycle ordering, or calling this after
139/// teardown or from a signal handler, is undefined behavior.
140#[no_mangle]
141pub const unsafe extern "C" fn xmlBoolToText(boolval: c_int) -> *const c_char {
142 if boolval != 0 {
143 c"true".as_ptr() as *const c_char
144 } else {
145 c"false".as_ptr() as *const c_char
146 }
147}
148
149/// Dump a debug representation of an xmlChar string.
150///
151/// UPSTREAM-PARITY: `xmlDebugDumpString()` — line breaks, tabs and CRs are
152/// rendered as a single space.
153///
154/// # SAFETY
155///
156/// - `output`, `str_val` must be valid pointers (or NULL
157/// where the upstream C contract allows), obtained from the
158/// matching constructor/owner and not yet freed; the callee may
159/// take or keep ownership exactly as the C API specifies.
160///
161/// The caller must not race this call with concurrent mutation of the
162/// same objects from other threads (per-object state is not internally
163/// synchronized). Violating any of the above is undefined behavior.
164///
165/// Exercised by the C-API differential courts
166/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
167/// courts; those pass byte-for-byte against the upstream oracle.
168#[no_mangle]
169pub unsafe extern "C" fn xmlDebugDumpString(output: *mut _IO_FILE, str_val: *const u8) {
170 if output.is_null() {
171 return;
172 }
173 if str_val.is_null() {
174 unsafe {
175 libc::fprintf(output, c"(NULL)".as_ptr() as *const c_char);
176 }
177 return;
178 }
179 unsafe {
180 // UPSTREAM-PARITY: xmlCtxtDumpString prints at most 40 characters;
181 // blank characters become spaces, bytes >= 0x80 are printed as
182 // `#%X`, and a longer string is truncated with "...".
183 let mut i = 0;
184 while i < 40 {
185 let c = *str_val.add(i);
186 if c == 0 {
187 return;
188 }
189 if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' {
190 libc::fprintf(output, c" ".as_ptr() as *const c_char);
191 } else if c >= 0x80 {
192 libc::fprintf(output, c"#%X".as_ptr() as *const c_char, c as c_int);
193 } else {
194 libc::fprintf(output, c"%c".as_ptr() as *const c_char, c as c_int);
195 }
196 i += 1;
197 }
198 libc::fprintf(output, c"...".as_ptr() as *const c_char);
199 }
200}
201
202/// Dump a debug representation of an attribute.
203///
204/// UPSTREAM-PARITY: `xmlDebugDumpAttr()`
205///
206/// # SAFETY
207///
208/// - `output`, `attr` must be valid pointers (or NULL
209/// where the upstream C contract allows), obtained from the
210/// matching constructor/owner and not yet freed; the callee may
211/// take or keep ownership exactly as the C API specifies.
212///
213/// The caller must not race this call with concurrent mutation of the
214/// same objects from other threads (per-object state is not internally
215/// synchronized). Violating any of the above is undefined behavior.
216///
217/// Exercised by the C-API differential courts
218/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
219/// courts; those pass byte-for-byte against the upstream oracle.
220#[no_mangle]
221pub unsafe extern "C" fn xmlDebugDumpAttr(
222 output: *mut _IO_FILE,
223 attr: *mut _xmlAttr,
224 depth: c_int,
225) {
226 if output.is_null() || attr.is_null() {
227 return;
228 }
229 unsafe {
230 for _ in 0..depth {
231 libc::fprintf(output, c" ".as_ptr() as *const c_char);
232 }
233 libc::fprintf(output, c"ATTRIBUTE ".as_ptr() as *const c_char);
234 xmlDebugDumpString(output, (*attr).name);
235 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
236 // The attribute value is dumped as a compact text node child.
237 xmlDebugDumpNode(output, (*attr).children, depth + 1);
238 }
239}
240
241/// Dump a debug representation of an attribute list.
242///
243/// UPSTREAM-PARITY: `xmlDebugDumpAttrList()`
244///
245/// # SAFETY
246///
247/// - `output`, `attr` must be valid pointers (or NULL
248/// where the upstream C contract allows), obtained from the
249/// matching constructor/owner and not yet freed; the callee may
250/// take or keep ownership exactly as the C API specifies.
251///
252/// The caller must not race this call with concurrent mutation of the
253/// same objects from other threads (per-object state is not internally
254/// synchronized). Violating any of the above is undefined behavior.
255///
256/// Exercised by the C-API differential courts
257/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
258/// courts; those pass byte-for-byte against the upstream oracle.
259#[no_mangle]
260pub unsafe extern "C" fn xmlDebugDumpAttrList(
261 output: *mut _IO_FILE,
262 attr: *mut _xmlAttr,
263 depth: c_int,
264) {
265 if output.is_null() {
266 return;
267 }
268 let mut cur = attr;
269 while !cur.is_null() {
270 unsafe {
271 xmlDebugDumpAttr(output, cur, depth);
272 cur = (*cur).next;
273 }
274 }
275}
276
277/// Dump a single node for debug output.
278///
279/// UPSTREAM-PARITY: `xmlDebugDumpOneNode()`
280///
281/// # SAFETY
282///
283/// - `output`, `node` must be valid pointers (or NULL
284/// where the upstream C contract allows), obtained from the
285/// matching constructor/owner and not yet freed; the callee may
286/// take or keep ownership exactly as the C API specifies.
287///
288/// The caller must not race this call with concurrent mutation of the
289/// same objects from other threads (per-object state is not internally
290/// synchronized). Violating any of the above is undefined behavior.
291///
292/// Exercised by the C-API differential courts
293/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
294/// courts; those pass byte-for-byte against the upstream oracle.
295#[no_mangle]
296pub unsafe extern "C" fn xmlDebugDumpOneNode(
297 output: *mut _IO_FILE,
298 node: *mut _xmlNode,
299 depth: c_int,
300) {
301 if output.is_null() || node.is_null() {
302 return;
303 }
304 unsafe {
305 // Indent
306 for _ in 0..depth {
307 libc::fprintf(output, c" ".as_ptr() as *const c_char);
308 }
309
310 // Print node type
311 match (*node).type_ {
312 1 => {
313 // XML_ELEMENT_NODE
314 libc::fprintf(output, c"ELEMENT ".as_ptr() as *const c_char);
315 // QName: prefix:name when a namespace prefix is present.
316 if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
317 libc::fprintf(
318 output,
319 c"%s:".as_ptr() as *const c_char,
320 (*(*node).ns).prefix,
321 );
322 }
323 xmlDebugDumpString(output, (*node).name);
324 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
325
326 // Namespace declarations on the element (upstream prints them
327 // before attributes).
328 if !(*node).nsDef.is_null() {
329 let mut ns = (*node).nsDef;
330 while !ns.is_null() {
331 for _ in 0..(depth + 1) {
332 libc::fprintf(output, c" ".as_ptr() as *const c_char);
333 }
334 libc::fprintf(output, c"namespace ".as_ptr() as *const c_char);
335 if (*ns).prefix.is_null() {
336 libc::fprintf(output, c" ".as_ptr() as *const c_char);
337 } else {
338 libc::fprintf(output, c"%s".as_ptr() as *const c_char, (*ns).prefix);
339 }
340 libc::fprintf(output, c" href=".as_ptr() as *const c_char);
341 libc::fprintf(output, c"%s\n".as_ptr() as *const c_char, (*ns).href);
342 ns = (*ns).next;
343 }
344 }
345
346 // Attributes
347 if !(*node).properties.is_null() {
348 xmlDebugDumpAttrList(output, (*node).properties, depth + 1);
349 }
350 }
351 2 => {
352 // XML_ATTRIBUTE_NODE
353 libc::fprintf(output, c"ATTRIBUTE ".as_ptr() as *const c_char);
354 xmlDebugDumpString(output, (*node).name);
355 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
356 }
357 3 => {
358 // XML_TEXT_NODE
359 libc::fprintf(output, c"TEXT".as_ptr() as *const c_char);
360 // UPSTREAM-PARITY: debugXML.c marks compact text via
361 // `node->content == (xmlChar *) &(node->properties)`.
362 let inline_addr = std::ptr::addr_of_mut!((*node).properties) as *const c_void;
363 if (*node).content as *const c_void == inline_addr {
364 libc::fprintf(output, c" compact".as_ptr() as *const c_char);
365 }
366 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
367 for _ in 0..(depth + 1) {
368 libc::fprintf(output, c" ".as_ptr() as *const c_char);
369 }
370 libc::fprintf(output, c"content=".as_ptr() as *const c_char);
371 if !(*node).content.is_null() {
372 xmlDebugDumpString(output, (*node).content as *const u8);
373 } else {
374 let c = crate::xml::tree::node_get_content(node);
375 if !c.is_null() {
376 libc::fprintf(output, c"%s".as_ptr() as *const c_char, c);
377 crate::abi::allocator::xmlFreeImpl(c as *mut c_void);
378 }
379 }
380 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
381 }
382 4 => {
383 // XML_CDATA_SECTION_NODE
384 libc::fprintf(output, c"CDATA_SECTION\n".as_ptr() as *const c_char);
385 for _ in 0..(depth + 1) {
386 libc::fprintf(output, c" ".as_ptr() as *const c_char);
387 }
388 libc::fprintf(output, c"content=".as_ptr() as *const c_char);
389 xmlDebugDumpString(output, (*node).content as *const u8);
390 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
391 }
392 5 => {
393 // XML_ENTITY_REF_NODE
394 libc::fprintf(output, c"ENTITY_REF(".as_ptr() as *const c_char);
395 xmlDebugDumpString(output, (*node).name);
396 libc::fprintf(output, c")\n".as_ptr() as *const c_char);
397 // The referenced entity's declaration.
398 let doc = (*node).doc;
399 let ent = if doc.is_null() {
400 ptr::null_mut()
401 } else {
402 crate::xml::entities::get_entity(doc, (*node).name)
403 };
404 if !ent.is_null() {
405 for _ in 0..(depth + 1) {
406 libc::fprintf(output, c" ".as_ptr() as *const c_char);
407 }
408 let etype = (*ent).etype;
409 let etype_name = entity_type_name(etype);
410 libc::fprintf(
411 output,
412 c"%s ".as_ptr() as *const c_char,
413 etype_name.as_ptr() as *const c_char,
414 );
415 libc::fprintf(output, c"%s\n".as_ptr() as *const c_char, (*ent).name);
416 for _ in 0..(depth + 1) {
417 libc::fprintf(output, c" ".as_ptr() as *const c_char);
418 }
419 libc::fprintf(output, c"content=".as_ptr() as *const c_char);
420 if !(*ent).content.is_null() {
421 xmlDebugDumpString(output, (*ent).content as *const u8);
422 }
423 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
424 }
425 }
426 6 => {
427 // XML_ENTITY_NODE
428 libc::fprintf(output, c"ENTITYDECL(".as_ptr() as *const c_char);
429 xmlDebugDumpString(output, (*node).name);
430 libc::fprintf(output, c")".as_ptr() as *const c_char);
431 if !(*node).content.is_null() {
432 libc::fprintf(output, c", internal\n ".as_ptr() as *const c_char);
433 libc::fprintf(output, c"content=".as_ptr() as *const c_char);
434 xmlDebugDumpString(output, (*node).content as *const u8);
435 }
436 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
437 }
438 7 => {
439 // XML_PI_NODE
440 libc::fprintf(output, c"PI ".as_ptr() as *const c_char);
441 xmlDebugDumpString(output, (*node).name);
442 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
443 if !(*node).content.is_null() {
444 for _ in 0..(depth + 1) {
445 libc::fprintf(output, c" ".as_ptr() as *const c_char);
446 }
447 libc::fprintf(output, c"content=".as_ptr() as *const c_char);
448 xmlDebugDumpString(output, (*node).content as *const u8);
449 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
450 }
451 }
452 8 => {
453 // XML_COMMENT_NODE
454 libc::fprintf(output, c"COMMENT\n".as_ptr() as *const c_char);
455 for _ in 0..(depth + 1) {
456 libc::fprintf(output, c" ".as_ptr() as *const c_char);
457 }
458 libc::fprintf(output, c"content=".as_ptr() as *const c_char);
459 xmlDebugDumpString(output, (*node).content as *const u8);
460 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
461 }
462 9 => {
463 // XML_DOCUMENT_NODE
464 libc::fprintf(output, c"DOCUMENT".as_ptr() as *const c_char);
465 }
466 10 => {
467 // XML_DOCUMENT_TYPE_NODE
468 libc::fprintf(output, c"DOCTYPE".as_ptr() as *const c_char);
469 }
470 14 => {
471 // XML_DTD_NODE. UPSTREAM-PARITY: xmlCtxtDumpDtdNode prints
472 // `DTD(name)`, `, PUBLIC extID` and `, SYSTEM sysID` all on
473 // one line.
474 libc::fprintf(output, c"DTD(".as_ptr() as *const c_char);
475 xmlDebugDumpString(output, (*node).name);
476 libc::fprintf(output, c")".as_ptr() as *const c_char);
477 let dtd = node as *mut crate::abi::structs::_xmlDtd;
478 if !(*dtd).ExternalID.is_null() {
479 libc::fprintf(output, c", PUBLIC ".as_ptr() as *const c_char);
480 libc::fprintf(output, c"%s".as_ptr() as *const c_char, (*dtd).ExternalID);
481 }
482 if !(*dtd).SystemID.is_null() {
483 libc::fprintf(output, c", SYSTEM ".as_ptr() as *const c_char);
484 libc::fprintf(output, c"%s".as_ptr() as *const c_char, (*dtd).SystemID);
485 }
486 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
487 // Element declarations (hash table).
488 let dtd = node as *mut crate::abi::structs::_xmlDtd;
489 if !(*dtd).elements.is_null() {
490 let ctx = DtdDumpCtx {
491 output,
492 depth: depth + 1,
493 };
494 crate::xml::hash::hash_scan(
495 (*dtd).elements as *mut crate::xml::hash::HashTable,
496 Some(dump_elemscan_cb),
497 &ctx as *const DtdDumpCtx as *mut c_void,
498 );
499 }
500 // Entity declarations.
501 if !(*dtd).entities.is_null() {
502 let ctx = DtdDumpCtx {
503 output,
504 depth: depth + 1,
505 };
506 crate::xml::hash::hash_scan(
507 (*dtd).entities as *mut crate::xml::hash::HashTable,
508 Some(dump_entityscan_cb),
509 &ctx as *const DtdDumpCtx as *mut c_void,
510 );
511 }
512 }
513 13 => {
514 // XML_HTML_DOCUMENT_NODE
515 libc::fprintf(output, c"HTML DOCUMENT".as_ptr() as *const c_char);
516 }
517 18 => {
518 // XML_NAMESPACE_DECL
519 libc::fprintf(output, c"NAMESPACE".as_ptr() as *const c_char);
520 if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
521 libc::fprintf(
522 output,
523 c" %s=%s".as_ptr() as *const c_char,
524 (*(*node).ns).prefix,
525 (*(*node).ns).href,
526 );
527 }
528 }
529 19 => {
530 // XML_XINCLUDE_START
531 if is_xinclude_node(node) {
532 libc::fprintf(output, c"XINCLUDE".as_ptr() as *const c_char);
533 } else {
534 libc::fprintf(output, c"XINCLUDE_START".as_ptr() as *const c_char);
535 }
536 }
537 20 => {
538 // XML_XINCLUDE_END
539 libc::fprintf(output, c"XINCLUDE_END".as_ptr() as *const c_char);
540 }
541 _ => {
542 libc::fprintf(
543 output,
544 c"UNKNOWN (%d)".as_ptr() as *const c_char,
545 (*node).type_ as c_int,
546 );
547 }
548 }
549 }
550}
551
552/// Map an entity type to its upstream debug name.
553fn entity_type_name(etype: c_int) -> Vec<u8> {
554 use crate::abi::types::xmlEntityType::*;
555 match etype {
556 t if t == XML_INTERNAL_GENERAL_ENTITY as c_int => b"INTERNAL_GENERAL_ENTITY\0".to_vec(),
557 t if t == XML_INTERNAL_PARAMETER_ENTITY as c_int => b"INTERNAL_PARAMETER_ENTITY\0".to_vec(),
558 t if t == XML_EXTERNAL_GENERAL_PARSED_ENTITY as c_int => {
559 b"EXTERNAL_GENERAL_PARSED_ENTITY\0".to_vec()
560 }
561 t if t == XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int => {
562 b"EXTERNAL_GENERAL_UNPARSED_ENTITY\0".to_vec()
563 }
564 t if t == XML_EXTERNAL_PARAMETER_ENTITY as c_int => b"EXTERNAL_PARAMETER_ENTITY\0".to_vec(),
565 t if t == XML_INTERNAL_PREDEFINED_ENTITY as c_int => {
566 b"INTERNAL_PREDEFINED_ENTITY\0".to_vec()
567 }
568 _ => b"UNKNOWN_ENTITY\0".to_vec(),
569 }
570}
571
572/// Context for DTD hash-scan debug callbacks.
573#[repr(C)]
574struct DtdDumpCtx {
575 output: *mut _IO_FILE,
576 depth: c_int,
577}
578
579/// Dump an element declaration (`ELEMDECL(name), TYPE (model)`).
580///
581/// # Safety
582///
583/// - `payload` must be NULL or a valid pointer to a live `_xmlElement` whose
584/// `name` is NULL or a NUL-terminated string and whose `content` is NULL or
585/// a valid content-model tree.
586/// - `data` must be NULL or a valid pointer to a `DtdDumpCtx` whose `output`
587/// is a valid writable `FILE*` and whose `depth` is non-negative.
588/// - The element and context must not be mutated concurrently during the
589/// call.
590unsafe extern "C" fn dump_elemscan_cb(payload: *mut c_void, data: *mut c_void, _name: *const u8) {
591 if payload.is_null() || data.is_null() {
592 return;
593 }
594 let ctx = unsafe { &*(data as *const DtdDumpCtx) };
595 let elem = unsafe { &*(payload as *mut crate::abi::structs::_xmlElement) };
596 unsafe {
597 for _ in 0..ctx.depth {
598 libc::fprintf(ctx.output, c" ".as_ptr() as *const c_char);
599 }
600 libc::fprintf(ctx.output, c"ELEMDECL(".as_ptr() as *const c_char);
601 if !elem.name.is_null() {
602 libc::fprintf(ctx.output, c"%s".as_ptr() as *const c_char, elem.name);
603 }
604 libc::fprintf(ctx.output, c")".as_ptr() as *const c_char);
605 // UPSTREAM-PARITY: 2.15 prints the MIXED label for every
606 // parenthesized content model (even element-only ones) in the debug
607 // dump.
608 match elem.type_ {
609 t if t == crate::abi::types::xmlElementTypeVal::XML_ELEMENT_TYPE_EMPTY as c_int => {
610 libc::fprintf(ctx.output, c", EMPTY\n".as_ptr() as *const c_char);
611 }
612 t if t == crate::abi::types::xmlElementTypeVal::XML_ELEMENT_TYPE_ANY as c_int => {
613 libc::fprintf(ctx.output, c", ANY\n".as_ptr() as *const c_char);
614 }
615 _ => {
616 libc::fprintf(ctx.output, c", MIXED ".as_ptr() as *const c_char);
617 dump_debug_content_model(ctx.output, elem.content);
618 libc::fprintf(ctx.output, c"\n".as_ptr() as *const c_char);
619 }
620 }
621 }
622}
623
624/// Render a content model tree in the upstream debug format
625/// (`xmlDebugDumpContentModel`, flattened).
626///
627/// # Safety
628///
629/// - `output` must be a valid writable `FILE*`.
630/// - `content` must be NULL or a valid pointer to a live `_xmlElementContent`
631/// tree; element-type nodes require `name` to be a valid NUL-terminated
632/// string and `prefix` to be NULL or a valid NUL-terminated string (both
633/// are passed to `fprintf`).
634/// - The tree must not be mutated or freed concurrently during the call.
635unsafe fn dump_debug_content_model(
636 output: *mut _IO_FILE,
637 content: *mut crate::abi::structs::_xmlElementContent,
638) {
639 use crate::abi::types::xmlElementContentType::*;
640 if content.is_null() {
641 return;
642 }
643 unsafe {
644 let c = &*content;
645 match c.type_ {
646 t if t == XML_ELEMENT_CONTENT_PCDATA as c_int => {
647 libc::fprintf(output, c"(#PCDATA)".as_ptr() as *const c_char);
648 }
649 t if t == XML_ELEMENT_CONTENT_ELEMENT as c_int => {
650 if !c.prefix.is_null() {
651 libc::fprintf(output, c"%s:".as_ptr() as *const c_char, c.prefix);
652 }
653 libc::fprintf(output, c"%s".as_ptr() as *const c_char, c.name);
654 }
655 _ => {
656 let sep = if c.type_ == XML_ELEMENT_CONTENT_SEQ as c_int {
657 c" , ".as_ptr() as *const c_char
658 } else {
659 c" | ".as_ptr() as *const c_char
660 };
661 libc::fprintf(output, c"(".as_ptr() as *const c_char);
662 let mut parts: Vec<*mut crate::abi::structs::_xmlElementContent> = Vec::new();
663 flatten_chain(c as *const _ as *mut _, c.type_, &mut parts);
664 for (i, &p) in parts.iter().enumerate() {
665 if i > 0 {
666 libc::fprintf(output, c"%s".as_ptr() as *const c_char, sep);
667 }
668 let pc = &*p;
669 if pc.type_ == XML_ELEMENT_CONTENT_PCDATA as c_int {
670 libc::fprintf(output, c"#PCDATA".as_ptr() as *const c_char);
671 } else if pc.type_ == XML_ELEMENT_CONTENT_ELEMENT as c_int {
672 if !pc.prefix.is_null() {
673 libc::fprintf(output, c"%s:".as_ptr() as *const c_char, pc.prefix);
674 }
675 libc::fprintf(output, c"%s".as_ptr() as *const c_char, pc.name);
676 } else {
677 dump_debug_content_model(output, p);
678 }
679 dump_debug_occurrence(output, pc.ocur);
680 }
681 libc::fprintf(output, c")".as_ptr() as *const c_char);
682 }
683 }
684 dump_debug_occurrence(output, c.ocur);
685 }
686}
687
688/// Collect the leaves of a same-type chain (left-leaning trees flatten).
689///
690/// # Safety
691///
692/// - `node` must be a valid non-NULL pointer to a live `_xmlElementContent`.
693/// - The subtree reachable through `c1`/`c2` must remain alive and unmutated
694/// for the duration of the call; the traversal reads raw pointers
695/// recursively and stores borrowed copies in `parts`.
696/// - `parts` must be a valid mutable `Vec` reference.
697fn flatten_chain(
698 node: *mut crate::abi::structs::_xmlElementContent,
699 chain_type: c_int,
700 parts: &mut Vec<*mut crate::abi::structs::_xmlElementContent>,
701) {
702 unsafe {
703 let c = &*node;
704 if c.type_ == chain_type && !c.c1.is_null() {
705 flatten_chain(c.c1, chain_type, parts);
706 if !c.c2.is_null() {
707 flatten_chain(c.c2, chain_type, parts);
708 }
709 } else {
710 parts.push(node);
711 }
712 }
713}
714
715/// Print the occurrence suffix.
716///
717/// # Safety
718///
719/// - `output` must be a valid writable `FILE*`; the matched occurrence
720/// suffixes are static strings passed to `fprintf`.
721/// - `ocur` is an arbitrary `c_int`; unmatched values return without writing.
722unsafe fn dump_debug_occurrence(output: *mut _IO_FILE, ocur: c_int) {
723 use crate::abi::types::xmlElementContentOccur::*;
724 let s = match ocur {
725 t if t == XML_ELEMENT_CONTENT_OPT as c_int => c"?".as_ptr() as *const c_char,
726 t if t == XML_ELEMENT_CONTENT_MULT as c_int => c"*".as_ptr() as *const c_char,
727 t if t == XML_ELEMENT_CONTENT_PLUS as c_int => c"+".as_ptr() as *const c_char,
728 _ => return,
729 };
730 unsafe {
731 libc::fprintf(output, c"%s".as_ptr() as *const c_char, s);
732 }
733}
734
735/// True when a null-terminated string contains markup-significant bytes
736/// (`<` or `&`), i.e. it cannot be a single plain text node.
737const unsafe fn contains_markup(s: *const crate::abi::types::xmlChar) -> bool {
738 if s.is_null() {
739 return false;
740 }
741 unsafe {
742 let mut i = 0usize;
743 while *s.add(i) != 0 {
744 let c = *s.add(i);
745 if c == b'<' || c == b'&' {
746 return true;
747 }
748 i += 1;
749 }
750 }
751 false
752}
753
754/// Dump an entity declaration (`ENTITYDECL(name), internal`).
755///
756/// # Safety
757///
758/// - `payload` must be NULL or a valid pointer to a live `_xmlEntity` whose
759/// `name` is NULL or a NUL-terminated string.
760/// - `data` must be NULL or a valid pointer to a `DtdDumpCtx` whose `output`
761/// is a valid writable `FILE*` and whose `depth` is non-negative.
762/// - The entity and context must not be mutated concurrently during the call.
763unsafe extern "C" fn dump_entityscan_cb(payload: *mut c_void, data: *mut c_void, _name: *const u8) {
764 if payload.is_null() || data.is_null() {
765 return;
766 }
767 let ctx = unsafe { &*(data as *const DtdDumpCtx) };
768 let ent = unsafe { &*(payload as *mut crate::abi::structs::_xmlEntity) };
769 unsafe {
770 for _ in 0..ctx.depth {
771 libc::fprintf(ctx.output, c" ".as_ptr() as *const c_char);
772 }
773 libc::fprintf(ctx.output, c"ENTITYDECL(".as_ptr() as *const c_char);
774 if !ent.name.is_null() {
775 libc::fprintf(ctx.output, c"%s".as_ptr() as *const c_char, ent.name);
776 }
777 if ent.etype == crate::abi::types::xmlEntityType::XML_INTERNAL_GENERAL_ENTITY as c_int {
778 libc::fprintf(ctx.output, c"), internal\n".as_ptr() as *const c_char);
779 // UPSTREAM-PARITY: the content line is the ENTITYDECL indent plus
780 // one extra leading space.
781 for _ in 0..ctx.depth {
782 libc::fprintf(ctx.output, c" ".as_ptr() as *const c_char);
783 }
784 libc::fprintf(ctx.output, c" content=".as_ptr() as *const c_char);
785 if !ent.content.is_null() {
786 xmlDebugDumpString(ctx.output, ent.content as *const u8);
787 }
788 libc::fprintf(ctx.output, c"\n".as_ptr() as *const c_char);
789 // The entity's parsed content tree: upstream (debugXML.c
790 // xmlCtxtDumpNode) recurses into ent->children, which the parser
791 // populates on first reference (xmlCtxtParseEntity). For entity
792 // declarations that were never referenced (no children) the raw
793 // content is synthesized as a compact text node for plain text.
794 if !ent.children.is_null() {
795 let mut c = ent.children;
796 while !c.is_null() {
797 xmlDebugDumpNode(ctx.output, c, (ctx.depth + 1) as c_int);
798 c = (*c).next;
799 }
800 } else if !ent.content.is_null() && !contains_markup(ent.content) {
801 for _ in 0..(ctx.depth + 1) {
802 libc::fprintf(ctx.output, c" ".as_ptr() as *const c_char);
803 }
804 libc::fprintf(ctx.output, c"TEXT compact\n".as_ptr() as *const c_char);
805 for _ in 0..(ctx.depth + 2) {
806 libc::fprintf(ctx.output, c" ".as_ptr() as *const c_char);
807 }
808 libc::fprintf(ctx.output, c"content=".as_ptr() as *const c_char);
809 xmlDebugDumpString(ctx.output, ent.content as *const u8);
810 libc::fprintf(ctx.output, c"\n".as_ptr() as *const c_char);
811 }
812 } else {
813 libc::fprintf(ctx.output, c")\n".as_ptr() as *const c_char);
814 }
815 }
816}
817
818/// Dump a node and its subtree.
819///
820/// UPSTREAM-PARITY: `xmlDebugDumpNode()`
821///
822/// # SAFETY
823///
824/// - `output`, `node` must be valid pointers (or NULL
825/// where the upstream C contract allows), obtained from the
826/// matching constructor/owner and not yet freed; the callee may
827/// take or keep ownership exactly as the C API specifies.
828///
829/// The caller must not race this call with concurrent mutation of the
830/// same objects from other threads (per-object state is not internally
831/// synchronized). Violating any of the above is undefined behavior.
832///
833/// Exercised by the C-API differential courts
834/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
835/// courts; those pass byte-for-byte against the upstream oracle.
836#[no_mangle]
837pub unsafe extern "C" fn xmlDebugDumpNode(
838 output: *mut _IO_FILE,
839 node: *mut _xmlNode,
840 depth: c_int,
841) {
842 if output.is_null() || node.is_null() || depth > MAX_DEPTH {
843 return;
844 }
845 unsafe {
846 xmlDebugDumpOneNode(output, node, depth);
847
848 // UPSTREAM-PARITY: only element-like nodes recurse into children;
849 // text nodes (including the non-compact merged representation) do not.
850 // The DTD node's declaration children are dumped from the hash tables
851 // inside xmlDebugDumpOneNode, so the children chain must not be
852 // walked again (upstream debugXML.c xmlCtxtDumpNode reaches the decl
853 // nodes through the chain, but the candidate keeps them in the DTD
854 // tables as well — walking both would duplicate them).
855 let t = (*node).type_;
856 let recurse = t == 1 // XML_ELEMENT_NODE
857 || t == 9 // XML_DOCUMENT_NODE
858 || t == 13 // XML_HTML_DOCUMENT_NODE
859 || t == 11; // XML_DOCUMENT_FRAG_NODE
860 if recurse && !(*node).children.is_null() {
861 let mut child = (*node).children;
862 while !child.is_null() {
863 xmlDebugDumpNode(output, child, depth + 1);
864 child = (*child).next;
865 }
866 }
867 }
868}
869
870/// Dump a node list.
871///
872/// UPSTREAM-PARITY: `xmlDebugDumpNodeList()`
873///
874/// # SAFETY
875///
876/// - `output`, `node` must be valid pointers (or NULL
877/// where the upstream C contract allows), obtained from the
878/// matching constructor/owner and not yet freed; the callee may
879/// take or keep ownership exactly as the C API specifies.
880///
881/// The caller must not race this call with concurrent mutation of the
882/// same objects from other threads (per-object state is not internally
883/// synchronized). Violating any of the above is undefined behavior.
884///
885/// Exercised by the C-API differential courts
886/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
887/// courts; those pass byte-for-byte against the upstream oracle.
888#[no_mangle]
889pub unsafe extern "C" fn xmlDebugDumpNodeList(
890 output: *mut _IO_FILE,
891 node: *mut _xmlNode,
892 depth: c_int,
893) {
894 if output.is_null() {
895 return;
896 }
897 let mut cur = node;
898 while !cur.is_null() {
899 unsafe {
900 xmlDebugDumpNode(output, cur, depth);
901 cur = (*cur).next;
902 }
903 }
904}
905
906/// Dump an entire document.
907///
908/// UPSTREAM-PARITY: `xmlDebugDumpDocument()`
909///
910/// # SAFETY
911///
912/// - `output`, `doc` must be valid pointers (or NULL
913/// where the upstream C contract allows), obtained from the
914/// matching constructor/owner and not yet freed; the callee may
915/// take or keep ownership exactly as the C API specifies.
916///
917/// The caller must not race this call with concurrent mutation of the
918/// same objects from other threads (per-object state is not internally
919/// synchronized). Violating any of the above is undefined behavior.
920///
921/// Exercised by the C-API differential courts
922/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
923/// courts; those pass byte-for-byte against the upstream oracle.
924#[no_mangle]
925pub unsafe extern "C" fn xmlDebugDumpDocument(output: *mut _IO_FILE, doc: *mut _xmlDoc) {
926 if output.is_null() || doc.is_null() {
927 return;
928 }
929 unsafe {
930 // UPSTREAM-PARITY: xmlCtxtDumpDocHead prints "HTML DOCUMENT" for
931 // HTML documents and "DOCUMENT" otherwise (debugXML.c).
932 if (*doc).type_ == crate::abi::types::xmlElementType::XML_HTML_DOCUMENT_NODE as c_int {
933 libc::fprintf(output, c"HTML DOCUMENT\n".as_ptr() as *const c_char);
934 } else {
935 libc::fprintf(output, c"DOCUMENT\n".as_ptr() as *const c_char);
936 }
937 if !(*doc).version.is_null() {
938 libc::fprintf(output, c"version=".as_ptr() as *const c_char);
939 xmlDebugDumpString(output, (*doc).version as *const u8);
940 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
941 }
942 if !(*doc).URL.is_null() {
943 libc::fprintf(output, c"URL=".as_ptr() as *const c_char);
944 // UPSTREAM-PARITY: xmlCtxtDumpDocHead prints the URL through
945 // xmlCtxtDumpString, so it is truncated at 40 characters.
946 xmlDebugDumpString(output, (*doc).URL as *const u8);
947 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
948 }
949 // UPSTREAM-PARITY: the standalone flag is tri-state; the debug dump
950 // prints "standalone=true" whenever it is not 0 (unset defaults to
951 // true in the parser).
952 if (*doc).standalone != 0 {
953 libc::fprintf(output, c"standalone=true\n".as_ptr() as *const c_char);
954 }
955
956 // Document-level namespace declarations (upstream keeps the xml
957 // namespace here; other prefixes live on the root element's nsDef).
958 if !(*doc).oldNs.is_null() {
959 let mut ns = (*doc).oldNs;
960 while !ns.is_null() {
961 libc::fprintf(output, c"namespace ".as_ptr() as *const c_char);
962 if (*ns).prefix.is_null() {
963 libc::fprintf(output, c" ".as_ptr() as *const c_char);
964 } else {
965 libc::fprintf(output, c"%s".as_ptr() as *const c_char, (*ns).prefix);
966 }
967 libc::fprintf(output, c" href=%s\n".as_ptr() as *const c_char, (*ns).href);
968 ns = (*ns).next;
969 }
970 }
971
972 // Dump the internal subset. UPSTREAM-PARITY: xmlCreateIntSubset keeps
973 // the DTD as a member of the document's children chain, so the
974 // children loop below dumps it; only dump it here when the
975 // construction path kept it solely on doc->intSubset (xmlCopyDoc,
976 // lazily-created subsets). Never dump both.
977 if !(*doc).intSubset.is_null() {
978 let mut in_chain = false;
979 let mut c = (*doc).children;
980 while !c.is_null() {
981 if c as *mut c_void == (*doc).intSubset as *mut c_void {
982 in_chain = true;
983 break;
984 }
985 c = (*c).next;
986 }
987 if !in_chain {
988 xmlDebugDumpNode(output, (*doc).intSubset as *mut _xmlNode, 1);
989 }
990 }
991
992 // Dump children of doc
993 if !(*doc).children.is_null() {
994 let mut child = (*doc).children;
995 while !child.is_null() {
996 xmlDebugDumpNode(output, child, 1);
997 child = (*child).next;
998 }
999 }
1000 }
1001}
1002
1003/// Dump the document head (first few nodes).
1004///
1005/// UPSTREAM-PARITY: `xmlDebugDumpDocumentHead()`
1006///
1007/// # SAFETY
1008///
1009/// - `output`, `doc` must be valid pointers (or NULL
1010/// where the upstream C contract allows), obtained from the
1011/// matching constructor/owner and not yet freed; the callee may
1012/// take or keep ownership exactly as the C API specifies.
1013///
1014/// The caller must not race this call with concurrent mutation of the
1015/// same objects from other threads (per-object state is not internally
1016/// synchronized). Violating any of the above is undefined behavior.
1017///
1018/// Exercised by the C-API differential courts
1019/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1020/// courts; those pass byte-for-byte against the upstream oracle.
1021#[no_mangle]
1022pub unsafe extern "C" fn xmlDebugDumpDocumentHead(output: *mut _IO_FILE, doc: *mut _xmlDoc) {
1023 if output.is_null() || doc.is_null() {
1024 return;
1025 }
1026 unsafe {
1027 xmlDebugDumpDocument(output, doc);
1028 }
1029}
1030
1031/// Count the number of nodes in a list reachable via next pointers.
1032///
1033/// UPSTREAM-PARITY: `xmlLsCountNode()`
1034///
1035/// # SAFETY
1036///
1037/// - `node` must be valid pointers (or NULL
1038/// where the upstream C contract allows), obtained from the
1039/// matching constructor/owner and not yet freed; the callee may
1040/// take or keep ownership exactly as the C API specifies.
1041///
1042/// The caller must not race this call with concurrent mutation of the
1043/// same objects from other threads (per-object state is not internally
1044/// synchronized). Violating any of the above is undefined behavior.
1045///
1046/// Exercised by the C-API differential courts
1047/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1048/// courts; those pass byte-for-byte against the upstream oracle.
1049#[no_mangle]
1050pub unsafe extern "C" fn xmlLsCountNode(node: *mut _xmlNode) -> c_int {
1051 if node.is_null() {
1052 return 0;
1053 }
1054 let mut count: c_int = 0;
1055 let mut cur = node;
1056 while !cur.is_null() {
1057 count += 1;
1058 unsafe {
1059 cur = (*cur).next;
1060 }
1061 }
1062 count
1063}
1064
1065/// Dump a single node summary (like `ls -l` for nodes).
1066///
1067/// UPSTREAM-PARITY: `xmlLsOneNode()`
1068///
1069/// # SAFETY
1070///
1071/// - `output`, `node` must be valid pointers (or NULL
1072/// where the upstream C contract allows), obtained from the
1073/// matching constructor/owner and not yet freed; the callee may
1074/// take or keep ownership exactly as the C API specifies.
1075///
1076/// The caller must not race this call with concurrent mutation of the
1077/// same objects from other threads (per-object state is not internally
1078/// synchronized). Violating any of the above is undefined behavior.
1079///
1080/// Exercised by the C-API differential courts
1081/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1082/// courts; those pass byte-for-byte against the upstream oracle.
1083#[no_mangle]
1084pub unsafe extern "C" fn xmlLsOneNode(output: *mut _IO_FILE, node: *mut _xmlNode) {
1085 if output.is_null() || node.is_null() {
1086 return;
1087 }
1088 unsafe {
1089 match (*node).type_ {
1090 1 => {
1091 // XML_ELEMENT_NODE
1092 libc::fprintf(output, c"E ".as_ptr() as *const c_char);
1093 if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
1094 libc::fprintf(
1095 output,
1096 c"%s:".as_ptr() as *const c_char,
1097 (*(*node).ns).prefix,
1098 );
1099 }
1100 xmlDebugDumpString(output, (*node).name);
1101 }
1102 2 => {
1103 libc::fprintf(output, c"A ".as_ptr() as *const c_char);
1104 xmlDebugDumpString(output, (*node).name);
1105 }
1106 3 => {
1107 libc::fprintf(output, c"T ".as_ptr() as *const c_char);
1108 if !(*node).content.is_null() {
1109 xmlDebugDumpString(output, (*node).content as *const u8);
1110 }
1111 }
1112 4 => {
1113 libc::fprintf(output, c"C ".as_ptr() as *const c_char);
1114 }
1115 5 => {
1116 libc::fprintf(output, c"E ".as_ptr() as *const c_char);
1117 xmlDebugDumpString(output, (*node).name);
1118 }
1119 6 => {
1120 libc::fprintf(output, c"E ".as_ptr() as *const c_char);
1121 xmlDebugDumpString(output, (*node).name);
1122 }
1123 7 => {
1124 libc::fprintf(output, c"PI ".as_ptr() as *const c_char);
1125 xmlDebugDumpString(output, (*node).name);
1126 }
1127 8 => {
1128 libc::fprintf(output, c"C ".as_ptr() as *const c_char);
1129 }
1130 9 => {
1131 libc::fprintf(output, c"D ".as_ptr() as *const c_char);
1132 }
1133 10 => {
1134 libc::fprintf(output, c"DTD ".as_ptr() as *const c_char);
1135 }
1136 14 => {
1137 libc::fprintf(output, c"X ".as_ptr() as *const c_char);
1138 }
1139 _ => {
1140 libc::fprintf(
1141 output,
1142 c"? (%d)".as_ptr() as *const c_char,
1143 (*node).type_ as c_int,
1144 );
1145 }
1146 }
1147 libc::fprintf(output, c"\n".as_ptr() as *const c_char);
1148 }
1149}
1150
1151/// Re-export _IO_FILE type for C ABI compatibility.
1152///
1153/// This is typically `FILE` in C. On Linux with libc, `_IO_FILE` is the struct
1154/// behind `FILE *`. We use `*mut _IO_FILE` to match the upstream signature.
1155pub type _IO_FILE = libc::FILE;
1156
1157// ═══════════════════════════════════════════════════════════════════════════════
1158// Tests
1159// ═══════════════════════════════════════════════════════════════════════════════
1160
1161#[cfg(test)]
1162mod tests {
1163 use super::*;
1164
1165 use crate::abi::types::xmlChar;
1166 use crate::xml::tree::*;
1167
1168 /// Helper: create a simple document for testing.
1169 #[allow(dead_code)]
1170 unsafe fn create_test_doc() -> *mut _xmlDoc {
1171 let doc = new_doc(c"1.0".as_ptr() as *const xmlChar);
1172 let root = new_node(ptr::null_mut(), c"root".as_ptr() as *const xmlChar);
1173 doc_set_root_element(doc, root);
1174 let child = new_child(root, ptr::null_mut(), c"child".as_ptr() as *const xmlChar);
1175 // Set a property using set_prop
1176 set_prop(
1177 child,
1178 c"attr1".as_ptr() as *const xmlChar,
1179 c"value1".as_ptr() as *const xmlChar,
1180 );
1181 doc
1182 }
1183
1184 /// Test that `xmlBoolToText` returns the static strings `true`/`false`.
1185 ///
1186 /// # Safety
1187 ///
1188 /// - The returned pointers are non-NULL (asserted) and point to static
1189 /// NUL-terminated strings; reading their first byte requires each
1190 /// pointer to be valid, which the assertions guarantee.
1191 #[test]
1192 fn test_xml_bool_to_text() {
1193 unsafe {
1194 let t = xmlBoolToText(1);
1195 assert!(!t.is_null());
1196 let f = xmlBoolToText(0);
1197 assert!(!f.is_null());
1198 // Check that the strings are correct by comparing first byte
1199 assert_eq!(*t as u8, b't');
1200 assert_eq!(*f as u8, b'f');
1201 }
1202 }
1203
1204 /// Test that `xmlDebugDumpString` tolerates NULL output and NULL string.
1205 ///
1206 /// # Safety
1207 ///
1208 /// - `xmlDebugDumpString` accepts NULL for both arguments per the C
1209 /// contract; `fmemopen` returns a valid writable `FILE*` when non-NULL,
1210 /// and `fclose` takes ownership of it exactly once.
1211 #[test]
1212 fn test_debug_dump_string_null() {
1213 unsafe {
1214 // Should not crash
1215 xmlDebugDumpString(ptr::null_mut(), ptr::null());
1216 // Should print "(null)"
1217 let f = libc::fmemopen(ptr::null_mut(), 0, c"w".as_ptr() as *const c_char);
1218 if !f.is_null() {
1219 xmlDebugDumpString(f, ptr::null());
1220 libc::fclose(f);
1221 }
1222 }
1223 }
1224
1225 /// Test that `xmlDebugDumpDocument` tolerates NULL output and doc.
1226 ///
1227 /// # Safety
1228 ///
1229 /// - `xmlDebugDumpDocument` accepts NULL arguments (early return); a
1230 /// non-NULL `fmemopen` result is a valid `FILE*` owned by the caller
1231 /// and closed exactly once with `fclose`.
1232 #[test]
1233 fn test_debug_dump_document_null() {
1234 unsafe {
1235 xmlDebugDumpDocument(ptr::null_mut(), ptr::null_mut());
1236 let f = libc::fmemopen(ptr::null_mut(), 0, c"w".as_ptr() as *const c_char);
1237 if !f.is_null() {
1238 xmlDebugDumpDocument(f, ptr::null_mut());
1239 libc::fclose(f);
1240 }
1241 }
1242 }
1243
1244 /// Test that `xmlDebugDumpNode` tolerates NULL output and node.
1245 ///
1246 /// # Safety
1247 ///
1248 /// - `xmlDebugDumpNode` accepts NULL arguments (early return); a non-NULL
1249 /// `fmemopen` result is a valid `FILE*` closed exactly once with
1250 /// `fclose`.
1251 #[test]
1252 fn test_debug_dump_node_null() {
1253 unsafe {
1254 xmlDebugDumpNode(ptr::null_mut(), ptr::null_mut(), 0);
1255 let f = libc::fmemopen(ptr::null_mut(), 0, c"w".as_ptr() as *const c_char);
1256 if !f.is_null() {
1257 xmlDebugDumpNode(f, ptr::null_mut(), 0);
1258 libc::fclose(f);
1259 }
1260 }
1261 }
1262
1263 /// Test counting a node and its sibling chain.
1264 ///
1265 /// # Safety
1266 ///
1267 /// - `node` and `sibling` are non-NULL pointers returned by `new_node`
1268 /// and linked with `add_sibling`; they remain valid until `free_node`
1269 /// transfers ownership exactly once at the end.
1270 /// - `xmlLsCountNode` borrows the chain and must not race concurrent
1271 /// mutation.
1272 #[test]
1273 fn test_ls_count_node() {
1274 unsafe {
1275 let node = new_node(ptr::null_mut(), c"test".as_ptr() as *const xmlChar);
1276 assert!(!node.is_null());
1277 let count = xmlLsCountNode(node);
1278 assert_eq!(count, 1);
1279
1280 // Add a sibling
1281 let sibling = new_node(ptr::null_mut(), c"sibling".as_ptr() as *const xmlChar);
1282 add_sibling(node, sibling);
1283 let count = xmlLsCountNode(node);
1284 assert_eq!(count, 2);
1285
1286 free_node(node);
1287 }
1288 }
1289
1290 /// Test that `xmlDebugDumpAttr` tolerates NULL output and attr.
1291 ///
1292 /// # Safety
1293 ///
1294 /// - `xmlDebugDumpAttr` accepts NULL arguments; a non-NULL `fmemopen`
1295 /// result is a valid `FILE*` closed exactly once with `fclose`.
1296 #[test]
1297 fn test_debug_dump_attr_null() {
1298 unsafe {
1299 xmlDebugDumpAttr(ptr::null_mut(), ptr::null_mut(), 0);
1300 let f = libc::fmemopen(ptr::null_mut(), 0, c"w".as_ptr() as *const c_char);
1301 if !f.is_null() {
1302 xmlDebugDumpAttr(f, ptr::null_mut(), 0);
1303 libc::fclose(f);
1304 }
1305 }
1306 }
1307
1308 /// Test that `xmlDebugDumpAttrList` tolerates NULL output and attr list.
1309 ///
1310 /// # Safety
1311 ///
1312 /// - `xmlDebugDumpAttrList` accepts NULL arguments and returns without
1313 /// dereferencing them; no pointers are owned or freed by this test.
1314 #[test]
1315 fn test_debug_dump_attr_list_null() {
1316 unsafe {
1317 xmlDebugDumpAttrList(ptr::null_mut(), ptr::null_mut(), 0);
1318 }
1319 }
1320
1321 /// Test that `xmlDebugDumpNodeList` tolerates NULL output and node list.
1322 ///
1323 /// # Safety
1324 ///
1325 /// - `xmlDebugDumpNodeList` accepts NULL arguments and returns without
1326 /// dereferencing them; no pointers are owned or freed by this test.
1327 #[test]
1328 fn test_debug_dump_node_list_null() {
1329 unsafe {
1330 xmlDebugDumpNodeList(ptr::null_mut(), ptr::null_mut(), 0);
1331 }
1332 }
1333
1334 /// Test that `xmlLsOneNode` tolerates NULL output and node.
1335 ///
1336 /// # Safety
1337 ///
1338 /// - `xmlLsOneNode` accepts NULL arguments; a non-NULL `fmemopen` result
1339 /// is a valid `FILE*` closed exactly once with `fclose`.
1340 #[test]
1341 fn test_ls_one_node_null() {
1342 unsafe {
1343 xmlLsOneNode(ptr::null_mut(), ptr::null_mut());
1344 let f = libc::fmemopen(ptr::null_mut(), 0, c"w".as_ptr() as *const c_char);
1345 if !f.is_null() {
1346 xmlLsOneNode(f, ptr::null_mut());
1347 libc::fclose(f);
1348 }
1349 }
1350 }
1351
1352 /// Test that `xmlDebugDumpDocumentHead` tolerates NULL output and doc.
1353 ///
1354 /// # Safety
1355 ///
1356 /// - `xmlDebugDumpDocumentHead` accepts NULL arguments and returns without
1357 /// dereferencing them; no pointers are owned or freed by this test.
1358 #[test]
1359 fn test_dump_document_head_null() {
1360 unsafe {
1361 xmlDebugDumpDocumentHead(ptr::null_mut(), ptr::null_mut());
1362 }
1363 }
1364
1365 /// Test that `xmlLsCountNode` returns 0 for a NULL node.
1366 ///
1367 /// # Safety
1368 ///
1369 /// - Passing NULL to `xmlLsCountNode` is accepted and yields 0 without
1370 /// dereferencing the pointer.
1371 #[test]
1372 fn test_ls_count_node_null() {
1373 assert_eq!(unsafe { xmlLsCountNode(ptr::null_mut()) }, 0);
1374 }
1375}