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()`
71#[no_mangle]
72pub unsafe extern "C" fn xmlDebugDumpString(output: *mut _IO_FILE, str_val: *const u8) {
73    if output.is_null() {
74        return;
75    }
76    if str_val.is_null() {
77        unsafe {
78            libc::fprintf(output, b"(null)\0".as_ptr() as *const c_char);
79        }
80        return;
81    }
82    unsafe {
83        let mut i = 0;
84        loop {
85            let c = *str_val.add(i);
86            if c == 0 {
87                break;
88            }
89            if c == b'\n' {
90                libc::fprintf(output, b"\\n\0".as_ptr() as *const c_char);
91            } else if c == b'\r' {
92                libc::fprintf(output, b"\\r\0".as_ptr() as *const c_char);
93            } else if c == b'\t' {
94                libc::fprintf(output, b"\\t\0".as_ptr() as *const c_char);
95            } else if c < 0x20 || c >= 0x7f {
96                libc::fprintf(output, b"\\x%02x\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    }
103}
104
105/// Dump a debug representation of an attribute.
106///
107/// UPSTREAM-PARITY: `xmlDebugDumpAttr()`
108#[no_mangle]
109pub unsafe extern "C" fn xmlDebugDumpAttr(
110    output: *mut _IO_FILE,
111    attr: *mut _xmlAttr,
112    depth: c_int,
113) {
114    if output.is_null() || attr.is_null() {
115        return;
116    }
117    unsafe {
118        for _ in 0..depth {
119            libc::fprintf(output, b"  \0".as_ptr() as *const c_char);
120        }
121        libc::fprintf(output, b"ATTRIBUTE \0".as_ptr() as *const c_char);
122        xmlDebugDumpString(output, (*attr).name as *const u8);
123        if !(*attr).ns.is_null() && !(*(*attr).ns).prefix.is_null() {
124            libc::fprintf(
125                output,
126                b":%s\0".as_ptr() as *const c_char,
127                (*(*attr).ns).prefix,
128            );
129        }
130        libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
131        if !(*attr).children.is_null() {
132            for _ in 0..(depth + 1) {
133                libc::fprintf(output, b"  \0".as_ptr() as *const c_char);
134            }
135            libc::fprintf(output, b"VALUE: \0".as_ptr() as *const c_char);
136            let text = (*attr).children;
137            if !text.is_null() && !(*text).content.is_null() {
138                xmlDebugDumpString(output, (*text).content as *const u8);
139            }
140            libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
141        }
142    }
143}
144
145/// Dump a debug representation of an attribute list.
146///
147/// UPSTREAM-PARITY: `xmlDebugDumpAttrList()`
148#[no_mangle]
149pub unsafe extern "C" fn xmlDebugDumpAttrList(
150    output: *mut _IO_FILE,
151    attr: *mut _xmlAttr,
152    depth: c_int,
153) {
154    if output.is_null() {
155        return;
156    }
157    let mut cur = attr;
158    while !cur.is_null() {
159        unsafe {
160            xmlDebugDumpAttr(output, cur, depth);
161            cur = (*cur).next;
162        }
163    }
164}
165
166/// Dump a single node for debug output.
167///
168/// UPSTREAM-PARITY: `xmlDebugDumpOneNode()`
169#[no_mangle]
170pub unsafe extern "C" fn xmlDebugDumpOneNode(
171    output: *mut _IO_FILE,
172    node: *mut _xmlNode,
173    depth: c_int,
174) {
175    if output.is_null() || node.is_null() {
176        return;
177    }
178    unsafe {
179        // Indent
180        for _ in 0..depth {
181            libc::fprintf(output, b"  \0".as_ptr() as *const c_char);
182        }
183
184        // Print node type
185        match (*node).type_ {
186            1 => {
187                // XML_ELEMENT_NODE
188                libc::fprintf(output, b"ELEMENT \0".as_ptr() as *const c_char);
189                xmlDebugDumpString(output, (*node).name as *const u8);
190                if !(*node).ns.is_null() {
191                    if !(*(*node).ns).prefix.is_null() {
192                        libc::fprintf(
193                            output,
194                            b" (ns=%s)\0".as_ptr() as *const c_char,
195                            (*(*node).ns).prefix,
196                        );
197                    }
198                }
199            }
200            2 => {
201                // XML_ATTRIBUTE_NODE
202                libc::fprintf(output, b"ATTRIBUTE \0".as_ptr() as *const c_char);
203                xmlDebugDumpString(output, (*node).name as *const u8);
204            }
205            3 => {
206                // XML_TEXT_NODE
207                libc::fprintf(output, b"TEXT\0".as_ptr() as *const c_char);
208                if !(*node).content.is_null() {
209                    libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
210                    for _ in 0..(depth + 1) {
211                        libc::fprintf(output, b"  \0".as_ptr() as *const c_char);
212                    }
213                    libc::fprintf(output, b"CONTENT: \0".as_ptr() as *const c_char);
214                    xmlDebugDumpString(output, (*node).content as *const u8);
215                }
216            }
217            4 => {
218                // XML_CDATA_SECTION_NODE
219                libc::fprintf(output, b"CDATA\0".as_ptr() as *const c_char);
220            }
221            5 => {
222                // XML_ENTITY_REF_NODE
223                libc::fprintf(output, b"ENTITY_REF \0".as_ptr() as *const c_char);
224                xmlDebugDumpString(output, (*node).name as *const u8);
225            }
226            6 => {
227                // XML_ENTITY_NODE
228                libc::fprintf(output, b"ENTITY \0".as_ptr() as *const c_char);
229                xmlDebugDumpString(output, (*node).name as *const u8);
230            }
231            7 => {
232                // XML_PI_NODE
233                libc::fprintf(output, b"PI \0".as_ptr() as *const c_char);
234                xmlDebugDumpString(output, (*node).name as *const u8);
235            }
236            8 => {
237                // XML_COMMENT_NODE
238                libc::fprintf(output, b"COMMENT\0".as_ptr() as *const c_char);
239            }
240            9 => {
241                // XML_DOCUMENT_NODE
242                libc::fprintf(output, b"DOCUMENT\0".as_ptr() as *const c_char);
243            }
244            10 => {
245                // XML_DOCUMENT_TYPE_NODE
246                libc::fprintf(output, b"DOCTYPE\0".as_ptr() as *const c_char);
247            }
248            11 => {
249                // XML_DOCUMENT_FRAG_NODE
250                libc::fprintf(output, b"DOCUMENT_FRAG\0".as_ptr() as *const c_char);
251            }
252            13 => {
253                // XML_NAMESPACE_DECL
254                libc::fprintf(output, b"NAMESPACE\0".as_ptr() as *const c_char);
255                if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
256                    libc::fprintf(
257                        output,
258                        b" %s=%s\0".as_ptr() as *const c_char,
259                        (*(*node).ns).prefix,
260                        (*(*node).ns).href,
261                    );
262                }
263            }
264            14 => {
265                // XML_XINCLUDE_START
266                if is_xinclude_node(node) {
267                    libc::fprintf(output, b"XINCLUDE\0".as_ptr() as *const c_char);
268                } else {
269                    libc::fprintf(output, b"XINCLUDE_START\0".as_ptr() as *const c_char);
270                }
271            }
272            15 => {
273                // XML_XINCLUDE_END
274                libc::fprintf(output, b"XINCLUDE_END\0".as_ptr() as *const c_char);
275            }
276            _ => {
277                libc::fprintf(
278                    output,
279                    b"UNKNOWN (%d)\0".as_ptr() as *const c_char,
280                    (*node).type_ as c_int,
281                );
282            }
283        }
284        libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
285
286        // Print attributes
287        if !(*node).properties.is_null() {
288            xmlDebugDumpAttrList(output, (*node).properties, depth + 1);
289        }
290    }
291}
292
293/// Dump a node and its subtree.
294///
295/// UPSTREAM-PARITY: `xmlDebugDumpNode()`
296#[no_mangle]
297pub unsafe extern "C" fn xmlDebugDumpNode(
298    output: *mut _IO_FILE,
299    node: *mut _xmlNode,
300    depth: c_int,
301) {
302    if output.is_null() || node.is_null() || depth > MAX_DEPTH {
303        return;
304    }
305    unsafe {
306        xmlDebugDumpOneNode(output, node, depth);
307
308        // Dump children
309        if !(*node).children.is_null() {
310            let mut child = (*node).children;
311            while !child.is_null() {
312                xmlDebugDumpNode(output, child, depth + 1);
313                child = (*child).next;
314            }
315        }
316    }
317}
318
319/// Dump a node list.
320///
321/// UPSTREAM-PARITY: `xmlDebugDumpNodeList()`
322#[no_mangle]
323pub unsafe extern "C" fn xmlDebugDumpNodeList(
324    output: *mut _IO_FILE,
325    node: *mut _xmlNode,
326    depth: c_int,
327) {
328    if output.is_null() {
329        return;
330    }
331    let mut cur = node;
332    while !cur.is_null() {
333        unsafe {
334            xmlDebugDumpNode(output, cur, depth);
335            cur = (*cur).next;
336        }
337    }
338}
339
340/// Dump an entire document.
341///
342/// UPSTREAM-PARITY: `xmlDebugDumpDocument()`
343#[no_mangle]
344pub unsafe extern "C" fn xmlDebugDumpDocument(output: *mut _IO_FILE, doc: *mut _xmlDoc) {
345    if output.is_null() || doc.is_null() {
346        return;
347    }
348    unsafe {
349        libc::fprintf(output, b"DOCUMENT\0".as_ptr() as *const c_char);
350        if !(*doc).name.is_null() {
351            libc::fprintf(output, b" %s\0".as_ptr() as *const c_char, (*doc).name);
352        }
353        if !(*doc).URL.is_null() {
354            libc::fprintf(output, b" URL=%s\0".as_ptr() as *const c_char, (*doc).URL);
355        }
356        libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
357
358        // Dump children of doc
359        if !(*doc).children.is_null() {
360            let mut child = (*doc).children;
361            while !child.is_null() {
362                xmlDebugDumpNode(output, child, 1);
363                child = (*child).next;
364            }
365        }
366    }
367}
368
369/// Dump the document head (first few nodes).
370///
371/// UPSTREAM-PARITY: `xmlDebugDumpDocumentHead()`
372#[no_mangle]
373pub unsafe extern "C" fn xmlDebugDumpDocumentHead(output: *mut _IO_FILE, doc: *mut _xmlDoc) {
374    if output.is_null() || doc.is_null() {
375        return;
376    }
377    unsafe {
378        xmlDebugDumpDocument(output, doc);
379    }
380}
381
382/// Count the number of nodes in a list reachable via next pointers.
383///
384/// UPSTREAM-PARITY: `xmlLsCountNode()`
385#[no_mangle]
386pub unsafe extern "C" fn xmlLsCountNode(node: *mut _xmlNode) -> c_int {
387    if node.is_null() {
388        return 0;
389    }
390    let mut count: c_int = 0;
391    let mut cur = node;
392    while !cur.is_null() {
393        count += 1;
394        unsafe {
395            cur = (*cur).next;
396        }
397    }
398    count
399}
400
401/// Dump a single node summary (like `ls -l` for nodes).
402///
403/// UPSTREAM-PARITY: `xmlLsOneNode()`
404#[no_mangle]
405pub unsafe extern "C" fn xmlLsOneNode(output: *mut _IO_FILE, node: *mut _xmlNode) {
406    if output.is_null() || node.is_null() {
407        return;
408    }
409    unsafe {
410        match (*node).type_ {
411            1 => {
412                // XML_ELEMENT_NODE
413                libc::fprintf(output, b"E \0".as_ptr() as *const c_char);
414                if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
415                    libc::fprintf(
416                        output,
417                        b"%s:\0".as_ptr() as *const c_char,
418                        (*(*node).ns).prefix,
419                    );
420                }
421                xmlDebugDumpString(output, (*node).name as *const u8);
422            }
423            2 => {
424                libc::fprintf(output, b"A \0".as_ptr() as *const c_char);
425                xmlDebugDumpString(output, (*node).name as *const u8);
426            }
427            3 => {
428                libc::fprintf(output, b"T \0".as_ptr() as *const c_char);
429                if !(*node).content.is_null() {
430                    xmlDebugDumpString(output, (*node).content as *const u8);
431                }
432            }
433            4 => {
434                libc::fprintf(output, b"C \0".as_ptr() as *const c_char);
435            }
436            5 => {
437                libc::fprintf(output, b"E \0".as_ptr() as *const c_char);
438                xmlDebugDumpString(output, (*node).name as *const u8);
439            }
440            6 => {
441                libc::fprintf(output, b"E \0".as_ptr() as *const c_char);
442                xmlDebugDumpString(output, (*node).name as *const u8);
443            }
444            7 => {
445                libc::fprintf(output, b"PI \0".as_ptr() as *const c_char);
446                xmlDebugDumpString(output, (*node).name as *const u8);
447            }
448            8 => {
449                libc::fprintf(output, b"C \0".as_ptr() as *const c_char);
450            }
451            9 => {
452                libc::fprintf(output, b"D \0".as_ptr() as *const c_char);
453            }
454            10 => {
455                libc::fprintf(output, b"DTD \0".as_ptr() as *const c_char);
456            }
457            14 => {
458                libc::fprintf(output, b"X \0".as_ptr() as *const c_char);
459            }
460            _ => {
461                libc::fprintf(
462                    output,
463                    b"? (%d)\0".as_ptr() as *const c_char,
464                    (*node).type_ as c_int,
465                );
466            }
467        }
468        libc::fprintf(output, b"\n\0".as_ptr() as *const c_char);
469    }
470}
471
472/// Re-export _IO_FILE type for C ABI compatibility.
473///
474/// This is typically `FILE` in C. On Linux with libc, `_IO_FILE` is the struct
475/// behind `FILE *`. We use `*mut _IO_FILE` to match the upstream signature.
476pub type _IO_FILE = libc::FILE;
477
478// ═══════════════════════════════════════════════════════════════════════════════
479// Tests
480// ═══════════════════════════════════════════════════════════════════════════════
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use crate::abi::allocator::xmlMalloc;
486    use crate::abi::structs::*;
487    use crate::abi::types::xmlChar;
488    use crate::xml::tree::*;
489
490    /// Helper: create a simple document for testing.
491    unsafe fn create_test_doc() -> *mut _xmlDoc {
492        let doc = new_doc(b"1.0\0".as_ptr() as *const xmlChar);
493        let root = new_node(ptr::null_mut(), b"root\0".as_ptr() as *const xmlChar);
494        doc_set_root_element(doc, root);
495        let child = new_child(root, ptr::null_mut(), b"child\0".as_ptr() as *const xmlChar);
496        // Set a property using set_prop
497        set_prop(
498            child,
499            b"attr1\0".as_ptr() as *const xmlChar,
500            b"value1\0".as_ptr() as *const xmlChar,
501        );
502        doc
503    }
504
505    #[test]
506    fn test_xml_bool_to_text() {
507        unsafe {
508            let t = xmlBoolToText(1);
509            assert!(!t.is_null());
510            let f = xmlBoolToText(0);
511            assert!(!f.is_null());
512            // Check that the strings are correct by comparing first byte
513            assert_eq!(*t as u8, b't');
514            assert_eq!(*f as u8, b'f');
515        }
516    }
517
518    #[test]
519    fn test_debug_dump_string_null() {
520        unsafe {
521            // Should not crash
522            xmlDebugDumpString(ptr::null_mut(), ptr::null());
523            // Should print "(null)"
524            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
525            if !f.is_null() {
526                xmlDebugDumpString(f, ptr::null());
527                libc::fclose(f);
528            }
529        }
530    }
531
532    #[test]
533    fn test_debug_dump_document_null() {
534        unsafe {
535            xmlDebugDumpDocument(ptr::null_mut(), ptr::null_mut());
536            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
537            if !f.is_null() {
538                xmlDebugDumpDocument(f, ptr::null_mut());
539                libc::fclose(f);
540            }
541        }
542    }
543
544    #[test]
545    fn test_debug_dump_node_null() {
546        unsafe {
547            xmlDebugDumpNode(ptr::null_mut(), ptr::null_mut(), 0);
548            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
549            if !f.is_null() {
550                xmlDebugDumpNode(f, ptr::null_mut(), 0);
551                libc::fclose(f);
552            }
553        }
554    }
555
556    #[test]
557    fn test_ls_count_node() {
558        unsafe {
559            let node = new_node(ptr::null_mut(), b"test\0".as_ptr() as *const xmlChar);
560            assert!(!node.is_null());
561            let count = xmlLsCountNode(node);
562            assert_eq!(count, 1);
563
564            // Add a sibling
565            let sibling = new_node(ptr::null_mut(), b"sibling\0".as_ptr() as *const xmlChar);
566            add_sibling(node, sibling);
567            let count = xmlLsCountNode(node);
568            assert_eq!(count, 2);
569
570            free_node(node);
571        }
572    }
573
574    #[test]
575    fn test_debug_dump_attr_null() {
576        unsafe {
577            xmlDebugDumpAttr(ptr::null_mut(), ptr::null_mut(), 0);
578            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
579            if !f.is_null() {
580                xmlDebugDumpAttr(f, ptr::null_mut(), 0);
581                libc::fclose(f);
582            }
583        }
584    }
585
586    #[test]
587    fn test_debug_dump_attr_list_null() {
588        unsafe {
589            xmlDebugDumpAttrList(ptr::null_mut(), ptr::null_mut(), 0);
590        }
591    }
592
593    #[test]
594    fn test_debug_dump_node_list_null() {
595        unsafe {
596            xmlDebugDumpNodeList(ptr::null_mut(), ptr::null_mut(), 0);
597        }
598    }
599
600    #[test]
601    fn test_ls_one_node_null() {
602        unsafe {
603            xmlLsOneNode(ptr::null_mut(), ptr::null_mut());
604            let f = libc::fmemopen(ptr::null_mut(), 0, b"w\0".as_ptr() as *const c_char);
605            if !f.is_null() {
606                xmlLsOneNode(f, ptr::null_mut());
607                libc::fclose(f);
608            }
609        }
610    }
611
612    #[test]
613    fn test_dump_document_head_null() {
614        unsafe {
615            xmlDebugDumpDocumentHead(ptr::null_mut(), ptr::null_mut());
616        }
617    }
618
619    #[test]
620    fn test_ls_count_node_null() {
621        assert_eq!(unsafe { xmlLsCountNode(ptr::null_mut()) }, 0);
622    }
623}