Skip to main content

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}