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