Skip to main content

libxml_rs/xml/c14n/
mod.rs

1//! Canonical XML implementation (§28, §85 Phase 7).
2//!
3//! Inclusive and exclusive canonicalization, comments, namespace propagation,
4//! attribute ordering, character escaping, subsets, node sets.
5//! Must be byte-exact compared to oracle.
6//!
7//! References:
8//! - XML Canonicalization (C14N) — inclusive: <https://www.w3.org/TR/xml-c14n11/>
9//! - Exclusive XML Canonicalization (C14N): <https://www.w3.org/2001/10/xml-exc-c14n>
10
11#![allow(
12    missing_docs,
13    non_snake_case,
14    non_camel_case_types,
15    non_upper_case_globals,
16    clippy::cast_possible_truncation,
17    clippy::cast_sign_loss,
18    clippy::cast_ptr_alignment,
19    clippy::missing_safety_doc,
20    clippy::too_many_lines,
21    clippy::type_complexity
22)]
23
24use core::ffi::c_void;
25use core::ptr;
26use std::collections::HashSet;
27use std::os::raw::{c_char, c_int};
28
29use crate::abi::structs::*;
30use crate::abi::types::xmlElementType::*;
31use crate::abi::types::*;
32use crate::xml::io;
33use crate::xml::tree;
34
35// ═══════════════════════════════════════════════════════════════════════════════
36// Constants
37// ═══════════════════════════════════════════════════════════════════════════════
38
39/// The XML namespace prefix string "xml".
40const XML_XML_PREFIX: &[xmlChar] = b"xml\0";
41
42/// The XML namespace URI string.
43const XML_XML_NS_URI: &[xmlChar] = b"http://www.w3.org/XML/1998/namespace\0";
44
45/// The xmlns namespace URI.
46const _XMLNS_NS_URI: &[xmlChar] = b"http://www.w3.org/2000/xmlns/\0";
47
48/// The xmlns prefix string.
49const _XMLNS_PREFIX: &[xmlChar] = b"xmlns\0";
50
51// ═══════════════════════════════════════════════════════════════════════════════
52// C14N Mode
53// ═══════════════════════════════════════════════════════════════════════════════
54
55/// Canonicalization mode flags.
56///
57/// # UPSTREAM-PARITY
58///
59/// ```c
60/// typedef enum {
61///     XML_C14N_1_0 = 0,       /* C14N 1.0 (inclusive) */
62///     XML_C14N_EXCLUSIVE_1_0 = 1, /* Exclusive C14N 1.0 */
63///     XML_C14N_1_1 = 2,       /* C14N 1.1 */
64///     XML_C14N_1_0_WITH_COMMENTS = 3, /* C14N 1.0 with comments */
65///     XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS = 4, /* Exclusive with comments */
66///     XML_C14N_1_1_WITH_COMMENTS = 5  /* C14N 1.1 with comments */
67/// } xmlC14NMode;
68/// ```
69#[repr(C)]
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum C14nMode {
72    XML_C14N_1_0 = 0,
73    XML_C14N_EXCLUSIVE_1_0 = 1,
74    XML_C14N_1_1 = 2,
75    XML_C14N_1_0_WITH_COMMENTS = 3,
76    XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS = 4,
77    XML_C14N_1_1_WITH_COMMENTS = 5,
78}
79
80impl C14nMode {
81    /// Returns true if this mode includes comments in the output.
82    fn with_comments(self) -> bool {
83        matches!(
84            self,
85            C14nMode::XML_C14N_1_0_WITH_COMMENTS
86                | C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS
87                | C14nMode::XML_C14N_1_1_WITH_COMMENTS
88        )
89    }
90
91    /// Returns true if this mode is exclusive.
92    fn is_exclusive(self) -> bool {
93        matches!(
94            self,
95            C14nMode::XML_C14N_EXCLUSIVE_1_0 | C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS
96        )
97    }
98}
99
100// ═══════════════════════════════════════════════════════════════════════════════
101// C14N Context
102// ═══════════════════════════════════════════════════════════════════════════════
103
104/// Namespace entry for the context stack.
105#[allow(dead_code)]
106#[derive(Debug, Clone)]
107struct NsEntry {
108    /// The prefix (NULL for default namespace).
109    prefix: *const xmlChar,
110    /// The namespace URI (NULL for undeclaration).
111    href: *const xmlChar,
112    /// Whether this namespace was rendered in the output.
113    rendered: bool,
114}
115
116/// C14N serialization context.
117///
118/// Tracks namespace propagation state, visited prefixes, and
119/// document-level state during canonicalization.
120#[derive(Debug)]
121pub struct C14nContext {
122    /// The canonicalization mode.
123    pub mode: C14nMode,
124    /// Stack of in-scope namespace declarations per depth.
125    ns_stack: Vec<Vec<NsEntry>>,
126    /// Set of inclusive namespace prefixes (for exclusive C14N).
127    #[allow(dead_code)]
128    inclusive_ns_prefixes: Option<HashSet<String>>,
129    /// The document being canonicalized.
130    #[allow(dead_code)]
131    doc: *mut _xmlDoc,
132}
133
134impl C14nContext {
135    /// Create a new C14N context.
136    ///
137    /// # SAFETY
138    ///
139    /// - `doc` must be a valid pointer to an `_xmlDoc` or NULL.
140    pub unsafe fn new(
141        doc: *mut _xmlDoc,
142        mode: C14nMode,
143        inclusive_ns_prefixes: Option<HashSet<String>>,
144    ) -> Self {
145        let mut ctx = C14nContext {
146            mode,
147            ns_stack: Vec::new(),
148            inclusive_ns_prefixes,
149            doc,
150        };
151        // Push the initial (document-level) namespace scope, which contains
152        // the implicit `xml` namespace.
153        let xml_prefix = XML_XML_PREFIX.as_ptr() as *const xmlChar;
154        let xml_href = XML_XML_NS_URI.as_ptr() as *const xmlChar;
155        ctx.ns_stack.push(vec![NsEntry {
156            prefix: xml_prefix,
157            href: xml_href,
158            rendered: false,
159        }]);
160        ctx
161    }
162
163    /// Enter a new namespace scope (depth + 1).
164    #[allow(dead_code)]
165    fn push_scope(&mut self) {
166        // Clone the current top scope as the base for the new scope
167        let base = if let Some(top) = self.ns_stack.last() {
168            top.clone()
169        } else {
170            Vec::new()
171        };
172        self.ns_stack.push(base);
173    }
174
175    /// Exit the current namespace scope (depth - 1).
176    #[allow(dead_code)]
177    fn pop_scope(&mut self) {
178        self.ns_stack.pop();
179    }
180
181    /// Add a namespace declaration to the current scope.
182    #[allow(dead_code)]
183    fn add_namespace(&mut self, prefix: *const xmlChar, href: *const xmlChar) {
184        if let Some(top) = self.ns_stack.last_mut() {
185            // Check if this prefix already exists in the current scope
186            if !top
187                .iter()
188                .any(|e| unsafe { crate::abi::exports_xml2::xmlStrEqual(e.prefix, prefix) != 0 })
189            {
190                top.push(NsEntry {
191                    prefix,
192                    href,
193                    rendered: false,
194                });
195            } else {
196                // Update the existing entry
197                if let Some(existing) = top.iter_mut().find(|e| unsafe {
198                    crate::abi::exports_xml2::xmlStrEqual(e.prefix, prefix) != 0
199                }) {
200                    existing.href = href;
201                    existing.rendered = false;
202                }
203            }
204        }
205    }
206
207    /// Check if a prefix is in scope.
208    #[allow(dead_code)]
209    fn is_prefix_in_scope(&self, prefix: *const xmlChar) -> bool {
210        self.ns_stack.iter().rev().any(|scope| {
211            scope
212                .iter()
213                .any(|e| unsafe { crate::abi::exports_xml2::xmlStrEqual(e.prefix, prefix) != 0 })
214        })
215    }
216
217    /// Get the href for a prefix from the current scope.
218    #[allow(dead_code)]
219    fn get_href_for_prefix(&self, prefix: *const xmlChar) -> *const xmlChar {
220        for scope in self.ns_stack.iter().rev() {
221            for entry in scope.iter() {
222                if unsafe { crate::abi::exports_xml2::xmlStrEqual(entry.prefix, prefix) != 0 } {
223                    return entry.href;
224                }
225            }
226        }
227        ptr::null()
228    }
229
230    /// Check if a prefix is in the inclusive namespace prefixes list.
231    #[allow(dead_code)]
232    fn is_inclusive_prefix(&self, prefix: *const xmlChar) -> bool {
233        if let Some(ref set) = self.inclusive_ns_prefixes {
234            if prefix.is_null() {
235                return set.contains("");
236            }
237            let prefix_str = unsafe {
238                let c_str = core::ffi::CStr::from_ptr(prefix as *const c_char);
239                match c_str.to_str() {
240                    Ok(s) => s.to_string(),
241                    Err(_) => return false,
242                }
243            };
244            set.contains(&prefix_str)
245        } else {
246            false
247        }
248    }
249
250    /// Mark a namespace as rendered.
251    #[allow(dead_code)]
252    fn mark_rendered(&mut self, prefix: *const xmlChar) {
253        for scope in self.ns_stack.iter_mut().rev() {
254            for entry in scope.iter_mut() {
255                if unsafe { crate::abi::exports_xml2::xmlStrEqual(entry.prefix, prefix) != 0 } {
256                    entry.rendered = true;
257                    return;
258                }
259            }
260        }
261    }
262
263    /// Check if a namespace is already rendered.
264    #[allow(dead_code)]
265    fn is_rendered(&self, prefix: *const xmlChar) -> bool {
266        for scope in self.ns_stack.iter().rev() {
267            for entry in scope.iter() {
268                if unsafe { crate::abi::exports_xml2::xmlStrEqual(entry.prefix, prefix) != 0 } {
269                    return entry.rendered;
270                }
271            }
272        }
273        false
274    }
275}
276
277// ═══════════════════════════════════════════════════════════════════════════════
278// C14N Escaping
279// ═══════════════════════════════════════════════════════════════════════════════
280
281/// Escape text content per C14N rules.
282///
283/// Canonical XML requires:
284/// - `<` → `&lt;`
285/// - `>` → `&gt;`
286/// - `&` → `&amp;`
287/// - Carriage return `\r` (0x0D) → `&#xD;`
288/// - `]]>` → `]]&gt;`
289///
290/// # SAFETY
291///
292/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
293/// - `text` must be a valid pointer to `len` bytes of xmlChar data, or NULL.
294unsafe fn c14n_escape_text(buf: *mut _xmlBuffer, text: *const xmlChar, len: c_int) {
295    if buf.is_null() || text.is_null() || len <= 0 {
296        return;
297    }
298
299    let mut i: c_int = 0;
300    while i < len {
301        let ch = unsafe { *text.add(i as usize) };
302
303        // Check for `]]>` sequence
304        if ch == b']'
305            && i + 2 < len
306            && unsafe { *text.add(i as usize + 1) == b']' }
307            && unsafe { *text.add(i as usize + 2) == b'>' }
308        {
309            // Write `]]&gt;` — escape the `>` that ends `]]>`
310            io::buf_add(buf, b"]]" as *const u8, 2); // write `]]`
311            io::buf_add(buf, b"&gt;" as *const u8, 4);
312            i += 3;
313            continue;
314        }
315
316        match ch {
317            b'<' => {
318                io::buf_add(buf, b"&lt;" as *const u8, 4);
319            }
320            b'>' => {
321                io::buf_add(buf, b"&gt;" as *const u8, 4);
322            }
323            b'&' => {
324                io::buf_add(buf, b"&amp;" as *const u8, 5);
325            }
326            0x0D => {
327                // Carriage return: &#xD;
328                io::buf_add(buf, b"&#xD;" as *const u8, 5);
329            }
330            _ => {
331                io::buf_add(buf, &ch as *const u8, 1);
332            }
333        }
334        i += 1;
335    }
336}
337
338/// Escape attribute values per C14N rules.
339///
340/// Canonical XML requires:
341/// - `<` → `&lt;`
342/// - `&` → `&amp;`
343/// - `"` → `&quot;`
344/// - Tab (0x09) → `&#x9;`
345/// - Newline (0x0A) → `&#xA;`
346/// - Carriage return (0x0D) → `&#xD;`
347/// - `]]>` → `]]&gt;`
348///
349/// # SAFETY
350///
351/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
352/// - `text` must be a valid pointer to a null-terminated xmlChar string, or NULL.
353unsafe fn c14n_escape_attr(buf: *mut _xmlBuffer, text: *const xmlChar) {
354    if buf.is_null() || text.is_null() {
355        return;
356    }
357
358    let len = tree::xml_strlen(text);
359    let mut i: c_int = 0;
360    while i < len {
361        let ch = unsafe { *text.add(i as usize) };
362
363        // Check for `]]>` sequence
364        if ch == b']'
365            && i + 2 < len
366            && unsafe { *text.add(i as usize + 1) == b']' }
367            && unsafe { *text.add(i as usize + 2) == b'>' }
368        {
369            // Write `]]&gt;`
370            io::buf_add(buf, b"]]" as *const u8, 2); // write `]]`
371            io::buf_add(buf, b"&gt;" as *const u8, 4);
372            i += 3;
373            continue;
374        }
375
376        match ch {
377            b'<' => {
378                io::buf_add(buf, b"&lt;" as *const u8, 4);
379            }
380            b'&' => {
381                io::buf_add(buf, b"&amp;" as *const u8, 5);
382            }
383            b'"' => {
384                io::buf_add(buf, b"&quot;" as *const u8, 6);
385            }
386            0x09 => {
387                // Tab
388                io::buf_add(buf, b"&#x9;" as *const u8, 5);
389            }
390            0x0A => {
391                // Newline
392                io::buf_add(buf, b"&#xA;" as *const u8, 5);
393            }
394            0x0D => {
395                // Carriage return
396                io::buf_add(buf, b"&#xD;" as *const u8, 5);
397            }
398            _ => {
399                io::buf_add(buf, &ch as *const u8, 1);
400            }
401        }
402        i += 1;
403    }
404}
405
406// ═══════════════════════════════════════════════════════════════════════════════
407// Namespace Collection
408// ═══════════════════════════════════════════════════════════════════════════════
409
410/// Represents a collected namespace declaration for output.
411#[derive(Debug, Clone)]
412struct CollectedNs {
413    /// The prefix (NULL for default namespace).
414    prefix: *const xmlChar,
415    /// The namespace URI.
416    href: *const xmlChar,
417}
418
419/// Collect visible namespace declarations for a node per inclusive/exclusive rules.
420///
421/// For **inclusive** C14N:
422/// - All namespaces in scope for the node are included.
423/// - Namespaces inherited from ancestors are included.
424/// - Namespace undeclarations are emitted when needed.
425///
426/// For **exclusive** C14N:
427/// - Only namespaces actually used by the node and its attributes are included.
428/// - If an inclusive-ns-prefix list is provided, those prefixes are always included.
429///
430/// # SAFETY
431///
432/// - `node` must be a valid pointer to an `_xmlNode` or NULL.
433/// - `ctx` must be a valid pointer to a `C14nContext`.
434unsafe fn c14n_collect_namespaces(node: *mut _xmlNode, ctx: &mut C14nContext) -> Vec<CollectedNs> {
435    if node.is_null() {
436        return Vec::new();
437    }
438
439    let n = unsafe { &*node };
440    if n.type_ != XML_ELEMENT_NODE as c_int {
441        return Vec::new();
442    }
443
444    let mut collected: Vec<CollectedNs> = Vec::new();
445    let mut seen_prefixes: Vec<*const xmlChar> = Vec::new();
446
447    // NOTE: The `xml` namespace (prefix `xml`, URI `http://www.w3.org/XML/1998/namespace`)
448    // is always implicitly available per the XML Namespaces specification.
449    // Per C14N, it MUST NOT be explicitly declared in the output.
450    // See W3C Canonical XML 1.0 §2.4 and Exclusive XML Canonicalization §2.1.2.
451
452    if ctx.mode.is_exclusive() {
453        // ── Exclusive C14N namespace collection ──
454        //
455        // Only include namespaces actually used by this node and its attributes,
456        // plus any prefixes in the inclusive-ns-prefixes list.
457
458        // Collect namespaces used by this node
459        let mut used_prefixes: Vec<*const xmlChar> = Vec::new();
460
461        // The node's own namespace
462        if !n.ns.is_null() {
463            let ns = unsafe { &*n.ns };
464            used_prefixes.push(ns.prefix);
465        }
466
467        // Namespaces used by attributes
468        let mut attr = n.properties;
469        while !attr.is_null() {
470            let a = unsafe { &*attr };
471            if !a.ns.is_null() {
472                let ans = unsafe { &*a.ns };
473                if !ans.prefix.is_null()
474                    && !used_prefixes.iter().any(|p| unsafe {
475                        crate::abi::exports_xml2::xmlStrEqual(*p, ans.prefix) != 0
476                    })
477                {
478                    used_prefixes.push(ans.prefix);
479                }
480            }
481            attr = a.next;
482        }
483
484        // For each used prefix, find the namespace declaration by walking
485        // up the ancestor chain
486        for &used_prefix in &used_prefixes {
487            let ns = find_ns_declaration(node, used_prefix);
488            if !ns.is_null() {
489                let ns_ref = unsafe { &*ns };
490                if !seen_prefixes.iter().any(|p| unsafe {
491                    crate::abi::exports_xml2::xmlStrEqual(*p, ns_ref.prefix) != 0
492                }) {
493                    collected.push(CollectedNs {
494                        prefix: ns_ref.prefix,
495                        href: ns_ref.href,
496                    });
497                    seen_prefixes.push(ns_ref.prefix);
498                }
499            }
500        }
501
502        // Include inclusive namespace prefixes
503        if let Some(ref inclusive_set) = ctx.inclusive_ns_prefixes {
504            for inc_prefix_str in inclusive_set.iter() {
505                let inc_prefix = if inc_prefix_str.is_empty() {
506                    ptr::null()
507                } else {
508                    let c_str = format!("{}\0", inc_prefix_str);
509                    c_str.as_ptr() as *const xmlChar
510                };
511
512                if !seen_prefixes.iter().any(|p| {
513                    if inc_prefix.is_null() {
514                        p.is_null()
515                    } else {
516                        !p.is_null()
517                            && unsafe { crate::abi::exports_xml2::xmlStrEqual(*p, inc_prefix) != 0 }
518                    }
519                }) {
520                    let ns = find_ns_declaration(node, inc_prefix);
521                    if !ns.is_null() {
522                        let ns_ref = unsafe { &*ns };
523                        collected.push(CollectedNs {
524                            prefix: ns_ref.prefix,
525                            href: ns_ref.href,
526                        });
527                        seen_prefixes.push(ns_ref.prefix);
528                    }
529                }
530            }
531        }
532    } else {
533        // ── Inclusive C14N namespace collection ──
534        //
535        // Collect ALL namespaces in scope for this node, walking up ancestors.
536
537        let mut cur: *mut _xmlNode = node;
538        while !cur.is_null() {
539            let cur_node = unsafe { &*cur };
540            let mut ns_def = cur_node.nsDef;
541            while !ns_def.is_null() {
542                let ns = unsafe { &*ns_def };
543                let ns_prefix = ns.prefix;
544
545                if !seen_prefixes.iter().any(|p| {
546                    if ns_prefix.is_null() && p.is_null() {
547                        return true;
548                    }
549                    if ns_prefix.is_null() || p.is_null() {
550                        return false;
551                    }
552                    unsafe { crate::abi::exports_xml2::xmlStrEqual(*p, ns_prefix) != 0 }
553                }) {
554                    collected.push(CollectedNs {
555                        prefix: ns_prefix,
556                        href: ns.href,
557                    });
558                    seen_prefixes.push(ns_prefix);
559                }
560                ns_def = ns.next;
561            }
562            cur = cur_node.parent;
563        }
564    }
565
566    collected
567}
568
569/// Find the namespace declaration for a given prefix by walking up the ancestor chain.
570///
571/// # SAFETY
572///
573/// - `node` must be a valid pointer to an `_xmlNode` or NULL.
574unsafe fn find_ns_declaration(node: *mut _xmlNode, prefix: *const xmlChar) -> *mut _xmlNs {
575    if node.is_null() {
576        return ptr::null_mut();
577    }
578
579    let mut cur: *mut _xmlNode = node;
580    while !cur.is_null() {
581        let cur_node = unsafe { &*cur };
582        let mut ns_def = cur_node.nsDef;
583        while !ns_def.is_null() {
584            let ns = unsafe { &*ns_def };
585            let match_found = if prefix.is_null() {
586                // Default namespace: prefix should be NULL
587                ns.prefix.is_null()
588            } else if ns.prefix.is_null() {
589                false
590            } else {
591                unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.prefix, prefix) != 0 }
592            };
593            if match_found {
594                return ns_def;
595            }
596            ns_def = ns.next;
597        }
598        cur = cur_node.parent;
599    }
600
601    ptr::null_mut()
602}
603
604// ═══════════════════════════════════════════════════════════════════════════════
605// Namespace Serialization
606// ═══════════════════════════════════════════════════════════════════════════════
607
608/// Serialize namespace declarations to the output buffer.
609///
610/// Outputs namespace declarations in canonical order.
611///
612/// # SAFETY
613///
614/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
615/// - `ns_list` contains raw pointers that must be valid.
616unsafe fn c14n_serialize_namespaces(buf: *mut _xmlBuffer, ns_list: &[CollectedNs]) {
617    if buf.is_null() || ns_list.is_empty() {
618        return;
619    }
620
621    for ns in ns_list {
622        io::buf_add(buf, b" xmlns" as *const u8, 6);
623        if !ns.prefix.is_null() {
624            io::buf_ccat(buf, b':');
625            io::buf_cat(buf, ns.prefix);
626        }
627        io::buf_add(buf, b"=\"" as *const u8, 2);
628        if !ns.href.is_null() {
629            c14n_escape_attr(buf, ns.href);
630        }
631        io::buf_ccat(buf, b'"');
632    }
633}
634
635// ═══════════════════════════════════════════════════════════════════════════════
636// Attribute Ordering (Canonical)
637// ═══════════════════════════════════════════════════════════════════════════════
638
639/// Compare two attribute pointers for canonical ordering.
640///
641/// Canonical ordering: lexicographic by namespace URI (with empty namespace
642/// coming first), then by local name.
643///
644/// Returns negative, zero, or positive.
645///
646/// # SAFETY
647///
648/// - `a` and `b` must be valid pointers to `_xmlAttr`.
649unsafe fn compare_attrs(a: *const _xmlAttr, b: *const _xmlAttr) -> std::cmp::Ordering {
650    let attr_a = unsafe { &*a };
651    let attr_b = unsafe { &*b };
652
653    // Get namespace URIs (empty string if no namespace)
654    let ns_uri_a = if !attr_a.ns.is_null() {
655        unsafe { &*attr_a.ns }.href
656    } else {
657        ptr::null()
658    };
659    let ns_uri_b = if !attr_b.ns.is_null() {
660        unsafe { &*attr_b.ns }.href
661    } else {
662        ptr::null()
663    };
664
665    // Namespace URI comparison: NULL (no namespace) sorts before any URI
666    if ns_uri_a.is_null() && !ns_uri_b.is_null() {
667        return std::cmp::Ordering::Less;
668    }
669    if !ns_uri_a.is_null() && ns_uri_b.is_null() {
670        return std::cmp::Ordering::Greater;
671    }
672    if !ns_uri_a.is_null() && !ns_uri_b.is_null() {
673        let cmp = unsafe { crate::abi::exports_xml2::xmlStrcmp(ns_uri_a, ns_uri_b) };
674        if cmp != 0 {
675            return cmp.cmp(&0);
676        }
677    }
678
679    // Local name comparison
680    let name_a = attr_a.name;
681    let name_b = attr_b.name;
682    if name_a.is_null() && name_b.is_null() {
683        return std::cmp::Ordering::Equal;
684    }
685    if name_a.is_null() {
686        return std::cmp::Ordering::Less;
687    }
688    if name_b.is_null() {
689        return std::cmp::Ordering::Greater;
690    }
691    let cmp = unsafe { crate::abi::exports_xml2::xmlStrcmp(name_a, name_b) };
692    cmp.cmp(&0)
693}
694
695/// Serialize attributes in canonical order.
696///
697/// Attributes are sorted lexicographically by namespace URI then local name.
698/// The `xmlns:*` attributes are NOT included here — they are handled separately
699/// by `c14n_serialize_namespaces`.
700///
701/// # SAFETY
702///
703/// - `node` must be a valid pointer to an `_xmlNode` or NULL.
704/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
705unsafe fn c14n_serialize_attributes(node: *mut _xmlNode, buf: *mut _xmlBuffer) {
706    if node.is_null() || buf.is_null() {
707        return;
708    }
709
710    let n = unsafe { &*node };
711    if n.type_ != XML_ELEMENT_NODE as c_int {
712        return;
713    }
714
715    // Collect attributes into a vector for sorting
716    let mut attrs: Vec<*mut _xmlAttr> = Vec::new();
717    let mut cur_attr = n.properties;
718    while !cur_attr.is_null() {
719        attrs.push(cur_attr);
720        cur_attr = unsafe { (*cur_attr).next };
721    }
722
723    // Sort attributes canonically
724    attrs.sort_by(|a, b| unsafe { compare_attrs(*a, *b) });
725
726    // Serialize each attribute
727    for &attr in &attrs {
728        let a = unsafe { &*attr };
729
730        io::buf_ccat(buf, b' ');
731
732        // Write attribute name with optional namespace prefix
733        if !a.ns.is_null() {
734            let ans = unsafe { &*a.ns };
735            if !ans.prefix.is_null() {
736                io::buf_cat(buf, ans.prefix);
737                io::buf_ccat(buf, b':');
738            }
739        }
740        if !a.name.is_null() {
741            io::buf_cat(buf, a.name);
742        }
743
744        io::buf_add(buf, b"=\"" as *const u8, 2);
745
746        // Attribute value from child text node
747        if !a.children.is_null() {
748            let child = unsafe { &*a.children };
749            if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
750                c14n_escape_attr(buf, child.content);
751            }
752        }
753
754        io::buf_ccat(buf, b'"');
755    }
756}
757
758// ═══════════════════════════════════════════════════════════════════════════════
759// Node Serialization (Canonical)
760// ═══════════════════════════════════════════════════════════════════════════════
761
762/// Serialize a single node in canonical form.
763///
764/// This is the core recursive canonical serialization function.
765///
766/// # SAFETY
767///
768/// - `node` must be a valid pointer to an `_xmlNode` or NULL.
769/// - `ctx` must be a valid pointer to a `C14nContext`.
770/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
771unsafe fn c14n_serialize_node(node: *mut _xmlNode, ctx: &mut C14nContext, buf: *mut _xmlBuffer) {
772    if node.is_null() || buf.is_null() {
773        return;
774    }
775
776    let n = unsafe { &*node };
777
778    match n.type_ {
779        t if t == XML_ELEMENT_NODE as c_int => {
780            c14n_serialize_element(node, ctx, buf);
781        }
782        t if t == XML_TEXT_NODE as c_int => {
783            if !n.content.is_null() {
784                c14n_escape_text(buf, n.content, tree::xml_strlen(n.content));
785            }
786        }
787        t if t == XML_CDATA_SECTION_NODE as c_int => {
788            // C14N converts CDATA sections to text
789            if !n.content.is_null() {
790                c14n_escape_text(buf, n.content, tree::xml_strlen(n.content));
791            }
792        }
793        t if t == XML_COMMENT_NODE as c_int => {
794            if ctx.mode.with_comments() {
795                io::buf_add(buf, b"<!--" as *const u8, 4);
796                if !n.content.is_null() {
797                    io::buf_cat(buf, n.content);
798                }
799                io::buf_add(buf, b"-->" as *const u8, 3);
800            }
801        }
802        t if t == XML_PI_NODE as c_int => {
803            io::buf_add(buf, b"<?" as *const u8, 2);
804            if !n.name.is_null() {
805                io::buf_cat(buf, n.name);
806            }
807            if !n.content.is_null() && unsafe { *n.content != 0 } {
808                io::buf_ccat(buf, b' ');
809                io::buf_cat(buf, n.content);
810            }
811            io::buf_add(buf, b"?>" as *const u8, 2);
812        }
813        t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
814            // Serialize children of the document node (skip XML declaration)
815            let mut child = n.children;
816            while !child.is_null() {
817                c14n_serialize_node(child, ctx, buf);
818                child = unsafe { (*child).next };
819            }
820        }
821        t if t == XML_DTD_NODE as c_int || t == XML_DOCUMENT_TYPE_NODE as c_int => {
822            // Skip DTD nodes in C14N output
823        }
824        t if t == XML_ENTITY_REF_NODE as c_int => {
825            // Entity references are not expanded in canonical XML;
826            // instead, the reference itself is output.
827            if !n.name.is_null() {
828                io::buf_ccat(buf, b'&');
829                io::buf_cat(buf, n.name);
830                io::buf_ccat(buf, b';');
831            }
832        }
833        _ => {
834            // For unknown types, write content if present
835            if !n.content.is_null() {
836                c14n_escape_text(buf, n.content, tree::xml_strlen(n.content));
837            }
838        }
839    }
840}
841
842/// Serialize an element node in canonical form.
843///
844/// This handles namespace collection, attribute ordering, and recursive
845/// child serialization.
846///
847/// # SAFETY
848///
849/// - `node` must be a valid pointer to an `_xmlNode` or NULL.
850/// - `ctx` must be a valid pointer to a `C14nContext`.
851/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
852unsafe fn c14n_serialize_element(node: *mut _xmlNode, ctx: &mut C14nContext, buf: *mut _xmlBuffer) {
853    if node.is_null() || buf.is_null() {
854        return;
855    }
856
857    let n = unsafe { &*node };
858
859    // Push a new namespace scope for this element
860    ctx.push_scope();
861
862    // Collect namespaces for this element
863    let ns_list = c14n_collect_namespaces(node, ctx);
864
865    // Open element: `<`
866    io::buf_ccat(buf, b'<');
867
868    // Write element name with optional namespace prefix
869    if !n.ns.is_null() {
870        let ns = unsafe { &*n.ns };
871        if !ns.prefix.is_null() {
872            io::buf_cat(buf, ns.prefix);
873            io::buf_ccat(buf, b':');
874        }
875    }
876    if !n.name.is_null() {
877        io::buf_cat(buf, n.name);
878    }
879
880    // Write namespace declarations
881    c14n_serialize_namespaces(buf, &ns_list);
882
883    // Write attributes in canonical order
884    c14n_serialize_attributes(node, buf);
885
886    if n.children.is_null() {
887        // Self-closing tag for empty elements
888        io::buf_add(buf, b"/>" as *const u8, 2);
889    } else {
890        io::buf_ccat(buf, b'>');
891
892        // Serialize children
893        let mut child = n.children;
894        while !child.is_null() {
895            c14n_serialize_node(child, ctx, buf);
896            child = unsafe { (*child).next };
897        }
898
899        // Close element: `</name>`
900        io::buf_add(buf, b"</" as *const u8, 2);
901        if !n.ns.is_null() {
902            let ns = unsafe { &*n.ns };
903            if !ns.prefix.is_null() {
904                io::buf_cat(buf, ns.prefix);
905                io::buf_ccat(buf, b':');
906            }
907        }
908        if !n.name.is_null() {
909            io::buf_cat(buf, n.name);
910        }
911        io::buf_ccat(buf, b'>');
912    }
913
914    // Pop namespace scope
915    ctx.pop_scope();
916}
917
918// ═══════════════════════════════════════════════════════════════════════════════
919// Public API — Document-level canonicalization
920// ═══════════════════════════════════════════════════════════════════════════════
921
922/// Canonicalize a document (or a subset of nodes) to an xmlBuffer.
923///
924/// If `nodes` is non-NULL, it is a NULL-terminated array of node pointers
925/// that form the subset to canonicalize. If NULL, the entire document is
926/// canonicalized.
927///
928/// # SAFETY
929///
930/// - `doc` must be a valid pointer to an `_xmlDoc` or NULL.
931/// - `nodes` may be NULL (meaning the entire document) or a NULL-terminated
932///   array of `_xmlNode` pointers.
933/// - `buf` must be a valid pointer to a mutable `_xmlBuffer`.
934/// - `inclusive_ns_prefixes` is a comma-separated list of prefixes or NULL.
935pub unsafe fn c14n_doc_dump_memory(
936    doc: *mut _xmlDoc,
937    nodes: *mut *mut _xmlNode,
938    mode: C14nMode,
939    inclusive_ns_prefixes: *const xmlChar,
940    with_comments: c_int,
941    result: *mut *mut xmlChar,
942) -> c_int {
943    if doc.is_null() || result.is_null() {
944        return -1;
945    }
946
947    // Determine the effective mode, considering with_comments flag
948    let effective_mode = if with_comments != 0 {
949        match mode {
950            C14nMode::XML_C14N_1_0 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
951            C14nMode::XML_C14N_EXCLUSIVE_1_0 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
952            C14nMode::XML_C14N_1_1 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
953            _ => mode,
954        }
955    } else {
956        mode
957    };
958
959    // Parse inclusive namespace prefixes
960    let inclusive_set = parse_inclusive_prefixes(inclusive_ns_prefixes);
961
962    let mut ctx = C14nContext::new(doc, effective_mode, inclusive_set);
963
964    // Create output buffer
965    let buf = io::buf_create(-1);
966    if buf.is_null() {
967        return -1;
968    }
969
970    if nodes.is_null() {
971        // Canonicalize entire document
972        let doc_node = doc as *mut _xmlNode;
973        let d = unsafe { &*doc_node };
974        let mut child = d.children;
975        while !child.is_null() {
976            c14n_serialize_node(child, &mut ctx, buf);
977            child = unsafe { (*child).next };
978        }
979    } else {
980        // Canonicalize a subset of nodes in document order
981        // First, collect and sort nodes in document order
982        let mut node_vec: Vec<*mut _xmlNode> = Vec::new();
983        let mut i = 0;
984        loop {
985            let n = unsafe { *nodes.add(i) };
986            if n.is_null() {
987                break;
988            }
989            node_vec.push(n);
990            i += 1;
991        }
992
993        // Sort nodes in document order
994        node_vec.sort_by(|a, b| unsafe { cmp_document_order(*a, *b) });
995
996        for &n in &node_vec {
997            c14n_serialize_node(n, &mut ctx, buf);
998        }
999    }
1000
1001    // Extract result string
1002    let content = io::buf_content(buf);
1003    let len = io::buf_length(buf);
1004    if content.is_null() || len < 0 {
1005        io::buf_free(buf);
1006        return -1;
1007    }
1008
1009    // Duplicate the content for the caller
1010    let result_str = crate::abi::exports_xml2::xmlStrdup(content);
1011    io::buf_free(buf);
1012
1013    if result_str.is_null() {
1014        return -1;
1015    }
1016
1017    unsafe {
1018        *result = result_str;
1019    }
1020
1021    len
1022}
1023
1024/// Canonicalize a document to an output buffer via callback.
1025///
1026/// # SAFETY
1027///
1028/// - `doc` must be a valid pointer to an `_xmlDoc` or NULL.
1029/// - `callback` must be a valid function pointer.
1030/// - `inclusive_ns_prefixes` is a comma-separated list of prefixes or NULL.
1031pub unsafe fn c14n_execute(
1032    doc: *mut _xmlDoc,
1033    mode: C14nMode,
1034    inclusive_ns_prefixes: *const xmlChar,
1035    with_comments: c_int,
1036    callback: Option<
1037        unsafe extern "C" fn(ctx: *mut c_void, data: *const c_char, len: c_int) -> c_int,
1038    >,
1039    callback_data: *mut c_void,
1040) -> c_int {
1041    if doc.is_null() || callback.is_none() {
1042        return -1;
1043    }
1044
1045    let callback = callback.unwrap();
1046
1047    // Determine effective mode
1048    let effective_mode = if with_comments != 0 {
1049        match mode {
1050            C14nMode::XML_C14N_1_0 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
1051            C14nMode::XML_C14N_EXCLUSIVE_1_0 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
1052            C14nMode::XML_C14N_1_1 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
1053            _ => mode,
1054        }
1055    } else {
1056        mode
1057    };
1058
1059    // Parse inclusive namespace prefixes
1060    let inclusive_set = parse_inclusive_prefixes(inclusive_ns_prefixes);
1061
1062    let mut ctx = C14nContext::new(doc, effective_mode, inclusive_set);
1063
1064    // Create output buffer
1065    let buf = io::buf_create(-1);
1066    if buf.is_null() {
1067        return -1;
1068    }
1069
1070    // Canonicalize entire document
1071    let doc_node = doc as *mut _xmlNode;
1072    let d = unsafe { &*doc_node };
1073    let mut child = d.children;
1074    while !child.is_null() {
1075        c14n_serialize_node(child, &mut ctx, buf);
1076        child = unsafe { (*child).next };
1077    }
1078
1079    // Call the callback with the result
1080    let content = io::buf_content(buf);
1081    let len = io::buf_length(buf);
1082    if content.is_null() || len < 0 {
1083        io::buf_free(buf);
1084        return -1;
1085    }
1086
1087    let ret = unsafe { callback(callback_data, content as *const c_char, len) };
1088
1089    io::buf_free(buf);
1090    ret
1091}
1092
1093/// Canonicalize a document and save to an output buffer.
1094///
1095/// # SAFETY
1096///
1097/// - `doc` must be a valid pointer to an `_xmlDoc` or NULL.
1098/// - `output` must be a valid pointer to an `_xmlOutputBuffer` or NULL.
1099/// - `inclusive_ns_prefixes` is a comma-separated list of prefixes or NULL.
1100pub unsafe fn c14n_doc_save_to(
1101    doc: *mut _xmlDoc,
1102    nodes: *mut *mut _xmlNode,
1103    mode: C14nMode,
1104    inclusive_ns_prefixes: *const xmlChar,
1105    with_comments: c_int,
1106    output: *mut _xmlOutputBuffer,
1107) -> c_int {
1108    if doc.is_null() || output.is_null() {
1109        return -1;
1110    }
1111
1112    // Determine effective mode
1113    let effective_mode = if with_comments != 0 {
1114        match mode {
1115            C14nMode::XML_C14N_1_0 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
1116            C14nMode::XML_C14N_EXCLUSIVE_1_0 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
1117            C14nMode::XML_C14N_1_1 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
1118            _ => mode,
1119        }
1120    } else {
1121        mode
1122    };
1123
1124    // Parse inclusive namespace prefixes
1125    let inclusive_set = parse_inclusive_prefixes(inclusive_ns_prefixes);
1126
1127    let mut ctx = C14nContext::new(doc, effective_mode, inclusive_set);
1128
1129    // Create buffer for serialization
1130    let buf = io::buf_create(-1);
1131    if buf.is_null() {
1132        return -1;
1133    }
1134
1135    if nodes.is_null() {
1136        // Canonicalize entire document
1137        let doc_node = doc as *mut _xmlNode;
1138        let d = unsafe { &*doc_node };
1139        let mut child = d.children;
1140        while !child.is_null() {
1141            c14n_serialize_node(child, &mut ctx, buf);
1142            child = unsafe { (*child).next };
1143        }
1144    } else {
1145        // Canonicalize a subset of nodes in document order
1146        let mut node_vec: Vec<*mut _xmlNode> = Vec::new();
1147        let mut i = 0;
1148        loop {
1149            let n = unsafe { *nodes.add(i) };
1150            if n.is_null() {
1151                break;
1152            }
1153            node_vec.push(n);
1154            i += 1;
1155        }
1156
1157        node_vec.sort_by(|a, b| unsafe { cmp_document_order(*a, *b) });
1158
1159        for &n in &node_vec {
1160            c14n_serialize_node(n, &mut ctx, buf);
1161        }
1162    }
1163
1164    // Write to output buffer
1165    let content = io::buf_content(buf);
1166    let len = io::buf_length(buf);
1167    if content.is_null() || len < 0 {
1168        io::buf_free(buf);
1169        return -1;
1170    }
1171
1172    let written = io::output_buffer_write(output, len, content as *const c_char);
1173    io::buf_free(buf);
1174
1175    // Flush the output buffer to ensure data reaches the underlying target
1176    let flush_ret = io::output_buffer_flush(output);
1177    if flush_ret < 0 {
1178        return written;
1179    }
1180
1181    written
1182}
1183
1184// ═══════════════════════════════════════════════════════════════════════════════
1185// Helper Functions
1186// ═══════════════════════════════════════════════════════════════════════════════
1187
1188/// Parse a comma-separated list of inclusive namespace prefixes.
1189///
1190/// Returns `None` if the input is NULL (meaning no inclusive prefixes).
1191fn parse_inclusive_prefixes(input: *const xmlChar) -> Option<HashSet<String>> {
1192    if input.is_null() {
1193        return None;
1194    }
1195
1196    let input_str = unsafe {
1197        let c_str = core::ffi::CStr::from_ptr(input as *const c_char);
1198        match c_str.to_str() {
1199            Ok(s) => s.to_string(),
1200            Err(_) => return None,
1201        }
1202    };
1203
1204    if input_str.is_empty() {
1205        return None;
1206    }
1207
1208    let mut set = HashSet::new();
1209    for prefix in input_str.split(',') {
1210        let trimmed = prefix.trim();
1211        if !trimmed.is_empty() {
1212            set.insert(trimmed.to_string());
1213        }
1214    }
1215
1216    if set.is_empty() {
1217        None
1218    } else {
1219        Some(set)
1220    }
1221}
1222
1223/// Compare two nodes in document order.
1224///
1225/// Returns negative if `a` comes before `b`, positive if `a` comes after `b`.
1226///
1227/// # SAFETY
1228///
1229/// - `a` and `b` must be valid pointers to `_xmlNode`.
1230unsafe fn cmp_document_order(a: *mut _xmlNode, b: *mut _xmlNode) -> std::cmp::Ordering {
1231    if a == b {
1232        return std::cmp::Ordering::Equal;
1233    }
1234
1235    // Build ancestor chains
1236    let mut ancestors_a: Vec<*mut _xmlNode> = Vec::new();
1237    let mut cur = a;
1238    while !cur.is_null() {
1239        ancestors_a.push(cur);
1240        cur = unsafe { (*cur).parent };
1241    }
1242
1243    let mut ancestors_b: Vec<*mut _xmlNode> = Vec::new();
1244    let mut cur = b;
1245    while !cur.is_null() {
1246        ancestors_b.push(cur);
1247        cur = unsafe { (*cur).parent };
1248    }
1249
1250    // Find the lowest common ancestor
1251    let mut i = ancestors_a.len();
1252    let mut j = ancestors_b.len();
1253
1254    while i > 0 && j > 0 && ancestors_a[i - 1] == ancestors_b[j - 1] {
1255        i -= 1;
1256        j -= 1;
1257    }
1258
1259    if i == 0 || j == 0 {
1260        // One is an ancestor of the other
1261        if i == 0 {
1262            return std::cmp::Ordering::Less;
1263        }
1264        return std::cmp::Ordering::Greater;
1265    }
1266
1267    // The nodes at i-1 and j-1 are siblings under the common ancestor.
1268    // Determine their order by walking the sibling chain.
1269    let sibling_a = ancestors_a[i - 1];
1270    let sibling_b = ancestors_b[j - 1];
1271
1272    // Walk forward from sibling_a to see if we find sibling_b
1273    let mut walk = sibling_a;
1274    while !walk.is_null() {
1275        if walk == sibling_b {
1276            return std::cmp::Ordering::Less;
1277        }
1278        walk = unsafe { (*walk).next };
1279    }
1280
1281    // sibling_b must be before sibling_a
1282    std::cmp::Ordering::Greater
1283}
1284
1285// ═══════════════════════════════════════════════════════════════════════════════
1286// C ABI Exports
1287// ═══════════════════════════════════════════════════════════════════════════════
1288
1289/// Serialize canonical XML to a memory buffer.
1290///
1291/// # UPSTREAM-PARITY
1292///
1293/// ```c
1294/// int xmlC14NDocDumpMemory(
1295///     xmlDocPtr doc,
1296///     xmlNodeSetPtr nodes,
1297///     int mode,
1298///     xmlChar **inclusive_ns_prefixes,
1299///     int with_comments,
1300///     xmlChar **result
1301/// );
1302/// ```
1303///
1304/// Serializes `doc` (or a subset of `nodes`) to canonical XML.
1305/// The result is allocated with `xmlMalloc` and must be freed by the caller
1306/// with `xmlFree`.
1307///
1308/// Returns the length of the result string in bytes, or -1 on error.
1309///
1310/// # SAFETY
1311///
1312/// - `doc` must be a valid pointer to an `_xmlDoc` or NULL.
1313/// - `nodes` may be NULL (entire document) or a NULL-terminated array of `_xmlNode` pointers.
1314/// - `result` must be a valid pointer to a `xmlChar*` that will receive the result.
1315#[no_mangle]
1316pub unsafe extern "C" fn xmlC14NDocDumpMemory(
1317    doc: *mut _xmlDoc,
1318    nodes: *mut *mut _xmlNode,
1319    mode: c_int,
1320    inclusive_ns_prefixes: *mut *mut xmlChar,
1321    with_comments: c_int,
1322    result: *mut *mut xmlChar,
1323) -> c_int {
1324    // SAFETY: Delegates to the safe internal implementation.
1325    // The caller must provide valid pointers or NULL as documented.
1326
1327    let c14n_mode = match mode {
1328        0 => C14nMode::XML_C14N_1_0,
1329        1 => C14nMode::XML_C14N_EXCLUSIVE_1_0,
1330        2 => C14nMode::XML_C14N_1_1,
1331        3 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
1332        4 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
1333        5 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
1334        _ => return -1,
1335    };
1336
1337    // The inclusive_ns_prefixes parameter in the upstream API is a
1338    // NULL-terminated array of xmlChar* strings. We join them with commas
1339    // for our internal parser.
1340    let joined_prefixes = if !inclusive_ns_prefixes.is_null() {
1341        let mut parts: Vec<*mut xmlChar> = Vec::new();
1342        let mut i = 0;
1343        loop {
1344            let p = unsafe { *inclusive_ns_prefixes.add(i) };
1345            if p.is_null() {
1346                break;
1347            }
1348            parts.push(p);
1349            i += 1;
1350        }
1351
1352        if parts.is_empty() {
1353            ptr::null()
1354        } else {
1355            // Build comma-separated string
1356            let mut result_str = Vec::<u8>::new();
1357            for (idx, &part) in parts.iter().enumerate() {
1358                if idx > 0 {
1359                    result_str.push(b',');
1360                }
1361                let len = tree::xml_strlen(part);
1362                let part_slice = unsafe { core::slice::from_raw_parts(part, len as usize) };
1363                result_str.extend_from_slice(part_slice);
1364            }
1365            result_str.push(0); // null-terminate
1366            result_str.as_ptr() as *const xmlChar
1367        }
1368    } else {
1369        ptr::null()
1370    };
1371
1372    unsafe {
1373        c14n_doc_dump_memory(
1374            doc,
1375            nodes,
1376            c14n_mode,
1377            joined_prefixes,
1378            with_comments,
1379            result,
1380        )
1381    }
1382}
1383
1384/// Canonicalize XML with a callback for output.
1385///
1386/// # UPSTREAM-PARITY
1387///
1388/// ```c
1389/// int xmlC14NExecute(
1390///     xmlDocPtr doc,
1391///     int mode,
1392///     xmlChar **inclusive_ns_prefixes,
1393///     int with_comments,
1394///     xmlC14NIOWriteCallback callback,
1395///     void *callback_data
1396/// );
1397/// ```
1398///
1399/// # SAFETY
1400///
1401/// - `doc` must be a valid pointer to an `_xmlDoc` or NULL.
1402/// - `callback` must be a valid function pointer or NULL.
1403#[no_mangle]
1404pub unsafe extern "C" fn xmlC14NExecute(
1405    doc: *mut _xmlDoc,
1406    mode: c_int,
1407    inclusive_ns_prefixes: *mut *mut xmlChar,
1408    with_comments: c_int,
1409    callback: Option<
1410        unsafe extern "C" fn(ctx: *mut c_void, data: *const c_char, len: c_int) -> c_int,
1411    >,
1412    callback_data: *mut c_void,
1413) -> c_int {
1414    // SAFETY: Delegates to the safe internal implementation.
1415
1416    let c14n_mode = match mode {
1417        0 => C14nMode::XML_C14N_1_0,
1418        1 => C14nMode::XML_C14N_EXCLUSIVE_1_0,
1419        2 => C14nMode::XML_C14N_1_1,
1420        3 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
1421        4 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
1422        5 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
1423        _ => return -1,
1424    };
1425
1426    let joined_prefixes = if !inclusive_ns_prefixes.is_null() {
1427        let mut parts: Vec<*mut xmlChar> = Vec::new();
1428        let mut i = 0;
1429        loop {
1430            let p = unsafe { *inclusive_ns_prefixes.add(i) };
1431            if p.is_null() {
1432                break;
1433            }
1434            parts.push(p);
1435            i += 1;
1436        }
1437
1438        if parts.is_empty() {
1439            ptr::null()
1440        } else {
1441            let mut result_str = Vec::<u8>::new();
1442            for (idx, &part) in parts.iter().enumerate() {
1443                if idx > 0 {
1444                    result_str.push(b',');
1445                }
1446                let len = tree::xml_strlen(part);
1447                let part_slice = unsafe { core::slice::from_raw_parts(part, len as usize) };
1448                result_str.extend_from_slice(part_slice);
1449            }
1450            result_str.push(0);
1451            result_str.as_ptr() as *const xmlChar
1452        }
1453    } else {
1454        ptr::null()
1455    };
1456
1457    unsafe {
1458        c14n_execute(
1459            doc,
1460            c14n_mode,
1461            joined_prefixes,
1462            with_comments,
1463            callback,
1464            callback_data,
1465        )
1466    }
1467}
1468
1469/// Save canonical XML to an output buffer.
1470///
1471/// # UPSTREAM-PARITY
1472///
1473/// ```c
1474/// int xmlC14NDocSaveTo(
1475///     xmlDocPtr doc,
1476///     xmlNodeSetPtr nodes,
1477///     int mode,
1478///     xmlChar **inclusive_ns_prefixes,
1479///     int with_comments,
1480///     xmlOutputBufferPtr output
1481/// );
1482/// ```
1483///
1484/// # SAFETY
1485///
1486/// - `doc` must be a valid pointer to an `_xmlDoc` or NULL.
1487/// - `output` must be a valid pointer to an `_xmlOutputBuffer` or NULL.
1488#[no_mangle]
1489pub unsafe extern "C" fn xmlC14NDocSaveTo(
1490    doc: *mut _xmlDoc,
1491    nodes: *mut *mut _xmlNode,
1492    mode: c_int,
1493    inclusive_ns_prefixes: *mut *mut xmlChar,
1494    with_comments: c_int,
1495    output: *mut _xmlOutputBuffer,
1496) -> c_int {
1497    // SAFETY: Delegates to the safe internal implementation.
1498
1499    let c14n_mode = match mode {
1500        0 => C14nMode::XML_C14N_1_0,
1501        1 => C14nMode::XML_C14N_EXCLUSIVE_1_0,
1502        2 => C14nMode::XML_C14N_1_1,
1503        3 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
1504        4 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
1505        5 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
1506        _ => return -1,
1507    };
1508
1509    let joined_prefixes: Option<Vec<u8>> = if !inclusive_ns_prefixes.is_null() {
1510        let mut parts: Vec<*mut xmlChar> = Vec::new();
1511        let mut i = 0;
1512        loop {
1513            let p = unsafe { *inclusive_ns_prefixes.add(i) };
1514            if p.is_null() {
1515                break;
1516            }
1517            parts.push(p);
1518            i += 1;
1519        }
1520
1521        if parts.is_empty() {
1522            None
1523        } else {
1524            let mut result_str = Vec::<u8>::new();
1525            for (idx, &part) in parts.iter().enumerate() {
1526                if idx > 0 {
1527                    result_str.push(b',');
1528                }
1529                let len = tree::xml_strlen(part);
1530                let part_slice = unsafe { core::slice::from_raw_parts(part, len as usize) };
1531                result_str.extend_from_slice(part_slice);
1532            }
1533            result_str.push(0);
1534            Some(result_str)
1535        }
1536    } else {
1537        None
1538    };
1539
1540    let joined_ptr = joined_prefixes
1541        .as_ref()
1542        .map(|v| v.as_ptr() as *const xmlChar)
1543        .unwrap_or(ptr::null());
1544
1545    unsafe { c14n_doc_save_to(doc, nodes, c14n_mode, joined_ptr, with_comments, output) }
1546}
1547
1548// ═══════════════════════════════════════════════════════════════════════════════
1549// Tests
1550// ═══════════════════════════════════════════════════════════════════════════════
1551
1552#[cfg(test)]
1553mod tests {
1554    use super::*;
1555    use crate::abi::allocator::xmlFreeImpl;
1556    use crate::xml::io;
1557    use crate::xml::tree;
1558    use core::ptr;
1559    use std::os::raw::c_int;
1560
1561    /// Helper: create a simple document for testing.
1562    ///
1563    /// Creates: `<root><child attr="value">text</child></root>`
1564    unsafe fn create_simple_doc() -> *mut _xmlDoc {
1565        let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1566        assert!(!doc.is_null());
1567
1568        let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
1569        assert!(!root.is_null());
1570        tree::doc_set_root_element(doc, root);
1571
1572        let child = tree::new_node(ptr::null_mut(), b"child\0" as *const u8 as *const xmlChar);
1573        assert!(!child.is_null());
1574        tree::add_child(root, child);
1575
1576        // Set attribute
1577        tree::set_prop(
1578            child,
1579            b"attr\0" as *const u8 as *const xmlChar,
1580            b"value\0" as *const u8 as *const xmlChar,
1581        );
1582
1583        // Set text content
1584        let text = tree::new_text(b"text\0" as *const u8 as *const xmlChar);
1585        assert!(!text.is_null());
1586        tree::add_child(child, text);
1587
1588        doc
1589    }
1590
1591    /// Helper: canonicalize a document and return the result as a String.
1592    unsafe fn canonicalize_doc(doc: *mut _xmlDoc, mode: C14nMode, with_comments: c_int) -> String {
1593        let mut result: *mut xmlChar = ptr::null_mut();
1594        let len = c14n_doc_dump_memory(
1595            doc,
1596            ptr::null_mut(),
1597            mode,
1598            ptr::null(),
1599            with_comments,
1600            &mut result as *mut *mut xmlChar,
1601        );
1602        assert!(len >= 0);
1603        assert!(!result.is_null());
1604
1605        let s = {
1606            let slice = core::slice::from_raw_parts(result, len as usize);
1607            String::from_utf8_lossy(slice).to_string()
1608        };
1609        xmlFreeImpl(result as *mut c_void);
1610        s
1611    }
1612
1613    // ── Basic document canonicalization ──
1614
1615    #[test]
1616    fn test_c14n_basic_document() {
1617        unsafe {
1618            let doc = create_simple_doc();
1619            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1620            assert!(
1621                result.contains("<root>"),
1622                "Result should contain <root>, got: {}",
1623                result
1624            );
1625            assert!(
1626                result.contains("<child"),
1627                "Result should contain <child>, got: {}",
1628                result
1629            );
1630            assert!(
1631                result.contains("attr=\"value\""),
1632                "Result should contain attr=\"value\", got: {}",
1633                result
1634            );
1635            assert!(
1636                result.contains("text"),
1637                "Result should contain text, got: {}",
1638                result
1639            );
1640            assert!(
1641                result.contains("</child>"),
1642                "Result should contain </child>, got: {}",
1643                result
1644            );
1645            assert!(
1646                result.contains("</root>"),
1647                "Result should contain </root>, got: {}",
1648                result
1649            );
1650            tree::free_doc(doc);
1651        }
1652    }
1653
1654    #[test]
1655    fn test_c14n_basic_empty_element() {
1656        unsafe {
1657            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1658            assert!(!doc.is_null());
1659            let root = tree::new_node(ptr::null_mut(), b"empty\0" as *const u8 as *const xmlChar);
1660            assert!(!root.is_null());
1661            tree::doc_set_root_element(doc, root);
1662
1663            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1664            // Empty element should be self-closing
1665            assert!(
1666                result.contains("<empty/>"),
1667                "Empty element should be self-closing, got: {}",
1668                result
1669            );
1670            tree::free_doc(doc);
1671        }
1672    }
1673
1674    // ── Namespace propagation ──
1675
1676    #[test]
1677    fn test_c14n_namespace_propagation() {
1678        unsafe {
1679            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1680            assert!(!doc.is_null());
1681
1682            let ns = tree::new_ns(
1683                ptr::null_mut(),
1684                b"http://example.com/ns\0" as *const u8 as *const xmlChar,
1685                b"ex\0" as *const u8 as *const xmlChar,
1686            );
1687            assert!(!ns.is_null());
1688
1689            let root = tree::new_node(ns, b"root\0" as *const u8 as *const xmlChar);
1690            assert!(!root.is_null());
1691            tree::doc_set_root_element(doc, root);
1692
1693            // Re-attach the namespace to the root node
1694            tree::new_ns(
1695                root,
1696                b"http://example.com/ns\0" as *const u8 as *const xmlChar,
1697                b"ex\0" as *const u8 as *const xmlChar,
1698            );
1699
1700            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1701            assert!(
1702                result.contains("xmlns:ex=\"http://example.com/ns\""),
1703                "Result should contain namespace declaration, got: {}",
1704                result
1705            );
1706            assert!(
1707                result.contains("<ex:root"),
1708                "Result should contain <ex:root, got: {}",
1709                result
1710            );
1711            tree::free_doc(doc);
1712        }
1713    }
1714
1715    // ── Attribute ordering ──
1716
1717    #[test]
1718    fn test_c14n_attribute_ordering() {
1719        unsafe {
1720            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1721            assert!(!doc.is_null());
1722
1723            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
1724            assert!(!root.is_null());
1725            tree::doc_set_root_element(doc, root);
1726
1727            // Set attributes in reverse alphabetical order
1728            tree::set_prop(
1729                root,
1730                b"zeta\0" as *const u8 as *const xmlChar,
1731                b"1\0" as *const u8 as *const xmlChar,
1732            );
1733            tree::set_prop(
1734                root,
1735                b"alpha\0" as *const u8 as *const xmlChar,
1736                b"2\0" as *const u8 as *const xmlChar,
1737            );
1738            tree::set_prop(
1739                root,
1740                b"beta\0" as *const u8 as *const xmlChar,
1741                b"3\0" as *const u8 as *const xmlChar,
1742            );
1743
1744            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1745
1746            // Find the position of each attribute in the output
1747            let alpha_pos = result.find("alpha=\"2\"");
1748            let beta_pos = result.find("beta=\"3\"");
1749            let zeta_pos = result.find("zeta=\"1\"");
1750
1751            assert!(alpha_pos.is_some(), "alpha attribute should be present");
1752            assert!(beta_pos.is_some(), "beta attribute should be present");
1753            assert!(zeta_pos.is_some(), "zeta attribute should be present");
1754
1755            // alpha should come before beta, beta before zeta
1756            assert!(
1757                alpha_pos.unwrap() < beta_pos.unwrap(),
1758                "alpha should come before beta"
1759            );
1760            assert!(
1761                beta_pos.unwrap() < zeta_pos.unwrap(),
1762                "beta should come before zeta"
1763            );
1764
1765            tree::free_doc(doc);
1766        }
1767    }
1768
1769    // ── Character escaping ──
1770
1771    #[test]
1772    fn test_c14n_character_escaping_text() {
1773        unsafe {
1774            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1775            assert!(!doc.is_null());
1776
1777            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
1778            assert!(!root.is_null());
1779            tree::doc_set_root_element(doc, root);
1780
1781            // Text with special characters
1782            let text = tree::new_text(b"a < b & c > d\r\0" as *const u8 as *const xmlChar);
1783            assert!(!text.is_null());
1784            tree::add_child(root, text);
1785
1786            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1787            assert!(result.contains("&lt;"), "Should escape <, got: {}", result);
1788            assert!(result.contains("&amp;"), "Should escape &, got: {}", result);
1789            assert!(result.contains("&gt;"), "Should escape >, got: {}", result);
1790            assert!(
1791                result.contains("&#xD;"),
1792                "Should escape CR, got: {}",
1793                result
1794            );
1795
1796            tree::free_doc(doc);
1797        }
1798    }
1799
1800    #[test]
1801    fn test_c14n_character_escaping_attr() {
1802        unsafe {
1803            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1804            assert!(!doc.is_null());
1805
1806            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
1807            assert!(!root.is_null());
1808            tree::doc_set_root_element(doc, root);
1809
1810            // Attribute value with special characters including tab, newline, CR
1811            tree::set_prop(
1812                root,
1813                b"test\0" as *const u8 as *const xmlChar,
1814                b"a < b & c \" d\t\n\r\0" as *const u8 as *const xmlChar,
1815            );
1816
1817            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1818            assert!(
1819                result.contains("&lt;"),
1820                "Should escape < in attr, got: {}",
1821                result
1822            );
1823            assert!(
1824                result.contains("&amp;"),
1825                "Should escape & in attr, got: {}",
1826                result
1827            );
1828            assert!(
1829                result.contains("&quot;"),
1830                "Should escape \" in attr, got: {}",
1831                result
1832            );
1833            assert!(
1834                result.contains("&#x9;"),
1835                "Should escape tab in attr, got: {}",
1836                result
1837            );
1838            assert!(
1839                result.contains("&#xA;"),
1840                "Should escape newline in attr, got: {}",
1841                result
1842            );
1843            assert!(
1844                result.contains("&#xD;"),
1845                "Should escape CR in attr, got: {}",
1846                result
1847            );
1848
1849            tree::free_doc(doc);
1850        }
1851    }
1852
1853    // ── With comments ──
1854
1855    #[test]
1856    fn test_c14n_with_comments() {
1857        unsafe {
1858            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1859            assert!(!doc.is_null());
1860
1861            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
1862            assert!(!root.is_null());
1863            tree::doc_set_root_element(doc, root);
1864
1865            let comment = tree::new_comment(b" a comment \0" as *const u8 as *const xmlChar);
1866            assert!(!comment.is_null());
1867            tree::add_child(root, comment);
1868
1869            // Without comments
1870            let result_no_comments = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1871            assert!(
1872                !result_no_comments.contains("<!--"),
1873                "Without comments: should not contain comments, got: {}",
1874                result_no_comments
1875            );
1876
1877            // With comments
1878            let result_with_comments =
1879                canonicalize_doc(doc, C14nMode::XML_C14N_1_0_WITH_COMMENTS, 0);
1880            assert!(
1881                result_with_comments.contains("<!--"),
1882                "With comments: should contain comments, got: {}",
1883                result_with_comments
1884            );
1885
1886            tree::free_doc(doc);
1887        }
1888    }
1889
1890    // ── Exclusive vs inclusive ──
1891
1892    #[test]
1893    fn test_c14n_exclusive_vs_inclusive() {
1894        unsafe {
1895            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1896            assert!(!doc.is_null());
1897
1898            // Create root with a namespace
1899            tree::new_ns(
1900                ptr::null_mut(),
1901                b"http://example.com/ns1\0" as *const u8 as *const xmlChar,
1902                b"ns1\0" as *const u8 as *const xmlChar,
1903            );
1904            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
1905            assert!(!root.is_null());
1906            tree::doc_set_root_element(doc, root);
1907            tree::new_ns(
1908                root,
1909                b"http://example.com/ns1\0" as *const u8 as *const xmlChar,
1910                b"ns1\0" as *const u8 as *const xmlChar,
1911            );
1912
1913            // Child without any namespace usage
1914            let child = tree::new_node(ptr::null_mut(), b"child\0" as *const u8 as *const xmlChar);
1915            assert!(!child.is_null());
1916            tree::add_child(root, child);
1917
1918            // Inclusive C14N should include the ns1 namespace on child
1919            let result_inclusive = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1920            // The inclusive output will include namespace declarations from ancestors
1921            // on each element.
1922
1923            // Exclusive C14N should NOT include the ns1 namespace on child
1924            // since the child doesn't use it
1925            let _result_exclusive = canonicalize_doc(doc, C14nMode::XML_C14N_EXCLUSIVE_1_0, 0);
1926
1927            // In inclusive mode, the namespace should be visible
1928            // In exclusive mode, it should NOT be on the child (which doesn't use ns1)
1929            // The root element in exclusive mode still has ns1 declared on it
1930
1931            // Both should contain the ns1 namespace somewhere
1932            assert!(
1933                result_inclusive.contains("ns1"),
1934                "Inclusive should have ns1, got: {}",
1935                result_inclusive
1936            );
1937
1938            tree::free_doc(doc);
1939        }
1940    }
1941
1942    // ── XML declaration handling ──
1943
1944    #[test]
1945    fn test_c14n_no_xml_declaration() {
1946        unsafe {
1947            let doc = create_simple_doc();
1948            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1949            // C14N output should NOT include the XML declaration
1950            assert!(
1951                !result.contains("<?xml"),
1952                "C14N output should not contain XML declaration, got: {}",
1953                result
1954            );
1955            tree::free_doc(doc);
1956        }
1957    }
1958
1959    // ── Empty document ──
1960
1961    #[test]
1962    fn test_c14n_empty_document() {
1963        unsafe {
1964            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1965            assert!(!doc.is_null());
1966
1967            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
1968            assert!(
1969                result.is_empty(),
1970                "Empty document should produce empty output, got: {}",
1971                result
1972            );
1973
1974            tree::free_doc(doc);
1975        }
1976    }
1977
1978    // ── Edge cases ──
1979
1980    #[test]
1981    fn test_c14n_null_doc() {
1982        unsafe {
1983            let mut result: *mut xmlChar = ptr::null_mut();
1984            let len = c14n_doc_dump_memory(
1985                ptr::null_mut(),
1986                ptr::null_mut(),
1987                C14nMode::XML_C14N_1_0,
1988                ptr::null(),
1989                0,
1990                &mut result as *mut *mut xmlChar,
1991            );
1992            assert_eq!(len, -1, "Null doc should return -1");
1993        }
1994    }
1995
1996    #[test]
1997    fn test_c14n_text_node() {
1998        unsafe {
1999            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
2000            assert!(!doc.is_null());
2001
2002            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
2003            assert!(!root.is_null());
2004            tree::doc_set_root_element(doc, root);
2005
2006            // Text node with various content
2007            let text = tree::new_text(b"Hello World\0" as *const u8 as *const xmlChar);
2008            assert!(!text.is_null());
2009            tree::add_child(root, text);
2010
2011            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
2012            assert!(
2013                result.contains("Hello World"),
2014                "Should contain text content, got: {}",
2015                result
2016            );
2017
2018            tree::free_doc(doc);
2019        }
2020    }
2021
2022    #[test]
2023    fn test_c14n_cdata_section() {
2024        unsafe {
2025            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
2026            assert!(!doc.is_null());
2027
2028            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
2029            assert!(!root.is_null());
2030            tree::doc_set_root_element(doc, root);
2031
2032            // CDATA section content is represented as a text node in the tree
2033            // For C14N, CDATA sections are converted to text
2034            let cdata =
2035                tree::new_text(b"<greeting>Hello</greeting>\0" as *const u8 as *const xmlChar);
2036            assert!(!cdata.is_null());
2037            tree::add_child(root, cdata);
2038
2039            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
2040            assert!(
2041                result.contains("&lt;greeting&gt;"),
2042                "CDATA should be converted to escaped text, got: {}",
2043                result
2044            );
2045
2046            tree::free_doc(doc);
2047        }
2048    }
2049
2050    #[test]
2051    fn test_c14n_pi_node() {
2052        unsafe {
2053            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
2054            assert!(!doc.is_null());
2055
2056            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
2057            assert!(!root.is_null());
2058            tree::doc_set_root_element(doc, root);
2059
2060            let pi = tree::new_pi(
2061                b"xml-model\0" as *const u8 as *const xmlChar,
2062                b"href=\"schema.xsd\"\0" as *const u8 as *const xmlChar,
2063            );
2064            assert!(!pi.is_null());
2065            tree::add_child(root, pi);
2066
2067            let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
2068            assert!(result.contains("<?"), "Should contain PI, got: {}", result);
2069
2070            tree::free_doc(doc);
2071        }
2072    }
2073
2074    #[test]
2075    fn test_c14n_with_comments_flag() {
2076        unsafe {
2077            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
2078            assert!(!doc.is_null());
2079
2080            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
2081            assert!(!root.is_null());
2082            tree::doc_set_root_element(doc, root);
2083
2084            let comment = tree::new_comment(b"test\0" as *const u8 as *const xmlChar);
2085            assert!(!comment.is_null());
2086            tree::add_child(root, comment);
2087
2088            // Using the with_comments flag with the base mode
2089            let mut result: *mut xmlChar = ptr::null_mut();
2090            let len = c14n_doc_dump_memory(
2091                doc,
2092                ptr::null_mut(),
2093                C14nMode::XML_C14N_1_0,
2094                ptr::null(),
2095                1, // with_comments = true
2096                &mut result as *mut *mut xmlChar,
2097            );
2098            assert!(len >= 0);
2099            let s = {
2100                let slice = core::slice::from_raw_parts(result, len as usize);
2101                String::from_utf8_lossy(slice).to_string()
2102            };
2103            xmlFreeImpl(result as *mut c_void);
2104
2105            assert!(
2106                s.contains("<!--"),
2107                "With comments flag should include comments, got: {}",
2108                s
2109            );
2110
2111            tree::free_doc(doc);
2112        }
2113    }
2114
2115    #[test]
2116    fn test_c14n_mode_enum_values() {
2117        // Verify mode enum values match upstream constants
2118        assert_eq!(C14nMode::XML_C14N_1_0 as c_int, 0);
2119        assert_eq!(C14nMode::XML_C14N_EXCLUSIVE_1_0 as c_int, 1);
2120        assert_eq!(C14nMode::XML_C14N_1_1 as c_int, 2);
2121        assert_eq!(C14nMode::XML_C14N_1_0_WITH_COMMENTS as c_int, 3);
2122        assert_eq!(C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS as c_int, 4);
2123        assert_eq!(C14nMode::XML_C14N_1_1_WITH_COMMENTS as c_int, 5);
2124    }
2125
2126    #[test]
2127    fn test_c14n_with_comments_property() {
2128        assert!(C14nMode::XML_C14N_1_0_WITH_COMMENTS.with_comments());
2129        assert!(C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS.with_comments());
2130        assert!(C14nMode::XML_C14N_1_1_WITH_COMMENTS.with_comments());
2131        assert!(!C14nMode::XML_C14N_1_0.with_comments());
2132        assert!(!C14nMode::XML_C14N_EXCLUSIVE_1_0.with_comments());
2133        assert!(!C14nMode::XML_C14N_1_1.with_comments());
2134    }
2135
2136    #[test]
2137    fn test_c14n_is_exclusive_property() {
2138        assert!(C14nMode::XML_C14N_EXCLUSIVE_1_0.is_exclusive());
2139        assert!(C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS.is_exclusive());
2140        assert!(!C14nMode::XML_C14N_1_0.is_exclusive());
2141        assert!(!C14nMode::XML_C14N_1_0_WITH_COMMENTS.is_exclusive());
2142        assert!(!C14nMode::XML_C14N_1_1.is_exclusive());
2143    }
2144
2145    #[test]
2146    fn test_c14n_escape_text_cr() {
2147        unsafe {
2148            let buf = io::buf_create(-1);
2149            assert!(!buf.is_null());
2150
2151            let text = b"line1\r\nline2\r\0" as *const u8 as *const xmlChar;
2152            c14n_escape_text(buf, text, 13);
2153
2154            let content = io::buf_content(buf);
2155            let len = io::buf_length(buf);
2156            let s = {
2157                let slice = core::slice::from_raw_parts(content, len as usize);
2158                String::from_utf8_lossy(slice).to_string()
2159            };
2160            assert!(
2161                s.contains("&#xD;"),
2162                "CR should be escaped as &#xD;, got: {}",
2163                s
2164            );
2165            assert!(s.contains("\n"), "LF should remain as-is, got: {}", s);
2166
2167            io::buf_free(buf);
2168        }
2169    }
2170
2171    #[test]
2172    fn test_c14n_escape_attr_tab_nl_cr() {
2173        unsafe {
2174            let buf = io::buf_create(-1);
2175            assert!(!buf.is_null());
2176
2177            let text = b"a\tb\nc\rd\0" as *const u8 as *const xmlChar;
2178            c14n_escape_attr(buf, text);
2179
2180            let content = io::buf_content(buf);
2181            let len = io::buf_length(buf);
2182            let s = {
2183                let slice = core::slice::from_raw_parts(content, len as usize);
2184                String::from_utf8_lossy(slice).to_string()
2185            };
2186            assert!(
2187                s.contains("&#x9;"),
2188                "Tab should be escaped as &#x9;, got: {}",
2189                s
2190            );
2191            assert!(
2192                s.contains("&#xA;"),
2193                "NL should be escaped as &#xA;, got: {}",
2194                s
2195            );
2196            assert!(
2197                s.contains("&#xD;"),
2198                "CR should be escaped as &#xD;, got: {}",
2199                s
2200            );
2201
2202            io::buf_free(buf);
2203        }
2204    }
2205
2206    #[test]
2207    fn test_c14n_parse_inclusive_prefixes() {
2208        // Test NULL input
2209        assert!(parse_inclusive_prefixes(ptr::null()).is_none());
2210
2211        // Test empty string
2212        let empty = b"\0" as *const u8 as *const xmlChar;
2213        assert!(parse_inclusive_prefixes(empty).is_none());
2214
2215        // Test single prefix
2216        let single = b"foo\0" as *const u8 as *const xmlChar;
2217        let result = parse_inclusive_prefixes(single);
2218        assert!(result.is_some());
2219        assert!(result.unwrap().contains("foo"));
2220
2221        // Test multiple prefixes
2222        let multi = b"foo,bar,baz\0" as *const u8 as *const xmlChar;
2223        let result = parse_inclusive_prefixes(multi);
2224        assert!(result.is_some());
2225        let set = result.unwrap();
2226        assert!(set.contains("foo"));
2227        assert!(set.contains("bar"));
2228        assert!(set.contains("baz"));
2229        assert_eq!(set.len(), 3);
2230    }
2231
2232    #[test]
2233    fn test_c14n_document_order() {
2234        unsafe {
2235            let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
2236            assert!(!doc.is_null());
2237
2238            let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
2239            assert!(!root.is_null());
2240            tree::doc_set_root_element(doc, root);
2241
2242            let child1 =
2243                tree::new_node(ptr::null_mut(), b"child1\0" as *const u8 as *const xmlChar);
2244            assert!(!child1.is_null());
2245            tree::add_child(root, child1);
2246
2247            let child2 =
2248                tree::new_node(ptr::null_mut(), b"child2\0" as *const u8 as *const xmlChar);
2249            assert!(!child2.is_null());
2250            tree::add_child(root, child2);
2251
2252            // child1 should come before child2
2253            assert_eq!(
2254                cmp_document_order(child1, child2),
2255                std::cmp::Ordering::Less,
2256                "child1 should be before child2"
2257            );
2258            assert_eq!(
2259                cmp_document_order(child2, child1),
2260                std::cmp::Ordering::Greater,
2261                "child2 should be after child1"
2262            );
2263            assert_eq!(
2264                cmp_document_order(child1, child1),
2265                std::cmp::Ordering::Equal,
2266                "Same node should be equal"
2267            );
2268
2269            tree::free_doc(doc);
2270        }
2271    }
2272
2273    #[test]
2274    fn test_c14n_escape_text_gt() {
2275        unsafe {
2276            let buf = io::buf_create(-1);
2277            assert!(!buf.is_null());
2278
2279            let text = b"a > b\0" as *const u8 as *const xmlChar;
2280            c14n_escape_text(buf, text, 5);
2281
2282            let content = io::buf_content(buf);
2283            let len = io::buf_length(buf);
2284            let s = {
2285                let slice = core::slice::from_raw_parts(content, len as usize);
2286                String::from_utf8_lossy(slice).to_string()
2287            };
2288            assert!(
2289                s.contains("&gt;"),
2290                "> should be escaped as &gt;, got: {}",
2291                s
2292            );
2293
2294            io::buf_free(buf);
2295        }
2296    }
2297
2298    #[test]
2299    fn test_c14n_escape_text_cdata_end() {
2300        unsafe {
2301            let buf = io::buf_create(-1);
2302            assert!(!buf.is_null());
2303
2304            let text = b"a]]>b\0" as *const u8 as *const xmlChar;
2305            c14n_escape_text(buf, text, 5);
2306
2307            let content = io::buf_content(buf);
2308            let len = io::buf_length(buf);
2309            let s = {
2310                let slice = core::slice::from_raw_parts(content, len as usize);
2311                String::from_utf8_lossy(slice).to_string()
2312            };
2313            // The `]]>` sequence should become `]]&gt;`
2314            assert!(
2315                s.contains("]]&gt;"),
2316                "]]> should be escaped as ]]&gt;, got: {}",
2317                s
2318            );
2319
2320            io::buf_free(buf);
2321        }
2322    }
2323
2324    #[test]
2325    fn test_c14n_execute_callback() {
2326        unsafe {
2327            let doc = create_simple_doc();
2328
2329            // Use a heap-allocated Vec passed through the callback context
2330            let output_vec = Box::into_raw(Box::new(Vec::<u8>::new()));
2331
2332            unsafe extern "C" fn test_callback(
2333                ctx: *mut c_void,
2334                data: *const c_char,
2335                len: c_int,
2336            ) -> c_int {
2337                let slice = unsafe { core::slice::from_raw_parts(data as *const u8, len as usize) };
2338                let output = unsafe { &mut *(ctx as *mut Vec<u8>) };
2339                output.extend_from_slice(slice);
2340                len
2341            }
2342
2343            let ret = c14n_execute(
2344                doc,
2345                C14nMode::XML_C14N_1_0,
2346                ptr::null(),
2347                0,
2348                Some(
2349                    test_callback
2350                        as unsafe extern "C" fn(*mut c_void, *const c_char, c_int) -> c_int,
2351                ),
2352                output_vec as *mut c_void,
2353            );
2354
2355            assert!(ret >= 0, "c14n_execute should succeed");
2356            let output = Box::from_raw(output_vec);
2357            let output_str = String::from_utf8_lossy(&output);
2358            assert!(
2359                output_str.contains("<root>"),
2360                "Callback output should contain <root>, got: {}",
2361                output_str
2362            );
2363
2364            tree::free_doc(doc);
2365        }
2366    }
2367
2368    #[test]
2369    fn test_c14n_save_to_output_buffer() {
2370        unsafe {
2371            let doc = create_simple_doc();
2372
2373            // Create an output buffer
2374            let buf = io::buf_create(-1);
2375            assert!(!buf.is_null());
2376
2377            let output = io::output_buffer_create_buffer(buf, ptr::null_mut());
2378            assert!(!output.is_null());
2379
2380            let ret = c14n_doc_save_to(
2381                doc,
2382                ptr::null_mut(),
2383                C14nMode::XML_C14N_1_0,
2384                ptr::null(),
2385                0,
2386                output,
2387            );
2388
2389            assert!(ret >= 0, "c14n_doc_save_to should succeed");
2390
2391            let content = io::buf_content(buf);
2392            let len = io::buf_length(buf);
2393            let s = {
2394                let slice = core::slice::from_raw_parts(content, len as usize);
2395                String::from_utf8_lossy(slice).to_string()
2396            };
2397            assert!(
2398                s.contains("<root>"),
2399                "Output buffer should contain <root>, got: {}",
2400                s
2401            );
2402
2403            io::output_buffer_close(output);
2404            io::buf_free(buf);
2405            tree::free_doc(doc);
2406        }
2407    }
2408
2409    #[test]
2410    fn test_c14n_c_abi_doc_dump_memory() {
2411        unsafe {
2412            let doc = create_simple_doc();
2413
2414            let mut result: *mut xmlChar = ptr::null_mut();
2415            let len = xmlC14NDocDumpMemory(
2416                doc,
2417                ptr::null_mut(),
2418                0, // XML_C14N_1_0
2419                ptr::null_mut(),
2420                0, // no comments
2421                &mut result as *mut *mut xmlChar,
2422            );
2423
2424            assert!(len >= 0, "xmlC14NDocDumpMemory should succeed");
2425            assert!(!result.is_null());
2426
2427            let s = {
2428                let slice = core::slice::from_raw_parts(result, len as usize);
2429                String::from_utf8_lossy(slice).to_string()
2430            };
2431            assert!(
2432                s.contains("<root>"),
2433                "C ABI export should produce canonical output, got: {}",
2434                s
2435            );
2436
2437            xmlFreeImpl(result as *mut c_void);
2438            tree::free_doc(doc);
2439        }
2440    }
2441
2442    #[test]
2443    fn test_c14n_c_abi_execute() {
2444        unsafe {
2445            let doc = create_simple_doc();
2446
2447            let output_vec = Box::into_raw(Box::new(Vec::<u8>::new()));
2448
2449            unsafe extern "C" fn test_callback(
2450                ctx: *mut c_void,
2451                data: *const c_char,
2452                len: c_int,
2453            ) -> c_int {
2454                let slice = unsafe { core::slice::from_raw_parts(data as *const u8, len as usize) };
2455                let output = unsafe { &mut *(ctx as *mut Vec<u8>) };
2456                output.extend_from_slice(slice);
2457                len
2458            }
2459
2460            let ret = xmlC14NExecute(
2461                doc,
2462                0, // XML_C14N_1_0
2463                ptr::null_mut(),
2464                0,
2465                Some(
2466                    test_callback
2467                        as unsafe extern "C" fn(*mut c_void, *const c_char, c_int) -> c_int,
2468                ),
2469                output_vec as *mut c_void,
2470            );
2471
2472            assert!(ret >= 0, "xmlC14NExecute should succeed");
2473            let output = Box::from_raw(output_vec);
2474            let output_str = String::from_utf8_lossy(&output);
2475            assert!(
2476                output_str.contains("<root>"),
2477                "C ABI execute should produce canonical output, got: {}",
2478                output_str
2479            );
2480
2481            tree::free_doc(doc);
2482        }
2483    }
2484
2485    #[test]
2486    fn test_c14n_c_abi_save_to() {
2487        unsafe {
2488            let doc = create_simple_doc();
2489
2490            let buf = io::buf_create(-1);
2491            assert!(!buf.is_null());
2492
2493            let output = io::output_buffer_create_buffer(buf, ptr::null_mut());
2494            assert!(!output.is_null());
2495
2496            let ret = xmlC14NDocSaveTo(
2497                doc,
2498                ptr::null_mut(),
2499                0, // XML_C14N_1_0
2500                ptr::null_mut(),
2501                0,
2502                output,
2503            );
2504
2505            assert!(ret >= 0, "xmlC14NDocSaveTo should succeed");
2506
2507            let content = io::buf_content(buf);
2508            let len = io::buf_length(buf);
2509            let s = {
2510                let slice = core::slice::from_raw_parts(content, len as usize);
2511                String::from_utf8_lossy(slice).to_string()
2512            };
2513            assert!(
2514                s.contains("<root>"),
2515                "C ABI save_to should produce canonical output, got: {}",
2516                s
2517            );
2518
2519            io::output_buffer_close(output);
2520            io::buf_free(buf);
2521            tree::free_doc(doc);
2522        }
2523    }
2524}