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::xmlFree;
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::xmlFree(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 builds children on
609            // first reference and dumps them; ours stores the raw content,
610            // so a compact text node is synthesized for plain text content
611            // only. KNOWN RESIDUAL: entity content containing markup (`<`,
612            // `&`) is not parsed into a child tree, so the oracle's
613            // element/entity-ref children are not reproduced here.
614            if !ent.content.is_null() && !contains_markup(ent.content) && ent.children.is_null() {
615                for _ in 0..(ctx.depth + 1) {
616                    libc::fprintf(ctx.output, b"  \0".as_ptr() as *const c_char);
617                }
618                libc::fprintf(ctx.output, b"TEXT compact\n\0".as_ptr() as *const c_char);
619                for _ in 0..(ctx.depth + 2) {
620                    libc::fprintf(ctx.output, b"  \0".as_ptr() as *const c_char);
621                }
622                libc::fprintf(ctx.output, b"content=\0".as_ptr() as *const c_char);
623                xmlDebugDumpString(ctx.output, ent.content as *const u8);
624                libc::fprintf(ctx.output, b"\n\0".as_ptr() as *const c_char);
625            }
626        } else {
627            libc::fprintf(ctx.output, b")\n\0".as_ptr() as *const c_char);
628        }
629    }
630}
631
632/// Dump a node and its subtree.
633///
634/// UPSTREAM-PARITY: `xmlDebugDumpNode()`
635#[no_mangle]
636pub unsafe extern "C" fn xmlDebugDumpNode(
637    output: *mut _IO_FILE,
638    node: *mut _xmlNode,
639    depth: c_int,
640) {
641    if output.is_null() || node.is_null() || depth > MAX_DEPTH {
642        return;
643    }
644    unsafe {
645        xmlDebugDumpOneNode(output, node, depth);
646
647        // UPSTREAM-PARITY: only element-like nodes recurse into children;
648        // text nodes (including the non-compact merged representation) do not.
649        let t = (*node).type_;
650        let recurse = t == 1 // XML_ELEMENT_NODE
651            || t == 9  // XML_DOCUMENT_NODE
652            || t == 13 // XML_HTML_DOCUMENT_NODE
653            || t == 11 // XML_DOCUMENT_FRAG_NODE
654            || t == 14; // XML_DTD_NODE
655        if recurse && !(*node).children.is_null() {
656            let mut child = (*node).children;
657            while !child.is_null() {
658                xmlDebugDumpNode(output, child, depth + 1);
659                child = (*child).next;
660            }
661        }
662    }
663}
664
665/// Dump a node list.
666///
667/// UPSTREAM-PARITY: `xmlDebugDumpNodeList()`
668#[no_mangle]
669pub unsafe extern "C" fn xmlDebugDumpNodeList(
670    output: *mut _IO_FILE,
671    node: *mut _xmlNode,
672    depth: c_int,
673) {
674    if output.is_null() {
675        return;
676    }
677    let mut cur = node;
678    while !cur.is_null() {
679        unsafe {
680            xmlDebugDumpNode(output, cur, depth);
681            cur = (*cur).next;
682        }
683    }
684}
685
686/// Dump an entire document.
687///
688/// UPSTREAM-PARITY: `xmlDebugDumpDocument()`
689#[no_mangle]
690pub unsafe extern "C" fn xmlDebugDumpDocument(output: *mut _IO_FILE, doc: *mut _xmlDoc) {
691    if output.is_null() || doc.is_null() {
692        return;
693    }
694    unsafe {
695        // UPSTREAM-PARITY: xmlCtxtDumpDocHead prints "HTML DOCUMENT" for
696        // HTML documents and "DOCUMENT" otherwise (debugXML.c).
697        if (*doc).type_ == crate::abi::types::xmlElementType::XML_HTML_DOCUMENT_NODE as c_int {
698            libc::fprintf(output, b"HTML DOCUMENT\n\0".as_ptr() as *const c_char);
699        } else {
700            libc::fprintf(output, b"DOCUMENT\n\0".as_ptr() as *const c_char);
701        }
702        if !(*doc).version.is_null() {
703            libc::fprintf(output, b"version=\0".as_ptr() as *const c_char);
704            xmlDebugDumpString(output, (*doc).version as *const u8);
705            libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
706        }
707        if !(*doc).URL.is_null() {
708            libc::fprintf(output, b"URL=\0".as_ptr() as *const c_char);
709            // UPSTREAM-PARITY: xmlCtxtDumpDocHead prints the URL through
710            // xmlCtxtDumpString, so it is truncated at 40 characters.
711            xmlDebugDumpString(output, (*doc).URL as *const u8);
712            libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
713        }
714        // UPSTREAM-PARITY: the standalone flag is tri-state; the debug dump
715        // prints "standalone=true" whenever it is not 0 (unset defaults to
716        // true in the parser).
717        if (*doc).standalone != 0 {
718            libc::fprintf(output, b"standalone=true\n\0".as_ptr() as *const c_char);
719        }
720
721        // Document-level namespace declarations (upstream keeps the xml
722        // namespace here; other prefixes live on the root element's nsDef).
723        if !(*doc).oldNs.is_null() {
724            let mut ns = (*doc).oldNs;
725            while !ns.is_null() {
726                libc::fprintf(output, b"namespace \0".as_ptr() as *const c_char);
727                if (*ns).prefix.is_null() {
728                    libc::fprintf(output, b" \0".as_ptr() as *const c_char);
729                } else {
730                    libc::fprintf(output, b"%s\0".as_ptr() as *const c_char, (*ns).prefix);
731                }
732                libc::fprintf(
733                    output,
734                    b" href=%s\n\0".as_ptr() as *const c_char,
735                    (*ns).href,
736                );
737                ns = (*ns).next;
738            }
739        }
740
741        // Dump the internal subset (upstream keeps the DTD as the document's
742        // first child; ours is stored on doc->intSubset).
743        if !(*doc).intSubset.is_null() {
744            xmlDebugDumpNode(output, (*doc).intSubset as *mut _xmlNode, 1);
745        }
746
747        // Dump children of doc
748        if !(*doc).children.is_null() {
749            let mut child = (*doc).children;
750            while !child.is_null() {
751                xmlDebugDumpNode(output, child, 1);
752                child = (*child).next;
753            }
754        }
755    }
756}
757
758/// Dump the document head (first few nodes).
759///
760/// UPSTREAM-PARITY: `xmlDebugDumpDocumentHead()`
761#[no_mangle]
762pub unsafe extern "C" fn xmlDebugDumpDocumentHead(output: *mut _IO_FILE, doc: *mut _xmlDoc) {
763    if output.is_null() || doc.is_null() {
764        return;
765    }
766    unsafe {
767        xmlDebugDumpDocument(output, doc);
768    }
769}
770
771/// Count the number of nodes in a list reachable via next pointers.
772///
773/// UPSTREAM-PARITY: `xmlLsCountNode()`
774#[no_mangle]
775pub unsafe extern "C" fn xmlLsCountNode(node: *mut _xmlNode) -> c_int {
776    if node.is_null() {
777        return 0;
778    }
779    let mut count: c_int = 0;
780    let mut cur = node;
781    while !cur.is_null() {
782        count += 1;
783        unsafe {
784            cur = (*cur).next;
785        }
786    }
787    count
788}
789
790/// Dump a single node summary (like `ls -l` for nodes).
791///
792/// UPSTREAM-PARITY: `xmlLsOneNode()`
793#[no_mangle]
794pub unsafe extern "C" fn xmlLsOneNode(output: *mut _IO_FILE, node: *mut _xmlNode) {
795    if output.is_null() || node.is_null() {
796        return;
797    }
798    unsafe {
799        match (*node).type_ {
800            1 => {
801                // XML_ELEMENT_NODE
802                libc::fprintf(output, b"E \0".as_ptr() as *const c_char);
803                if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
804                    libc::fprintf(
805                        output,
806                        b"%s:\0".as_ptr() as *const c_char,
807                        (*(*node).ns).prefix,
808                    );
809                }
810                xmlDebugDumpString(output, (*node).name as *const u8);
811            }
812            2 => {
813                libc::fprintf(output, b"A \0".as_ptr() as *const c_char);
814                xmlDebugDumpString(output, (*node).name as *const u8);
815            }
816            3 => {
817                libc::fprintf(output, b"T \0".as_ptr() as *const c_char);
818                if !(*node).content.is_null() {
819                    xmlDebugDumpString(output, (*node).content as *const u8);
820                }
821            }
822            4 => {
823                libc::fprintf(output, b"C \0".as_ptr() as *const c_char);
824            }
825            5 => {
826                libc::fprintf(output, b"E \0".as_ptr() as *const c_char);
827                xmlDebugDumpString(output, (*node).name as *const u8);
828            }
829            6 => {
830                libc::fprintf(output, b"E \0".as_ptr() as *const c_char);
831                xmlDebugDumpString(output, (*node).name as *const u8);
832            }
833            7 => {
834                libc::fprintf(output, b"PI \0".as_ptr() as *const c_char);
835                xmlDebugDumpString(output, (*node).name as *const u8);
836            }
837            8 => {
838                libc::fprintf(output, b"C \0".as_ptr() as *const c_char);
839            }
840            9 => {
841                libc::fprintf(output, b"D \0".as_ptr() as *const c_char);
842            }
843            10 => {
844                libc::fprintf(output, b"DTD \0".as_ptr() as *const c_char);
845            }
846            14 => {
847                libc::fprintf(output, b"X \0".as_ptr() as *const c_char);
848            }
849            _ => {
850                libc::fprintf(
851                    output,
852                    b"? (%d)\0".as_ptr() as *const c_char,
853                    (*node).type_ as c_int,
854                );
855            }
856        }
857        libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
858    }
859}
860
861/// Re-export _IO_FILE type for C ABI compatibility.
862///
863/// This is typically `FILE` in C. On Linux with libc, `_IO_FILE` is the struct
864/// behind `FILE *`. We use `*mut _IO_FILE` to match the upstream signature.
865pub type _IO_FILE = libc::FILE;
866
867// ═══════════════════════════════════════════════════════════════════════════════
868// Tests
869// ═══════════════════════════════════════════════════════════════════════════════
870
871#[cfg(test)]
872mod tests {
873    use super::*;
874    use crate::abi::allocator::xmlMalloc;
875    use crate::abi::structs::*;
876    use crate::abi::types::xmlChar;
877    use crate::xml::tree::*;
878
879    /// Helper: create a simple document for testing.
880    unsafe fn create_test_doc() -> *mut _xmlDoc {
881        let doc = new_doc(b"1.0\0".as_ptr() as *const xmlChar);
882        let root = new_node(ptr::null_mut(), b"root\0".as_ptr() as *const xmlChar);
883        doc_set_root_element(doc, root);
884        let child = new_child(root, ptr::null_mut(), b"child\0".as_ptr() as *const xmlChar);
885        // Set a property using set_prop
886        set_prop(
887            child,
888            b"attr1\0".as_ptr() as *const xmlChar,
889            b"value1\0".as_ptr() as *const xmlChar,
890        );
891        doc
892    }
893
894    #[test]
895    fn test_xml_bool_to_text() {
896        unsafe {
897            let t = xmlBoolToText(1);
898            assert!(!t.is_null());
899            let f = xmlBoolToText(0);
900            assert!(!f.is_null());
901            // Check that the strings are correct by comparing first byte
902            assert_eq!(*t as u8, b't');
903            assert_eq!(*f as u8, b'f');
904        }
905    }
906
907    #[test]
908    fn test_debug_dump_string_null() {
909        unsafe {
910            // Should not crash
911            xmlDebugDumpString(ptr::null_mut(), ptr::null());
912            // Should print "(null)"
913            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
914            if !f.is_null() {
915                xmlDebugDumpString(f, ptr::null());
916                libc::fclose(f);
917            }
918        }
919    }
920
921    #[test]
922    fn test_debug_dump_document_null() {
923        unsafe {
924            xmlDebugDumpDocument(ptr::null_mut(), ptr::null_mut());
925            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
926            if !f.is_null() {
927                xmlDebugDumpDocument(f, ptr::null_mut());
928                libc::fclose(f);
929            }
930        }
931    }
932
933    #[test]
934    fn test_debug_dump_node_null() {
935        unsafe {
936            xmlDebugDumpNode(ptr::null_mut(), ptr::null_mut(), 0);
937            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
938            if !f.is_null() {
939                xmlDebugDumpNode(f, ptr::null_mut(), 0);
940                libc::fclose(f);
941            }
942        }
943    }
944
945    #[test]
946    fn test_ls_count_node() {
947        unsafe {
948            let node = new_node(ptr::null_mut(), b"test\0".as_ptr() as *const xmlChar);
949            assert!(!node.is_null());
950            let count = xmlLsCountNode(node);
951            assert_eq!(count, 1);
952
953            // Add a sibling
954            let sibling = new_node(ptr::null_mut(), b"sibling\0".as_ptr() as *const xmlChar);
955            add_sibling(node, sibling);
956            let count = xmlLsCountNode(node);
957            assert_eq!(count, 2);
958
959            free_node(node);
960        }
961    }
962
963    #[test]
964    fn test_debug_dump_attr_null() {
965        unsafe {
966            xmlDebugDumpAttr(ptr::null_mut(), ptr::null_mut(), 0);
967            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
968            if !f.is_null() {
969                xmlDebugDumpAttr(f, ptr::null_mut(), 0);
970                libc::fclose(f);
971            }
972        }
973    }
974
975    #[test]
976    fn test_debug_dump_attr_list_null() {
977        unsafe {
978            xmlDebugDumpAttrList(ptr::null_mut(), ptr::null_mut(), 0);
979        }
980    }
981
982    #[test]
983    fn test_debug_dump_node_list_null() {
984        unsafe {
985            xmlDebugDumpNodeList(ptr::null_mut(), ptr::null_mut(), 0);
986        }
987    }
988
989    #[test]
990    fn test_ls_one_node_null() {
991        unsafe {
992            xmlLsOneNode(ptr::null_mut(), ptr::null_mut());
993            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
994            if !f.is_null() {
995                xmlLsOneNode(f, ptr::null_mut());
996                libc::fclose(f);
997            }
998        }
999    }
1000
1001    #[test]
1002    fn test_dump_document_head_null() {
1003        unsafe {
1004            xmlDebugDumpDocumentHead(ptr::null_mut(), ptr::null_mut());
1005        }
1006    }
1007
1008    #[test]
1009    fn test_ls_count_node_null() {
1010        assert_eq!(unsafe { xmlLsCountNode(ptr::null_mut()) }, 0);
1011    }
1012}