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