#![allow(
missing_docs,
non_snake_case,
non_camel_case_types,
non_upper_case_globals,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_ptr_alignment,
clippy::missing_safety_doc,
clippy::too_many_lines,
clippy::type_complexity
)]
use core::ffi::c_void;
use core::ptr;
use std::collections::HashSet;
use std::os::raw::{c_char, c_int};
use crate::abi::callbacks::xmlC14NIsVisibleCallback;
use crate::abi::structs::*;
use crate::abi::types::xmlElementType::*;
use crate::abi::types::*;
use crate::xml::io;
use crate::xml::tree;
const XML_XML_PREFIX: &[xmlChar] = b"xml\0";
const XML_XML_NS_URI: &[xmlChar] = b"http://www.w3.org/XML/1998/namespace\0";
const _XMLNS_NS_URI: &[xmlChar] = b"http://www.w3.org/2000/xmlns/\0";
const _XMLNS_PREFIX: &[xmlChar] = b"xmlns\0";
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum C14nMode {
XML_C14N_1_0 = 0,
XML_C14N_EXCLUSIVE_1_0 = 1,
XML_C14N_1_1 = 2,
XML_C14N_1_0_WITH_COMMENTS = 3,
XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS = 4,
XML_C14N_1_1_WITH_COMMENTS = 5,
}
impl C14nMode {
const fn with_comments(self) -> bool {
matches!(
self,
C14nMode::XML_C14N_1_0_WITH_COMMENTS
| C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS
| C14nMode::XML_C14N_1_1_WITH_COMMENTS
)
}
const fn is_exclusive(self) -> bool {
matches!(
self,
C14nMode::XML_C14N_EXCLUSIVE_1_0 | C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS
)
}
const fn is_1_0(self) -> bool {
matches!(
self,
C14nMode::XML_C14N_1_0 | C14nMode::XML_C14N_1_0_WITH_COMMENTS
)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct NsEntry {
prefix: *const xmlChar,
href: *const xmlChar,
rendered: bool,
}
#[derive(Debug)]
pub struct C14nContext {
pub mode: C14nMode,
ns_stack: Vec<Vec<NsEntry>>,
#[allow(dead_code)]
inclusive_ns_prefixes: Option<HashSet<String>>,
#[allow(dead_code)]
doc: *mut _xmlDoc,
rendered_stack: Vec<Vec<(Vec<u8>, Vec<u8>)>>,
pos: u8,
parent_is_doc: bool,
visible_set: Option<HashSet<*mut c_void>>,
visibility_callback: Option<(xmlC14NIsVisibleCallback, *mut c_void)>,
pub invalid_node: bool,
}
impl C14nContext {
pub unsafe fn new(
doc: *mut _xmlDoc,
mode: C14nMode,
inclusive_ns_prefixes: Option<HashSet<String>>,
) -> Self {
Self::with_visible_set(doc, mode, inclusive_ns_prefixes, None)
}
pub unsafe fn with_visible_set(
doc: *mut _xmlDoc,
mode: C14nMode,
inclusive_ns_prefixes: Option<HashSet<String>>,
visible_set: Option<HashSet<*mut c_void>>,
) -> Self {
let mut ctx = C14nContext {
mode,
ns_stack: Vec::new(),
inclusive_ns_prefixes,
doc,
rendered_stack: Vec::new(),
pos: 0, parent_is_doc: true,
visible_set,
visibility_callback: None,
invalid_node: false,
};
let xml_prefix = XML_XML_PREFIX.as_ptr() as *const xmlChar;
let xml_href = XML_XML_NS_URI.as_ptr() as *const xmlChar;
ctx.ns_stack.push(vec![NsEntry {
prefix: xml_prefix,
href: xml_href,
rendered: false,
}]);
ctx
}
pub unsafe fn with_visibility_callback(
doc: *mut _xmlDoc,
mode: C14nMode,
inclusive_ns_prefixes: Option<HashSet<String>>,
callback: xmlC14NIsVisibleCallback,
user_data: *mut c_void,
) -> Self {
let mut ctx = C14nContext::with_visible_set(doc, mode, inclusive_ns_prefixes, None);
ctx.visibility_callback = Some((callback, user_data));
ctx
}
fn is_visible_node<T>(&self, node: *mut T) -> bool {
if let Some((cb, user_data)) = self.visibility_callback {
let parent = if node.is_null() {
ptr::null_mut()
} else {
unsafe { (*(node as *mut _xmlNode)).parent }
};
return unsafe { cb(user_data, node as *mut _xmlNode, parent) } != 0;
}
match &self.visible_set {
None => true,
Some(set) => !node.is_null() && set.contains(&(node as *mut c_void)),
}
}
const fn is_visible_ns(&self) -> bool {
self.visible_set.is_none()
}
fn push_rendered_scope(&mut self) {
self.rendered_stack.push(Vec::new());
}
fn pop_rendered_scope(&mut self) {
self.rendered_stack.pop();
}
fn already_rendered(&self, prefix: &[u8], href: &[u8], parent_frame_only: bool) -> bool {
let len = self.rendered_stack.len();
let mut idx = len;
let min = if parent_frame_only {
len.saturating_sub(2)
} else {
0
};
while idx > min {
idx -= 1;
for (p, h) in self.rendered_stack[idx].iter().rev() {
if p == prefix {
return h == href;
}
}
}
prefix.is_empty() && href.is_empty()
}
fn mark_rendered_pair(&mut self, prefix: &[u8], href: &[u8]) {
if let Some(top) = self.rendered_stack.last_mut() {
top.push((prefix.to_vec(), href.to_vec()));
}
}
#[allow(dead_code)]
fn push_scope(&mut self) {
let base = if let Some(top) = self.ns_stack.last() {
top.clone()
} else {
Vec::new()
};
self.ns_stack.push(base);
}
#[allow(dead_code)]
fn pop_scope(&mut self) {
self.ns_stack.pop();
}
#[allow(dead_code)]
fn add_namespace(&mut self, prefix: *const xmlChar, href: *const xmlChar) {
if let Some(top) = self.ns_stack.last_mut() {
if !top
.iter()
.any(|e| unsafe { crate::abi::exports_xml2::xmlStrEqual(e.prefix, prefix) != 0 })
{
top.push(NsEntry {
prefix,
href,
rendered: false,
});
} else {
if let Some(existing) = top.iter_mut().find(|e| unsafe {
crate::abi::exports_xml2::xmlStrEqual(e.prefix, prefix) != 0
}) {
existing.href = href;
existing.rendered = false;
}
}
}
}
#[allow(dead_code)]
fn is_prefix_in_scope(&self, prefix: *const xmlChar) -> bool {
self.ns_stack.iter().rev().any(|scope| {
scope
.iter()
.any(|e| unsafe { crate::abi::exports_xml2::xmlStrEqual(e.prefix, prefix) != 0 })
})
}
#[allow(dead_code)]
fn get_href_for_prefix(&self, prefix: *const xmlChar) -> *const xmlChar {
for scope in self.ns_stack.iter().rev() {
for entry in scope.iter() {
if unsafe { crate::abi::exports_xml2::xmlStrEqual(entry.prefix, prefix) != 0 } {
return entry.href;
}
}
}
ptr::null()
}
#[allow(dead_code)]
fn is_inclusive_prefix(&self, prefix: *const xmlChar) -> bool {
if let Some(ref set) = self.inclusive_ns_prefixes {
if prefix.is_null() {
return set.contains("");
}
let prefix_str = unsafe {
let c_str = core::ffi::CStr::from_ptr(prefix as *const c_char);
match c_str.to_str() {
Ok(s) => s.to_string(),
Err(_) => return false,
}
};
set.contains(&prefix_str)
} else {
false
}
}
#[allow(dead_code)]
fn mark_rendered(&mut self, prefix: *const xmlChar) {
for scope in self.ns_stack.iter_mut().rev() {
for entry in scope.iter_mut() {
if unsafe { crate::abi::exports_xml2::xmlStrEqual(entry.prefix, prefix) != 0 } {
entry.rendered = true;
return;
}
}
}
}
#[allow(dead_code)]
fn is_rendered(&self, prefix: *const xmlChar) -> bool {
for scope in self.ns_stack.iter().rev() {
for entry in scope.iter() {
if unsafe { crate::abi::exports_xml2::xmlStrEqual(entry.prefix, prefix) != 0 } {
return entry.rendered;
}
}
}
false
}
}
unsafe fn c14n_escape_text(buf: *mut _xmlBuffer, text: *const xmlChar, len: c_int) {
if buf.is_null() || text.is_null() || len <= 0 {
return;
}
let mut i: c_int = 0;
while i < len {
let ch = unsafe { *text.add(i as usize) };
if ch == b']'
&& i + 2 < len
&& unsafe { *text.add(i as usize + 1) == b']' }
&& unsafe { *text.add(i as usize + 2) == b'>' }
{
io::buf_add(buf, b"]]" as *const u8, 2); io::buf_add(buf, b">" as *const u8, 4);
i += 3;
continue;
}
match ch {
b'<' => {
io::buf_add(buf, b"<" as *const u8, 4);
}
b'>' => {
io::buf_add(buf, b">" as *const u8, 4);
}
b'&' => {
io::buf_add(buf, b"&" as *const u8, 5);
}
0x0D => {
io::buf_add(buf, b"
" as *const u8, 5);
}
_ => {
io::buf_add(buf, &ch as *const u8, 1);
}
}
i += 1;
}
}
unsafe fn c14n_escape_attr(buf: *mut _xmlBuffer, text: *const xmlChar) {
if buf.is_null() || text.is_null() {
return;
}
let len = tree::xml_strlen(text);
let mut i: c_int = 0;
while i < len {
let ch = unsafe { *text.add(i as usize) };
if ch == b']'
&& i + 2 < len
&& unsafe { *text.add(i as usize + 1) == b']' }
&& unsafe { *text.add(i as usize + 2) == b'>' }
{
io::buf_add(buf, b"]]" as *const u8, 2); io::buf_add(buf, b">" as *const u8, 4);
i += 3;
continue;
}
match ch {
b'<' => {
io::buf_add(buf, b"<" as *const u8, 4);
}
b'&' => {
io::buf_add(buf, b"&" as *const u8, 5);
}
b'"' => {
io::buf_add(buf, b""" as *const u8, 6);
}
0x09 => {
io::buf_add(buf, b"	" as *const u8, 5);
}
0x0A => {
io::buf_add(buf, b"
" as *const u8, 5);
}
0x0D => {
io::buf_add(buf, b"
" as *const u8, 5);
}
_ => {
io::buf_add(buf, &ch as *const u8, 1);
}
}
i += 1;
}
}
unsafe fn c14n_escape_comment(buf: *mut _xmlBuffer, text: *const xmlChar) {
if buf.is_null() || text.is_null() {
return;
}
let len = tree::xml_strlen(text);
let mut i: c_int = 0;
while i < len {
let ch = unsafe { *text.add(i as usize) };
if ch == 0x0D {
io::buf_add(buf, b"
" as *const u8, 5);
} else {
io::buf_add(buf, &ch as *const u8, 1);
}
i += 1;
}
}
unsafe fn c14n_escape_pi(buf: *mut _xmlBuffer, text: *const xmlChar) {
if buf.is_null() || text.is_null() {
return;
}
let len = tree::xml_strlen(text);
let mut i: c_int = 0;
while i < len {
let ch = unsafe { *text.add(i as usize) };
if ch == 0x0D {
io::buf_add(buf, b"
" as *const u8, 5);
} else {
io::buf_add(buf, &ch as *const u8, 1);
}
i += 1;
}
}
#[derive(Debug, Clone)]
struct CollectedNs {
prefix: *const xmlChar,
href: *const xmlChar,
}
unsafe fn is_xml_ns_ref(ns: &_xmlNs) -> bool {
!ns.prefix.is_null()
&& !ns.href.is_null()
&& crate::abi::exports_xml2::xmlStrEqual(
ns.prefix,
XML_XML_PREFIX.as_ptr() as *const xmlChar,
) != 0
&& crate::abi::exports_xml2::xmlStrEqual(ns.href, XML_XML_NS_URI.as_ptr() as *const xmlChar)
!= 0
}
unsafe fn cstr_bytes(p: *const xmlChar) -> Vec<u8> {
if p.is_null() {
return Vec::new();
}
let mut l = 0usize;
while *p.add(l) != 0 {
l += 1;
}
core::slice::from_raw_parts(p, l).to_vec()
}
unsafe fn exc_push_ns(
ctx: &mut C14nContext,
collected: &mut Vec<CollectedNs>,
ns: *mut _xmlNs,
has_empty_ns: &mut bool,
parent_frame_only: bool,
visible: bool,
) {
if ns.is_null() {
return;
}
let ns_ref = unsafe { &*ns };
if is_xml_ns_ref(ns_ref) {
return;
}
if !ctx.is_visible_ns() {
return;
}
let prefix_bytes = unsafe { cstr_bytes(ns_ref.prefix) };
let href_bytes = unsafe { cstr_bytes(ns_ref.href) };
let already = ctx.already_rendered(&prefix_bytes, &href_bytes, parent_frame_only);
if visible {
ctx.mark_rendered_pair(&prefix_bytes, &href_bytes);
}
if !already {
collected.push(CollectedNs {
prefix: ns_ref.prefix,
href: ns_ref.href,
});
}
if ns_ref.prefix.is_null() {
*has_empty_ns = true;
}
}
unsafe fn c14n_collect_namespaces(
node: *mut _xmlNode,
ctx: &mut C14nContext,
visible: bool,
) -> Vec<CollectedNs> {
if node.is_null() {
return Vec::new();
}
let n = unsafe { &*node };
if n.type_ != XML_ELEMENT_NODE as c_int {
return Vec::new();
}
let mut collected: Vec<CollectedNs> = Vec::new();
let mut seen_prefixes: Vec<*const xmlChar> = Vec::new();
if ctx.mode.is_exclusive() {
let mut has_empty_ns = false;
let mut has_empty_ns_in_inclusive_list = false;
let mut has_visibly_utilized_empty_ns = false;
let inclusive_prefixes: Vec<String> = ctx
.inclusive_ns_prefixes
.clone()
.map(|s| s.into_iter().collect())
.unwrap_or_default();
for inc_prefix_str in &inclusive_prefixes {
let is_default = inc_prefix_str.is_empty() || inc_prefix_str == "#default";
if is_default {
has_empty_ns_in_inclusive_list = true;
}
let inc_prefix: *const xmlChar = if is_default {
ptr::null()
} else {
let c_str = format!("{}\0", inc_prefix_str);
c_str.as_ptr() as *const xmlChar
};
let ns = find_ns_declaration(node, inc_prefix);
exc_push_ns(
ctx,
&mut collected,
ns,
&mut has_empty_ns,
!is_default,
visible,
);
}
let node_ns = if n.ns.is_null() {
has_visibly_utilized_empty_ns = true;
find_ns_declaration(node, ptr::null())
} else {
n.ns
};
exc_push_ns(
ctx,
&mut collected,
node_ns,
&mut has_empty_ns,
false,
visible,
);
let mut attr = n.properties;
while !attr.is_null() {
let a = unsafe { &*attr };
if !a.ns.is_null() {
let ans = unsafe { &*a.ns };
if !is_xml_ns_ref(ans) && ctx.is_visible_ns() {
exc_push_ns(ctx, &mut collected, a.ns, &mut has_empty_ns, false, visible);
} else if ans.prefix.is_null() && !ans.href.is_null() && *ans.href == 0 {
has_visibly_utilized_empty_ns = true;
}
}
attr = a.next;
}
if visible
&& !has_empty_ns
&& (has_visibly_utilized_empty_ns || has_empty_ns_in_inclusive_list)
{
let empty_prefix: Vec<u8> = Vec::new();
let empty_href: Vec<u8> = Vec::new();
if !ctx.already_rendered(&empty_prefix, &empty_href, false) {
collected.push(CollectedNs {
prefix: ptr::null(),
href: ptr::null(),
});
}
}
} else {
let mut has_empty_ns = false;
let mut cur: *mut _xmlNode = node;
while !cur.is_null() {
let cur_node = unsafe { &*cur };
let mut ns_def = cur_node.nsDef;
while !ns_def.is_null() {
let ns = unsafe { &*ns_def };
let ns_prefix = ns.prefix;
if !seen_prefixes.iter().any(|p| {
if ns_prefix.is_null() && p.is_null() {
return true;
}
if ns_prefix.is_null() || p.is_null() {
return false;
}
unsafe { crate::abi::exports_xml2::xmlStrEqual(*p, ns_prefix) != 0 }
}) {
seen_prefixes.push(ns_prefix);
if is_xml_ns_ref(ns) || !ctx.is_visible_ns() {
ns_def = ns.next;
continue;
}
let prefix_bytes = unsafe { cstr_bytes(ns_prefix) };
let href_bytes = unsafe { cstr_bytes(ns.href) };
let is_empty_ns = prefix_bytes.is_empty() && href_bytes.is_empty();
let already = ctx.already_rendered(&prefix_bytes, &href_bytes, !is_empty_ns);
if visible {
ctx.mark_rendered_pair(&prefix_bytes, &href_bytes);
}
if !already {
collected.push(CollectedNs {
prefix: ns_prefix,
href: ns.href,
});
}
if ns_prefix.is_null() {
has_empty_ns = true;
}
}
ns_def = ns.next;
}
cur = cur_node.parent;
}
if visible && !has_empty_ns {
let empty_prefix: Vec<u8> = Vec::new();
let empty_href: Vec<u8> = Vec::new();
if !ctx.already_rendered(&empty_prefix, &empty_href, false) {
collected.push(CollectedNs {
prefix: ptr::null(),
href: ptr::null(),
});
}
}
}
collected.sort_by(|a, b| {
let (ap, bp) = (a.prefix, b.prefix);
match (ap.is_null(), bp.is_null()) {
(true, true) => std::cmp::Ordering::Equal,
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
(false, false) => unsafe { crate::abi::exports_xml2::xmlStrcmp(ap, bp) }.cmp(&0),
}
});
collected
}
unsafe fn find_ns_declaration(node: *mut _xmlNode, prefix: *const xmlChar) -> *mut _xmlNs {
if node.is_null() {
return ptr::null_mut();
}
let mut cur: *mut _xmlNode = node;
while !cur.is_null() {
let cur_node = unsafe { &*cur };
let mut ns_def = cur_node.nsDef;
while !ns_def.is_null() {
let ns = unsafe { &*ns_def };
let match_found = if prefix.is_null() {
ns.prefix.is_null()
} else if ns.prefix.is_null() {
false
} else {
unsafe { crate::abi::exports_xml2::xmlStrEqual(ns.prefix, prefix) != 0 }
};
if match_found {
return ns_def;
}
ns_def = ns.next;
}
cur = cur_node.parent;
}
ptr::null_mut()
}
unsafe fn c14n_serialize_namespaces(buf: *mut _xmlBuffer, ns_list: &[CollectedNs]) {
if buf.is_null() || ns_list.is_empty() {
return;
}
for ns in ns_list {
io::buf_add(buf, b" xmlns" as *const u8, 6);
if !ns.prefix.is_null() {
io::buf_ccat(buf, b':');
io::buf_cat(buf, ns.prefix);
}
io::buf_add(buf, b"=\"" as *const u8, 2);
if !ns.href.is_null() {
c14n_escape_attr(buf, ns.href);
}
io::buf_ccat(buf, b'"');
}
}
unsafe fn compare_attrs(a: *const _xmlAttr, b: *const _xmlAttr) -> std::cmp::Ordering {
if a == b {
return std::cmp::Ordering::Equal;
}
if a.is_null() {
return std::cmp::Ordering::Less;
}
if b.is_null() {
return std::cmp::Ordering::Greater;
}
let attr_a = unsafe { &*a };
let attr_b = unsafe { &*b };
if attr_a.ns == attr_b.ns {
return unsafe { crate::abi::exports_xml2::xmlStrcmp(attr_a.name, attr_b.name) }.cmp(&0);
}
if attr_a.ns.is_null() {
return std::cmp::Ordering::Less;
}
if attr_b.ns.is_null() {
return std::cmp::Ordering::Greater;
}
if unsafe { (*(attr_a.ns)).prefix.is_null() } {
return std::cmp::Ordering::Less;
}
if unsafe { (*(attr_b.ns)).prefix.is_null() } {
return std::cmp::Ordering::Greater;
}
let ret =
unsafe { crate::abi::exports_xml2::xmlStrcmp((*(attr_a.ns)).href, (*(attr_b.ns)).href) };
if ret != 0 {
return ret.cmp(&0);
}
unsafe { crate::abi::exports_xml2::xmlStrcmp(attr_a.name, attr_b.name) }.cmp(&0)
}
unsafe fn is_xml_attr_ref(attr: &_xmlAttr) -> bool {
!attr.ns.is_null() && unsafe { is_xml_ns_ref(&*attr.ns) }
}
unsafe fn find_hidden_parent_attr(
ctx: &C14nContext,
cur: *mut _xmlNode,
name: &[u8],
) -> *mut _xmlAttr {
let mut cur = cur;
while !cur.is_null() && !ctx.is_visible_node(cur) {
let name_c = format!("{}\0", String::from_utf8_lossy(name));
let res = crate::xml::tree::has_ns_prop(
cur,
name_c.as_ptr() as *const xmlChar,
XML_XML_NS_URI.as_ptr() as *const xmlChar,
);
if !res.is_null() {
return res;
}
cur = unsafe { (*cur).parent };
}
ptr::null_mut()
}
unsafe fn fixup_base_attr(ctx: &C14nContext, base_attr: *mut _xmlAttr) -> Option<Vec<u8>> {
let mut res: Vec<u8> = Vec::new();
let ba = unsafe { &*base_attr };
if !ba.children.is_null() {
let child = unsafe { &*ba.children };
if !child.content.is_null() {
let mut l = 0usize;
while *child.content.add(l) != 0 {
l += 1;
}
res = core::slice::from_raw_parts(child.content, l).to_vec();
}
}
let mut cur = if ba.parent.is_null() {
ptr::null_mut()
} else {
unsafe { (*ba.parent).parent }
};
while !cur.is_null() && !ctx.is_visible_node(cur) {
let tmp_c = b"base\0";
let attr = crate::xml::tree::has_ns_prop(
cur,
tmp_c.as_ptr() as *const xmlChar,
XML_XML_NS_URI.as_ptr() as *const xmlChar,
);
if !attr.is_null() {
let mut tmp_str: Vec<u8> = Vec::new();
let a = unsafe { &*attr };
if !a.children.is_null() {
let child = unsafe { &*a.children };
if !child.content.is_null() {
let mut l = 0usize;
while *child.content.add(l) != 0 {
l += 1;
}
tmp_str = core::slice::from_raw_parts(child.content, l).to_vec();
}
}
let tl = tmp_str.len();
if tl > 1 && tmp_str[tl - 2] == b'.' {
tmp_str.push(b'/');
}
let uri_c = format!("{}\0", String::from_utf8_lossy(&res));
let base_c = format!("{}\0", String::from_utf8_lossy(&tmp_str));
let built = crate::abi::exports_uri::xmlBuildURI(
uri_c.as_ptr() as *const c_char,
base_c.as_ptr() as *const c_char,
);
if built.is_null() {
return None;
}
let mut l = 0usize;
while *built.add(l) != 0 {
l += 1;
}
res = core::slice::from_raw_parts(built, l).to_vec();
crate::abi::allocator::xmlFreeImpl(built as *mut c_void);
}
cur = unsafe { (*cur).parent };
}
if res.is_empty() {
None
} else {
Some(res)
}
}
struct AttrOut {
attr: *mut _xmlAttr,
base_value: Option<Vec<u8>>,
}
unsafe fn c14n_serialize_attributes(
node: *mut _xmlNode,
ctx: &mut C14nContext,
buf: *mut _xmlBuffer,
element_visible: bool,
) {
if node.is_null() || buf.is_null() {
return;
}
let n = unsafe { &*node };
if n.type_ != XML_ELEMENT_NODE as c_int {
return;
}
let mut list: Vec<AttrOut> = Vec::new();
let mut cur_attr = n.properties;
if ctx.mode.is_1_0() {
while !cur_attr.is_null() {
if ctx.is_visible_node(cur_attr) {
list.push(AttrOut {
attr: cur_attr,
base_value: None,
});
}
cur_attr = unsafe { (*cur_attr).next };
}
if element_visible && !n.parent.is_null() && !ctx.is_visible_node(n.parent) {
let mut tmp = n.parent;
while !tmp.is_null() && unsafe { (*tmp).type_ == XML_ELEMENT_NODE as c_int } {
let mut attr = unsafe { (*tmp).properties };
while !attr.is_null() {
let a = unsafe { &*attr };
if unsafe { is_xml_attr_ref(a) } {
let dup = list.iter().any(|o| unsafe {
compare_attrs(o.attr, attr) == std::cmp::Ordering::Equal
});
if !dup {
list.push(AttrOut {
attr,
base_value: None,
});
}
}
attr = unsafe { (*attr).next };
}
tmp = unsafe { (*tmp).parent };
}
}
} else if ctx.mode.is_exclusive() {
while !cur_attr.is_null() {
if ctx.is_visible_node(cur_attr) {
list.push(AttrOut {
attr: cur_attr,
base_value: None,
});
}
cur_attr = unsafe { (*cur_attr).next };
}
} else {
let mut xml_lang_attr: *mut _xmlAttr = ptr::null_mut();
let mut xml_space_attr: *mut _xmlAttr = ptr::null_mut();
let mut xml_base_attr: *mut _xmlAttr = ptr::null_mut();
while !cur_attr.is_null() {
let a = unsafe { &*cur_attr };
if !element_visible || !unsafe { is_xml_attr_ref(a) } {
if ctx.is_visible_node(cur_attr) {
list.push(AttrOut {
attr: cur_attr,
base_value: None,
});
}
} else {
let mut matched = false;
if xml_lang_attr.is_null()
&& !a.name.is_null()
&& unsafe {
crate::abi::exports_xml2::xmlStrEqual(
a.name,
c"lang".as_ptr() as *const xmlChar,
) != 0
}
{
xml_lang_attr = cur_attr;
matched = true;
}
if !matched
&& xml_space_attr.is_null()
&& !a.name.is_null()
&& unsafe {
crate::abi::exports_xml2::xmlStrEqual(
a.name,
c"space".as_ptr() as *const xmlChar,
) != 0
}
{
xml_space_attr = cur_attr;
matched = true;
}
if !matched
&& xml_base_attr.is_null()
&& !a.name.is_null()
&& unsafe {
crate::abi::exports_xml2::xmlStrEqual(
a.name,
c"base".as_ptr() as *const xmlChar,
) != 0
}
{
xml_base_attr = cur_attr;
matched = true;
}
if !matched && ctx.is_visible_node(cur_attr) {
list.push(AttrOut {
attr: cur_attr,
base_value: None,
});
}
}
cur_attr = unsafe { (*cur_attr).next };
}
if element_visible {
if xml_lang_attr.is_null() {
xml_lang_attr = find_hidden_parent_attr(ctx, n.parent, b"lang");
}
if !xml_lang_attr.is_null() {
list.push(AttrOut {
attr: xml_lang_attr,
base_value: None,
});
}
if xml_space_attr.is_null() {
xml_space_attr = find_hidden_parent_attr(ctx, n.parent, b"space");
}
if !xml_space_attr.is_null() {
list.push(AttrOut {
attr: xml_space_attr,
base_value: None,
});
}
if xml_base_attr.is_null() {
xml_base_attr = find_hidden_parent_attr(ctx, n.parent, b"base");
}
if !xml_base_attr.is_null() {
if let Some(resolved) = fixup_base_attr(ctx, xml_base_attr) {
list.push(AttrOut {
attr: xml_base_attr,
base_value: Some(resolved),
});
}
}
}
}
list.sort_by(|a, b| unsafe { compare_attrs(a.attr, b.attr) });
for out in &list {
let a = unsafe { &*out.attr };
io::buf_ccat(buf, b' ');
if !a.ns.is_null() {
let ans = unsafe { &*a.ns };
if !ans.prefix.is_null() {
io::buf_cat(buf, ans.prefix);
io::buf_ccat(buf, b':');
}
}
if !a.name.is_null() {
io::buf_cat(buf, a.name);
}
io::buf_add(buf, b"=\"" as *const u8, 2);
if let Some(ref value) = out.base_value {
if !value.is_empty() {
let value_c = format!("{}\0", String::from_utf8_lossy(value));
c14n_escape_attr(buf, value_c.as_ptr() as *const xmlChar);
}
} else if !a.children.is_null() {
let child = unsafe { &*a.children };
if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
c14n_escape_attr(buf, child.content);
}
}
io::buf_ccat(buf, b'"');
}
}
unsafe fn c14n_serialize_node(node: *mut _xmlNode, ctx: &mut C14nContext, buf: *mut _xmlBuffer) {
if node.is_null() || buf.is_null() {
return;
}
let n = unsafe { &*node };
match n.type_ {
t if t == XML_ELEMENT_NODE as c_int => {
c14n_serialize_element(node, ctx, buf);
}
t if t == XML_TEXT_NODE as c_int => {
if ctx.is_visible_node(node) && !n.content.is_null() {
c14n_escape_text(buf, n.content, tree::xml_strlen(n.content));
}
}
t if t == XML_CDATA_SECTION_NODE as c_int => {
if ctx.is_visible_node(node) && !n.content.is_null() {
c14n_escape_text(buf, n.content, tree::xml_strlen(n.content));
}
}
t if t == XML_COMMENT_NODE as c_int => {
if ctx.is_visible_node(node) && ctx.mode.with_comments() {
if ctx.pos == 2 {
io::buf_ccat(buf, b'\n');
}
io::buf_add(buf, b"<!--" as *const u8, 4);
if !n.content.is_null() {
c14n_escape_comment(buf, n.content);
}
io::buf_add(buf, b"-->" as *const u8, 3);
if ctx.pos == 0 {
io::buf_ccat(buf, b'\n');
}
}
}
t if t == XML_PI_NODE as c_int => {
if ctx.is_visible_node(node) {
if ctx.pos == 2 {
io::buf_ccat(buf, b'\n');
}
io::buf_add(buf, b"<?" as *const u8, 2);
if !n.name.is_null() {
io::buf_cat(buf, n.name);
}
if !n.content.is_null() && unsafe { *n.content != 0 } {
io::buf_ccat(buf, b' ');
c14n_escape_pi(buf, n.content);
}
io::buf_add(buf, b"?>" as *const u8, 2);
if ctx.pos == 0 {
io::buf_ccat(buf, b'\n');
}
}
}
t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
ctx.pos = 0; ctx.parent_is_doc = true;
let mut child = n.children;
while !child.is_null() {
c14n_serialize_node(child, ctx, buf);
child = unsafe { (*child).next };
}
}
t if t == XML_DTD_NODE as c_int || t == XML_DOCUMENT_TYPE_NODE as c_int => {
}
t if t == XML_ENTITY_REF_NODE as c_int => {
ctx.invalid_node = true;
}
_ => {
if !n.content.is_null() {
c14n_escape_text(buf, n.content, tree::xml_strlen(n.content));
}
}
}
}
unsafe fn doc_has_relative_ns(doc: *mut _xmlDoc) -> bool {
unsafe {
let mut stack: Vec<*mut _xmlNode> = Vec::new();
if !(*doc).children.is_null() {
stack.push((*doc).children);
}
while let Some(node) = stack.pop() {
let mut cur = node;
while !cur.is_null() {
let t = (*cur).type_;
if t == crate::abi::types::xmlElementType::XML_ELEMENT_NODE as c_int {
let mut ns_def = (*cur).nsDef;
while !ns_def.is_null() {
let href = (*ns_def).href;
if !href.is_null() && *href != 0 && !has_uri_scheme_bytes(href) {
return true;
}
ns_def = (*ns_def).next;
}
if !(*cur).children.is_null() {
stack.push((*cur).children);
}
}
cur = (*cur).next;
}
}
false
}
}
const unsafe fn has_uri_scheme_bytes(uri: *const xmlChar) -> bool {
unsafe {
let mut i: usize = 0;
while *uri.add(i) != 0 {
let c = *uri.add(i);
if c == b':' {
return i > 0;
}
if i == 0 {
if !c.is_ascii_alphabetic() {
return false;
}
} else if !(c.is_ascii_alphanumeric() || c == b'+' || c == b'-' || c == b'.') {
return false;
}
i += 1;
}
false
}
}
unsafe fn c14n_serialize_element(node: *mut _xmlNode, ctx: &mut C14nContext, buf: *mut _xmlBuffer) {
if node.is_null() || buf.is_null() {
return;
}
let n = unsafe { &*node };
let visible = ctx.is_visible_node(node);
let parent_is_doc = ctx.parent_is_doc;
if visible && parent_is_doc {
ctx.parent_is_doc = false;
ctx.pos = 1; }
ctx.push_scope();
ctx.push_rendered_scope();
let ns_list = c14n_collect_namespaces(node, ctx, visible);
if visible {
io::buf_ccat(buf, b'<');
if !n.ns.is_null() {
let ns = unsafe { &*n.ns };
if !ns.prefix.is_null() {
io::buf_cat(buf, ns.prefix);
io::buf_ccat(buf, b':');
}
}
if !n.name.is_null() {
io::buf_cat(buf, n.name);
}
c14n_serialize_namespaces(buf, &ns_list);
c14n_serialize_attributes(node, ctx, buf, visible);
if n.children.is_null() {
io::buf_ccat(buf, b'>');
io::buf_add(buf, b"</" as *const u8, 2);
if !n.ns.is_null() {
let ns = unsafe { &*n.ns };
if !ns.prefix.is_null() {
io::buf_cat(buf, ns.prefix);
io::buf_ccat(buf, b':');
}
}
if !n.name.is_null() {
io::buf_cat(buf, n.name);
}
io::buf_ccat(buf, b'>');
} else {
io::buf_ccat(buf, b'>');
let mut child = n.children;
while !child.is_null() {
c14n_serialize_node(child, ctx, buf);
child = unsafe { (*child).next };
}
io::buf_add(buf, b"</" as *const u8, 2);
if !n.ns.is_null() {
let ns = unsafe { &*n.ns };
if !ns.prefix.is_null() {
io::buf_cat(buf, ns.prefix);
io::buf_ccat(buf, b':');
}
}
if !n.name.is_null() {
io::buf_cat(buf, n.name);
}
io::buf_ccat(buf, b'>');
}
} else {
let mut child = n.children;
while !child.is_null() {
c14n_serialize_node(child, ctx, buf);
child = unsafe { (*child).next };
}
}
ctx.pop_rendered_scope();
ctx.pop_scope();
if visible && parent_is_doc {
ctx.parent_is_doc = true;
ctx.pos = 2; }
}
pub unsafe fn c14n_doc_dump_memory(
doc: *mut _xmlDoc,
nodes: *mut *mut _xmlNode,
mode: C14nMode,
inclusive_ns_prefixes: *const xmlChar,
with_comments: c_int,
result: *mut *mut xmlChar,
) -> c_int {
if doc.is_null() || result.is_null() {
return -1;
}
let effective_mode = if with_comments != 0 {
match mode {
C14nMode::XML_C14N_1_0 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
C14nMode::XML_C14N_EXCLUSIVE_1_0 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
C14nMode::XML_C14N_1_1 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
_ => mode,
}
} else {
mode
};
let inclusive_set = parse_inclusive_prefixes(inclusive_ns_prefixes);
let visible_set = build_visible_set(nodes);
let mut ctx = C14nContext::with_visible_set(doc, effective_mode, inclusive_set, visible_set);
if doc_has_relative_ns(doc) {
return -1;
}
let buf = io::buf_create(-1);
if buf.is_null() {
return -1;
}
c14n_walk_document(doc, &mut ctx, buf);
if ctx.invalid_node {
io::buf_free(buf);
return -1;
}
let content = io::buf_content(buf);
let len = io::buf_length(buf);
if content.is_null() || len < 0 {
io::buf_free(buf);
return -1;
}
let result_str = crate::abi::exports_xml2::xmlStrdup(content);
io::buf_free(buf);
if result_str.is_null() {
return -1;
}
unsafe {
*result = result_str;
}
len
}
pub unsafe fn c14n_execute(
doc: *mut _xmlDoc,
mode: C14nMode,
inclusive_ns_prefixes: *const xmlChar,
with_comments: c_int,
callback: Option<
unsafe extern "C" fn(ctx: *mut c_void, data: *const c_char, len: c_int) -> c_int,
>,
callback_data: *mut c_void,
) -> c_int {
if doc.is_null() || callback.is_none() {
return -1;
}
let callback = callback.unwrap();
let effective_mode = if with_comments != 0 {
match mode {
C14nMode::XML_C14N_1_0 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
C14nMode::XML_C14N_EXCLUSIVE_1_0 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
C14nMode::XML_C14N_1_1 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
_ => mode,
}
} else {
mode
};
let inclusive_set = parse_inclusive_prefixes(inclusive_ns_prefixes);
let mut ctx = C14nContext::new(doc, effective_mode, inclusive_set);
if doc_has_relative_ns(doc) {
return -1;
}
let buf = io::buf_create(-1);
if buf.is_null() {
return -1;
}
c14n_walk_document(doc, &mut ctx, buf);
if ctx.invalid_node {
io::buf_free(buf);
return -1;
}
let content = io::buf_content(buf);
let len = io::buf_length(buf);
if content.is_null() || len < 0 {
io::buf_free(buf);
return -1;
}
let ret = unsafe { callback(callback_data, content as *const c_char, len) };
io::buf_free(buf);
ret
}
pub unsafe fn c14n_execute_visibility(
doc: *mut _xmlDoc,
mode: C14nMode,
inclusive_ns_prefixes: *const xmlChar,
with_comments: c_int,
is_visible_callback: Option<crate::abi::callbacks::xmlC14NIsVisibleCallback>,
user_data: *mut c_void,
output: *mut _xmlOutputBuffer,
) -> c_int {
if doc.is_null() || output.is_null() {
return -1;
}
let effective_mode = if with_comments != 0 {
match mode {
C14nMode::XML_C14N_1_0 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
C14nMode::XML_C14N_EXCLUSIVE_1_0 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
C14nMode::XML_C14N_1_1 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
_ => mode,
}
} else {
mode
};
let inclusive_set = parse_inclusive_prefixes(inclusive_ns_prefixes);
let mut ctx = match is_visible_callback {
Some(cb) => {
C14nContext::with_visibility_callback(doc, effective_mode, inclusive_set, cb, user_data)
}
None => C14nContext::new(doc, effective_mode, inclusive_set),
};
if doc_has_relative_ns(doc) {
return -1;
}
let buf = io::buf_create(-1);
if buf.is_null() {
return -1;
}
c14n_walk_document(doc, &mut ctx, buf);
if ctx.invalid_node {
io::buf_free(buf);
return -1;
}
let content = io::buf_content(buf);
let len = io::buf_length(buf);
if content.is_null() || len < 0 {
io::buf_free(buf);
return -1;
}
let ret = crate::abi::exports_xml2::xmlOutputBufferWrite(output, len, content as *const c_char);
io::buf_free(buf);
ret
}
pub unsafe fn c14n_doc_save_to(
doc: *mut _xmlDoc,
nodes: *mut *mut _xmlNode,
mode: C14nMode,
inclusive_ns_prefixes: *const xmlChar,
with_comments: c_int,
output: *mut _xmlOutputBuffer,
) -> c_int {
if doc.is_null() || output.is_null() {
return -1;
}
let effective_mode = if with_comments != 0 {
match mode {
C14nMode::XML_C14N_1_0 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
C14nMode::XML_C14N_EXCLUSIVE_1_0 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
C14nMode::XML_C14N_1_1 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
_ => mode,
}
} else {
mode
};
let inclusive_set = parse_inclusive_prefixes(inclusive_ns_prefixes);
let visible_set = build_visible_set(nodes);
let mut ctx = C14nContext::with_visible_set(doc, effective_mode, inclusive_set, visible_set);
if doc_has_relative_ns(doc) {
return -1;
}
let buf = io::buf_create(-1);
if buf.is_null() {
return -1;
}
c14n_walk_document(doc, &mut ctx, buf);
if ctx.invalid_node {
io::buf_free(buf);
return -1;
}
let content = io::buf_content(buf);
let len = io::buf_length(buf);
if content.is_null() || len < 0 {
io::buf_free(buf);
return -1;
}
let written = io::output_buffer_write(output, len, content as *const c_char);
io::buf_free(buf);
let flush_ret = io::output_buffer_flush(output);
if flush_ret < 0 {
return written;
}
written
}
fn parse_inclusive_prefixes(input: *const xmlChar) -> Option<HashSet<String>> {
if input.is_null() {
return None;
}
let input_str = unsafe {
let c_str = core::ffi::CStr::from_ptr(input as *const c_char);
match c_str.to_str() {
Ok(s) => s.to_string(),
Err(_) => return None,
}
};
if input_str.is_empty() {
return None;
}
let mut set = HashSet::new();
for prefix in input_str.split(',') {
let trimmed = prefix.trim();
if !trimmed.is_empty() {
set.insert(trimmed.to_string());
}
}
if set.is_empty() {
None
} else {
Some(set)
}
}
#[cfg(test)]
unsafe fn cmp_document_order(a: *mut _xmlNode, b: *mut _xmlNode) -> std::cmp::Ordering {
if a == b {
return std::cmp::Ordering::Equal;
}
let mut ancestors_a: Vec<*mut _xmlNode> = Vec::new();
let mut cur = a;
while !cur.is_null() {
ancestors_a.push(cur);
cur = unsafe { (*cur).parent };
}
let mut ancestors_b: Vec<*mut _xmlNode> = Vec::new();
let mut cur = b;
while !cur.is_null() {
ancestors_b.push(cur);
cur = unsafe { (*cur).parent };
}
let mut i = ancestors_a.len();
let mut j = ancestors_b.len();
while i > 0 && j > 0 && ancestors_a[i - 1] == ancestors_b[j - 1] {
i -= 1;
j -= 1;
}
if i == 0 || j == 0 {
if i == 0 {
return std::cmp::Ordering::Less;
}
return std::cmp::Ordering::Greater;
}
let sibling_a = ancestors_a[i - 1];
let sibling_b = ancestors_b[j - 1];
let mut walk = sibling_a;
while !walk.is_null() {
if walk == sibling_b {
return std::cmp::Ordering::Less;
}
walk = unsafe { (*walk).next };
}
std::cmp::Ordering::Greater
}
#[no_mangle]
pub unsafe extern "C" fn xmlC14NDocDumpMemory(
doc: *mut _xmlDoc,
nodes: *mut _xmlNodeSet,
mode: c_int,
inclusive_ns_prefixes: *mut *mut xmlChar,
with_comments: c_int,
result: *mut *mut xmlChar,
) -> c_int {
let c14n_mode = match mode {
0 => C14nMode::XML_C14N_1_0,
1 => C14nMode::XML_C14N_EXCLUSIVE_1_0,
2 => C14nMode::XML_C14N_1_1,
3 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
4 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
5 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
_ => return -1,
};
let joined_prefixes = if !inclusive_ns_prefixes.is_null() {
let mut parts: Vec<*mut xmlChar> = Vec::new();
let mut i = 0;
loop {
let p = unsafe { *inclusive_ns_prefixes.add(i) };
if p.is_null() {
break;
}
parts.push(p);
i += 1;
}
if parts.is_empty() {
ptr::null()
} else {
let mut result_str = Vec::<u8>::new();
for (idx, &part) in parts.iter().enumerate() {
if idx > 0 {
result_str.push(b',');
}
let len = tree::xml_strlen(part);
let part_slice = unsafe { core::slice::from_raw_parts(part, len as usize) };
result_str.extend_from_slice(part_slice);
}
result_str.push(0); result_str.as_ptr() as *const xmlChar
}
} else {
ptr::null()
};
let nodes_array = node_set_to_array(nodes);
let nodes_ptr = if nodes_array.is_empty() {
ptr::null_mut()
} else {
nodes_array.as_ptr() as *mut *mut _xmlNode
};
let ret = unsafe {
c14n_doc_dump_memory(
doc,
nodes_ptr,
c14n_mode,
joined_prefixes,
with_comments,
result,
)
};
drop(nodes_array);
ret
}
unsafe fn build_visible_set(nodes: *mut *mut _xmlNode) -> Option<HashSet<*mut c_void>> {
if nodes.is_null() {
return None;
}
let mut set = HashSet::new();
let mut i = 0usize;
loop {
let n = unsafe { *nodes.add(i) };
if n.is_null() {
break;
}
set.insert(n as *mut c_void);
i += 1;
}
Some(set)
}
unsafe fn c14n_walk_document(doc: *mut _xmlDoc, ctx: &mut C14nContext, buf: *mut _xmlBuffer) {
let doc_node = doc as *mut _xmlNode;
let d = unsafe { &*doc_node };
let mut child = d.children;
while !child.is_null() {
c14n_serialize_node(child, ctx, buf);
child = unsafe { (*child).next };
}
}
unsafe fn node_set_to_array(nodes: *mut _xmlNodeSet) -> Vec<*mut _xmlNode> {
if nodes.is_null() {
return Vec::new();
}
let ns = unsafe { &*nodes };
let nr = if ns.nodeNr > 0 { ns.nodeNr as usize } else { 0 };
let mut vec: Vec<*mut _xmlNode> = Vec::with_capacity(nr + 1);
for i in 0..nr {
vec.push(unsafe { *ns.nodeTab.add(i) });
}
vec.push(ptr::null_mut());
vec
}
#[no_mangle]
pub unsafe extern "C" fn xmlC14NExecute(
doc: *mut _xmlDoc,
is_visible_callback: Option<crate::abi::callbacks::xmlC14NIsVisibleCallback>,
user_data: *mut c_void,
mode: c_int,
inclusive_ns_prefixes: *mut *mut xmlChar,
with_comments: c_int,
output: *mut _xmlOutputBuffer,
) -> c_int {
if doc.is_null() || output.is_null() {
return -1;
}
let c14n_mode = match mode {
0 => C14nMode::XML_C14N_1_0,
1 => C14nMode::XML_C14N_EXCLUSIVE_1_0,
2 => C14nMode::XML_C14N_1_1,
3 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
4 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
5 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
_ => return -1,
};
let joined_prefixes = if !inclusive_ns_prefixes.is_null() {
let mut parts: Vec<*mut xmlChar> = Vec::new();
let mut i = 0;
loop {
let p = unsafe { *inclusive_ns_prefixes.add(i) };
if p.is_null() {
break;
}
parts.push(p);
i += 1;
}
if parts.is_empty() {
ptr::null()
} else {
let mut result_str = Vec::<u8>::new();
for (idx, &part) in parts.iter().enumerate() {
if idx > 0 {
result_str.push(b',');
}
let len = tree::xml_strlen(part);
let part_slice = unsafe { core::slice::from_raw_parts(part, len as usize) };
result_str.extend_from_slice(part_slice);
}
result_str.push(0);
result_str.as_ptr() as *const xmlChar
}
} else {
ptr::null()
};
unsafe {
c14n_execute_visibility(
doc,
c14n_mode,
joined_prefixes,
with_comments,
is_visible_callback,
user_data,
output,
)
}
}
#[no_mangle]
pub unsafe extern "C" fn xmlC14NDocSaveTo(
doc: *mut _xmlDoc,
nodes: *mut _xmlNodeSet,
mode: c_int,
inclusive_ns_prefixes: *mut *mut xmlChar,
with_comments: c_int,
output: *mut _xmlOutputBuffer,
) -> c_int {
let c14n_mode = match mode {
0 => C14nMode::XML_C14N_1_0,
1 => C14nMode::XML_C14N_EXCLUSIVE_1_0,
2 => C14nMode::XML_C14N_1_1,
3 => C14nMode::XML_C14N_1_0_WITH_COMMENTS,
4 => C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS,
5 => C14nMode::XML_C14N_1_1_WITH_COMMENTS,
_ => return -1,
};
let joined_prefixes: Option<Vec<u8>> = if !inclusive_ns_prefixes.is_null() {
let mut parts: Vec<*mut xmlChar> = Vec::new();
let mut i = 0;
loop {
let p = unsafe { *inclusive_ns_prefixes.add(i) };
if p.is_null() {
break;
}
parts.push(p);
i += 1;
}
if parts.is_empty() {
None
} else {
let mut result_str = Vec::<u8>::new();
for (idx, &part) in parts.iter().enumerate() {
if idx > 0 {
result_str.push(b',');
}
let len = tree::xml_strlen(part);
let part_slice = unsafe { core::slice::from_raw_parts(part, len as usize) };
result_str.extend_from_slice(part_slice);
}
result_str.push(0);
Some(result_str)
}
} else {
None
};
let joined_ptr = joined_prefixes
.as_ref()
.map(|v| v.as_ptr() as *const xmlChar)
.unwrap_or(ptr::null());
let nodes_array = node_set_to_array(nodes);
let nodes_ptr = if nodes_array.is_empty() {
ptr::null_mut()
} else {
nodes_array.as_ptr() as *mut *mut _xmlNode
};
let ret =
unsafe { c14n_doc_save_to(doc, nodes_ptr, c14n_mode, joined_ptr, with_comments, output) };
drop(nodes_array);
ret
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::allocator::xmlFreeImpl;
use crate::xml::io;
use crate::xml::tree;
use core::ptr;
use std::os::raw::c_int;
unsafe fn create_simple_doc() -> *mut _xmlDoc {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let child = tree::new_node(ptr::null_mut(), b"child\0" as *const u8 as *const xmlChar);
assert!(!child.is_null());
tree::add_child(root, child);
tree::set_prop(
child,
b"attr\0" as *const u8 as *const xmlChar,
b"value\0" as *const u8 as *const xmlChar,
);
let text = tree::new_text(b"text\0" as *const u8 as *const xmlChar);
assert!(!text.is_null());
tree::add_child(child, text);
doc
}
unsafe fn canonicalize_doc(doc: *mut _xmlDoc, mode: C14nMode, with_comments: c_int) -> String {
let mut result: *mut xmlChar = ptr::null_mut();
let len = c14n_doc_dump_memory(
doc,
ptr::null_mut(),
mode,
ptr::null(),
with_comments,
&mut result as *mut *mut xmlChar,
);
assert!(len >= 0);
assert!(!result.is_null());
let s = {
let slice = core::slice::from_raw_parts(result, len as usize);
String::from_utf8_lossy(slice).to_string()
};
xmlFreeImpl(result as *mut c_void);
s
}
#[test]
fn test_c14n_entity_ref_node_fails_like_upstream() {
unsafe {
let xml = b"<?xml version='1.0'?><!DOCTYPE r [<!ELEMENT r (#PCDATA)><!ENTITY foo \"FOO\">]><r>a &foo; b</r>\0";
let doc = crate::abi::exports_xml2::xmlReadMemory(
xml.as_ptr() as *const c_char,
(xml.len() - 1) as c_int,
b"t.xml\0" as *const u8 as *const c_char,
ptr::null(),
0,
);
assert!(!doc.is_null(), "doc must parse (NOENT unset keeps the ref)");
let mut result: *mut xmlChar = ptr::null_mut();
let len = c14n_doc_dump_memory(
doc,
ptr::null_mut(),
C14nMode::XML_C14N_1_0,
ptr::null(),
0,
&mut result as *mut *mut xmlChar,
);
assert_eq!(len, -1, "entity-ref node must fail canonicalization");
assert!(result.is_null());
crate::abi::exports_xml2::xmlFreeDoc(doc);
}
}
#[test]
fn test_c14n_basic_document() {
unsafe {
let doc = create_simple_doc();
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
result.contains("<root>"),
"Result should contain <root>, got: {}",
result
);
assert!(
result.contains("<child"),
"Result should contain <child>, got: {}",
result
);
assert!(
result.contains("attr=\"value\""),
"Result should contain attr=\"value\", got: {}",
result
);
assert!(
result.contains("text"),
"Result should contain text, got: {}",
result
);
assert!(
result.contains("</child>"),
"Result should contain </child>, got: {}",
result
);
assert!(
result.contains("</root>"),
"Result should contain </root>, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_basic_empty_element() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"empty\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
result.contains("<empty></empty>"),
"Empty element should be expanded, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_namespace_propagation() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let ns = tree::new_ns(
ptr::null_mut(),
b"http://example.com/ns\0" as *const u8 as *const xmlChar,
b"ex\0" as *const u8 as *const xmlChar,
);
assert!(!ns.is_null());
let root = tree::new_node(ns, b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
tree::new_ns(
root,
b"http://example.com/ns\0" as *const u8 as *const xmlChar,
b"ex\0" as *const u8 as *const xmlChar,
);
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
result.contains("xmlns:ex=\"http://example.com/ns\""),
"Result should contain namespace declaration, got: {}",
result
);
assert!(
result.contains("<ex:root"),
"Result should contain <ex:root, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_attribute_ordering() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
tree::set_prop(
root,
b"zeta\0" as *const u8 as *const xmlChar,
b"1\0" as *const u8 as *const xmlChar,
);
tree::set_prop(
root,
b"alpha\0" as *const u8 as *const xmlChar,
b"2\0" as *const u8 as *const xmlChar,
);
tree::set_prop(
root,
b"beta\0" as *const u8 as *const xmlChar,
b"3\0" as *const u8 as *const xmlChar,
);
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
let alpha_pos = result.find("alpha=\"2\"");
let beta_pos = result.find("beta=\"3\"");
let zeta_pos = result.find("zeta=\"1\"");
assert!(alpha_pos.is_some(), "alpha attribute should be present");
assert!(beta_pos.is_some(), "beta attribute should be present");
assert!(zeta_pos.is_some(), "zeta attribute should be present");
assert!(
alpha_pos.unwrap() < beta_pos.unwrap(),
"alpha should come before beta"
);
assert!(
beta_pos.unwrap() < zeta_pos.unwrap(),
"beta should come before zeta"
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_character_escaping_text() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let text = tree::new_text(b"a < b & c > d\r\0" as *const u8 as *const xmlChar);
assert!(!text.is_null());
tree::add_child(root, text);
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(result.contains("<"), "Should escape <, got: {}", result);
assert!(result.contains("&"), "Should escape &, got: {}", result);
assert!(result.contains(">"), "Should escape >, got: {}", result);
assert!(
result.contains("
"),
"Should escape CR, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_character_escaping_attr() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
tree::set_prop(
root,
b"test\0" as *const u8 as *const xmlChar,
b"a < b & c \" d\t\n\r\0" as *const u8 as *const xmlChar,
);
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
result.contains("<"),
"Should escape < in attr, got: {}",
result
);
assert!(
result.contains("&"),
"Should escape & in attr, got: {}",
result
);
assert!(
result.contains("""),
"Should escape \" in attr, got: {}",
result
);
assert!(
result.contains("	"),
"Should escape tab in attr, got: {}",
result
);
assert!(
result.contains("
"),
"Should escape newline in attr, got: {}",
result
);
assert!(
result.contains("
"),
"Should escape CR in attr, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_with_comments() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let comment = tree::new_comment(b" a comment \0" as *const u8 as *const xmlChar);
assert!(!comment.is_null());
tree::add_child(root, comment);
let result_no_comments = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
!result_no_comments.contains("<!--"),
"Without comments: should not contain comments, got: {}",
result_no_comments
);
let result_with_comments =
canonicalize_doc(doc, C14nMode::XML_C14N_1_0_WITH_COMMENTS, 0);
assert!(
result_with_comments.contains("<!--"),
"With comments: should contain comments, got: {}",
result_with_comments
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_exclusive_vs_inclusive() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
tree::new_ns(
ptr::null_mut(),
b"http://example.com/ns1\0" as *const u8 as *const xmlChar,
b"ns1\0" as *const u8 as *const xmlChar,
);
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
tree::new_ns(
root,
b"http://example.com/ns1\0" as *const u8 as *const xmlChar,
b"ns1\0" as *const u8 as *const xmlChar,
);
let child = tree::new_node(ptr::null_mut(), b"child\0" as *const u8 as *const xmlChar);
assert!(!child.is_null());
tree::add_child(root, child);
let result_inclusive = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
let _result_exclusive = canonicalize_doc(doc, C14nMode::XML_C14N_EXCLUSIVE_1_0, 0);
assert!(
result_inclusive.contains("ns1"),
"Inclusive should have ns1, got: {}",
result_inclusive
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_no_xml_declaration() {
unsafe {
let doc = create_simple_doc();
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
!result.contains("<?xml"),
"C14N output should not contain XML declaration, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_empty_document() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
result.is_empty(),
"Empty document should produce empty output, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_null_doc() {
unsafe {
let mut result: *mut xmlChar = ptr::null_mut();
let len = c14n_doc_dump_memory(
ptr::null_mut(),
ptr::null_mut(),
C14nMode::XML_C14N_1_0,
ptr::null(),
0,
&mut result as *mut *mut xmlChar,
);
assert_eq!(len, -1, "Null doc should return -1");
}
}
#[test]
fn test_c14n_text_node() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let text = tree::new_text(b"Hello World\0" as *const u8 as *const xmlChar);
assert!(!text.is_null());
tree::add_child(root, text);
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
result.contains("Hello World"),
"Should contain text content, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_cdata_section() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let cdata =
tree::new_text(b"<greeting>Hello</greeting>\0" as *const u8 as *const xmlChar);
assert!(!cdata.is_null());
tree::add_child(root, cdata);
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(
result.contains("<greeting>"),
"CDATA should be converted to escaped text, got: {}",
result
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_pi_node() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let pi = tree::new_pi(
b"xml-model\0" as *const u8 as *const xmlChar,
b"href=\"schema.xsd\"\0" as *const u8 as *const xmlChar,
);
assert!(!pi.is_null());
tree::add_child(root, pi);
let result = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert!(result.contains("<?"), "Should contain PI, got: {}", result);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_with_comments_flag() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let comment = tree::new_comment(b"test\0" as *const u8 as *const xmlChar);
assert!(!comment.is_null());
tree::add_child(root, comment);
let mut result: *mut xmlChar = ptr::null_mut();
let len = c14n_doc_dump_memory(
doc,
ptr::null_mut(),
C14nMode::XML_C14N_1_0,
ptr::null(),
1, &mut result as *mut *mut xmlChar,
);
assert!(len >= 0);
let s = {
let slice = core::slice::from_raw_parts(result, len as usize);
String::from_utf8_lossy(slice).to_string()
};
xmlFreeImpl(result as *mut c_void);
assert!(
s.contains("<!--"),
"With comments flag should include comments, got: {}",
s
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_mode_enum_values() {
assert_eq!(C14nMode::XML_C14N_1_0 as c_int, 0);
assert_eq!(C14nMode::XML_C14N_EXCLUSIVE_1_0 as c_int, 1);
assert_eq!(C14nMode::XML_C14N_1_1 as c_int, 2);
assert_eq!(C14nMode::XML_C14N_1_0_WITH_COMMENTS as c_int, 3);
assert_eq!(C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS as c_int, 4);
assert_eq!(C14nMode::XML_C14N_1_1_WITH_COMMENTS as c_int, 5);
}
#[test]
fn test_c14n_with_comments_property() {
assert!(C14nMode::XML_C14N_1_0_WITH_COMMENTS.with_comments());
assert!(C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS.with_comments());
assert!(C14nMode::XML_C14N_1_1_WITH_COMMENTS.with_comments());
assert!(!C14nMode::XML_C14N_1_0.with_comments());
assert!(!C14nMode::XML_C14N_EXCLUSIVE_1_0.with_comments());
assert!(!C14nMode::XML_C14N_1_1.with_comments());
}
#[test]
fn test_c14n_is_exclusive_property() {
assert!(C14nMode::XML_C14N_EXCLUSIVE_1_0.is_exclusive());
assert!(C14nMode::XML_C14N_EXCLUSIVE_1_0_WITH_COMMENTS.is_exclusive());
assert!(!C14nMode::XML_C14N_1_0.is_exclusive());
assert!(!C14nMode::XML_C14N_1_0_WITH_COMMENTS.is_exclusive());
assert!(!C14nMode::XML_C14N_1_1.is_exclusive());
}
#[test]
fn test_c14n_escape_text_cr() {
unsafe {
let buf = io::buf_create(-1);
assert!(!buf.is_null());
let text = b"line1\r\nline2\r\0" as *const u8 as *const xmlChar;
c14n_escape_text(buf, text, 13);
let content = io::buf_content(buf);
let len = io::buf_length(buf);
let s = {
let slice = core::slice::from_raw_parts(content, len as usize);
String::from_utf8_lossy(slice).to_string()
};
assert!(
s.contains("
"),
"CR should be escaped as 
, got: {}",
s
);
assert!(s.contains("\n"), "LF should remain as-is, got: {}", s);
io::buf_free(buf);
}
}
#[test]
fn test_c14n_escape_attr_tab_nl_cr() {
unsafe {
let buf = io::buf_create(-1);
assert!(!buf.is_null());
let text = b"a\tb\nc\rd\0" as *const u8 as *const xmlChar;
c14n_escape_attr(buf, text);
let content = io::buf_content(buf);
let len = io::buf_length(buf);
let s = {
let slice = core::slice::from_raw_parts(content, len as usize);
String::from_utf8_lossy(slice).to_string()
};
assert!(
s.contains("	"),
"Tab should be escaped as 	, got: {}",
s
);
assert!(
s.contains("
"),
"NL should be escaped as 
, got: {}",
s
);
assert!(
s.contains("
"),
"CR should be escaped as 
, got: {}",
s
);
io::buf_free(buf);
}
}
#[test]
fn test_c14n_parse_inclusive_prefixes() {
assert!(parse_inclusive_prefixes(ptr::null()).is_none());
let empty = b"\0" as *const u8 as *const xmlChar;
assert!(parse_inclusive_prefixes(empty).is_none());
let single = b"foo\0" as *const u8 as *const xmlChar;
let result = parse_inclusive_prefixes(single);
assert!(result.is_some());
assert!(result.unwrap().contains("foo"));
let multi = b"foo,bar,baz\0" as *const u8 as *const xmlChar;
let result = parse_inclusive_prefixes(multi);
assert!(result.is_some());
let set = result.unwrap();
assert!(set.contains("foo"));
assert!(set.contains("bar"));
assert!(set.contains("baz"));
assert_eq!(set.len(), 3);
}
#[test]
fn test_c14n_document_order() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
assert!(!doc.is_null());
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
assert!(!root.is_null());
tree::doc_set_root_element(doc, root);
let child1 =
tree::new_node(ptr::null_mut(), b"child1\0" as *const u8 as *const xmlChar);
assert!(!child1.is_null());
tree::add_child(root, child1);
let child2 =
tree::new_node(ptr::null_mut(), b"child2\0" as *const u8 as *const xmlChar);
assert!(!child2.is_null());
tree::add_child(root, child2);
assert_eq!(
cmp_document_order(child1, child2),
std::cmp::Ordering::Less,
"child1 should be before child2"
);
assert_eq!(
cmp_document_order(child2, child1),
std::cmp::Ordering::Greater,
"child2 should be after child1"
);
assert_eq!(
cmp_document_order(child1, child1),
std::cmp::Ordering::Equal,
"Same node should be equal"
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_escape_text_gt() {
unsafe {
let buf = io::buf_create(-1);
assert!(!buf.is_null());
let text = b"a > b\0" as *const u8 as *const xmlChar;
c14n_escape_text(buf, text, 5);
let content = io::buf_content(buf);
let len = io::buf_length(buf);
let s = {
let slice = core::slice::from_raw_parts(content, len as usize);
String::from_utf8_lossy(slice).to_string()
};
assert!(
s.contains(">"),
"> should be escaped as >, got: {}",
s
);
io::buf_free(buf);
}
}
#[test]
fn test_c14n_escape_text_cdata_end() {
unsafe {
let buf = io::buf_create(-1);
assert!(!buf.is_null());
let text = b"a]]>b\0" as *const u8 as *const xmlChar;
c14n_escape_text(buf, text, 5);
let content = io::buf_content(buf);
let len = io::buf_length(buf);
let s = {
let slice = core::slice::from_raw_parts(content, len as usize);
String::from_utf8_lossy(slice).to_string()
};
assert!(
s.contains("]]>"),
"]]> should be escaped as ]]>, got: {}",
s
);
io::buf_free(buf);
}
}
#[test]
fn test_c14n_execute_callback() {
unsafe {
let doc = create_simple_doc();
let output_vec = Box::into_raw(Box::new(Vec::<u8>::new()));
unsafe extern "C" fn test_callback(
ctx: *mut c_void,
data: *const c_char,
len: c_int,
) -> c_int {
let slice = unsafe { core::slice::from_raw_parts(data as *const u8, len as usize) };
let output = unsafe { &mut *(ctx as *mut Vec<u8>) };
output.extend_from_slice(slice);
len
}
let ret = c14n_execute(
doc,
C14nMode::XML_C14N_1_0,
ptr::null(),
0,
Some(
test_callback
as unsafe extern "C" fn(*mut c_void, *const c_char, c_int) -> c_int,
),
output_vec as *mut c_void,
);
assert!(ret >= 0, "c14n_execute should succeed");
let output = Box::from_raw(output_vec);
let output_str = String::from_utf8_lossy(&output);
assert!(
output_str.contains("<root>"),
"Callback output should contain <root>, got: {}",
output_str
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_save_to_output_buffer() {
unsafe {
let doc = create_simple_doc();
let buf = io::buf_create(-1);
assert!(!buf.is_null());
let output = io::output_buffer_create_buffer(buf, ptr::null_mut());
assert!(!output.is_null());
let ret = c14n_doc_save_to(
doc,
ptr::null_mut(),
C14nMode::XML_C14N_1_0,
ptr::null(),
0,
output,
);
assert!(ret >= 0, "c14n_doc_save_to should succeed");
let content = io::buf_content(buf);
let len = io::buf_length(buf);
let s = {
let slice = core::slice::from_raw_parts(content, len as usize);
String::from_utf8_lossy(slice).to_string()
};
assert!(
s.contains("<root>"),
"Output buffer should contain <root>, got: {}",
s
);
io::output_buffer_close(output);
io::buf_free(buf);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_c_abi_doc_dump_memory() {
unsafe {
let doc = create_simple_doc();
let mut result: *mut xmlChar = ptr::null_mut();
let len = xmlC14NDocDumpMemory(
doc,
ptr::null_mut(),
0, ptr::null_mut(),
0, &mut result as *mut *mut xmlChar,
);
assert!(len >= 0, "xmlC14NDocDumpMemory should succeed");
assert!(!result.is_null());
let s = {
let slice = core::slice::from_raw_parts(result, len as usize);
String::from_utf8_lossy(slice).to_string()
};
assert!(
s.contains("<root>"),
"C ABI export should produce canonical output, got: {}",
s
);
xmlFreeImpl(result as *mut c_void);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_c_abi_execute() {
unsafe {
let doc = create_simple_doc();
let buf = io::buf_create(-1);
assert!(!buf.is_null());
let output = io::output_buffer_create_buffer(buf, ptr::null_mut());
assert!(!output.is_null());
let ret = xmlC14NExecute(
doc,
None, ptr::null_mut(), 0, ptr::null_mut(), 0, output,
);
assert!(ret >= 0, "xmlC14NExecute should succeed");
io::output_buffer_close(output);
let content = io::buf_content(buf);
let len = io::buf_length(buf);
let s = {
let slice = core::slice::from_raw_parts(content, len as usize);
String::from_utf8_lossy(slice).to_string()
};
assert!(
s.contains("<root>"),
"C ABI execute should produce canonical output, got: {}",
s
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_c_abi_save_to() {
unsafe {
let doc = create_simple_doc();
let buf = io::buf_create(-1);
assert!(!buf.is_null());
let output = io::output_buffer_create_buffer(buf, ptr::null_mut());
assert!(!output.is_null());
let ret = xmlC14NDocSaveTo(
doc,
ptr::null_mut(),
0, ptr::null_mut(),
0,
output,
);
assert!(ret >= 0, "xmlC14NDocSaveTo should succeed");
let content = io::buf_content(buf);
let len = io::buf_length(buf);
let s = {
let slice = core::slice::from_raw_parts(content, len as usize);
String::from_utf8_lossy(slice).to_string()
};
assert!(
s.contains("<root>"),
"C ABI save_to should produce canonical output, got: {}",
s
);
io::output_buffer_close(output);
io::buf_free(buf);
tree::free_doc(doc);
}
}
unsafe fn build_nested_ns_doc() -> *mut _xmlDoc {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let ns = tree::new_ns(
ptr::null_mut(),
b"http://u/p\0" as *const u8 as *const xmlChar,
b"p\0" as *const u8 as *const xmlChar,
);
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
tree::doc_set_root_element(doc, root);
tree::new_ns(
root,
b"http://u/p\0" as *const u8 as *const xmlChar,
b"p\0" as *const u8 as *const xmlChar,
);
let one = tree::new_node(ns, b"one\0" as *const u8 as *const xmlChar);
tree::add_child(root, one);
let two = tree::new_node(ns, b"two\0" as *const u8 as *const xmlChar);
tree::add_child(one, two);
tree::set_prop(
two,
b"a\0" as *const u8 as *const xmlChar,
b"1\0" as *const u8 as *const xmlChar,
);
let attr = (*two).properties;
(*attr).ns = ns;
doc
}
#[test]
fn test_c14n_exclusive_skips_ancestor_rendered_ns() {
unsafe {
let doc = build_nested_ns_doc();
let out = canonicalize_doc(doc, C14nMode::XML_C14N_EXCLUSIVE_1_0, 0);
assert_eq!(
out,
"<root><p:one xmlns:p=\"http://u/p\"><p:two p:a=\"1\"></p:two></p:one></root>"
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_namespace_sorting() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
tree::doc_set_root_element(doc, root);
tree::new_ns(
root,
b"http://u/z\0" as *const u8 as *const xmlChar,
b"z\0" as *const u8 as *const xmlChar,
);
tree::new_ns(
root,
b"http://u/a\0" as *const u8 as *const xmlChar,
b"a\0" as *const u8 as *const xmlChar,
);
let out = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert_eq!(
out,
"<root xmlns:a=\"http://u/a\" xmlns:z=\"http://u/z\"></root>"
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_xml_ns_never_rendered() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
tree::doc_set_root_element(doc, root);
let xml_ns = tree::new_ns(
ptr::null_mut(),
b"http://www.w3.org/XML/1998/namespace\0" as *const u8 as *const xmlChar,
b"xml\0" as *const u8 as *const xmlChar,
);
(*doc).oldNs = xml_ns;
let out = canonicalize_doc(doc, C14nMode::XML_C14N_EXCLUSIVE_1_0, 0);
assert_eq!(out, "<root></root>");
assert!(!out.contains("xmlns:xml"));
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_empty_default_undeclaration() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
tree::doc_set_root_element(doc, root);
let d_ns = tree::new_ns(
ptr::null_mut(),
b"http://u/d\0" as *const u8 as *const xmlChar,
ptr::null(),
);
tree::new_ns(
root,
b"http://u/d\0" as *const u8 as *const xmlChar,
ptr::null(),
);
let a = tree::new_node(d_ns, b"a\0" as *const u8 as *const xmlChar);
tree::add_child(root, a);
tree::new_ns(a, b"\0" as *const u8 as *const xmlChar, ptr::null());
(*a).ns = ptr::null_mut();
let out = canonicalize_doc(doc, C14nMode::XML_C14N_EXCLUSIVE_1_0, 0);
assert_eq!(out, "<root xmlns=\"http://u/d\"><a xmlns=\"\"></a></root>");
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_relative_ns_rejected_exclusive() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
tree::doc_set_root_element(doc, root);
tree::new_ns(
root,
b"u\0" as *const u8 as *const xmlChar,
b"p\0" as *const u8 as *const xmlChar,
);
let mut result: *mut xmlChar = ptr::null_mut();
let len = c14n_doc_dump_memory(
doc,
ptr::null_mut(),
C14nMode::XML_C14N_EXCLUSIVE_1_0,
ptr::null(),
0,
&mut result as *mut *mut xmlChar,
);
assert!(
len < 0,
"exclusive C14N must reject relative namespace URIs"
);
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_pi_document_level_newlines() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let pi1 = tree::new_pi(b"one\0" as *const u8 as *const xmlChar, ptr::null());
tree::add_child(doc as *mut _xmlNode, pi1);
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
tree::add_child(doc as *mut _xmlNode, root);
let pi2 = tree::new_pi(b"three\0" as *const u8 as *const xmlChar, ptr::null());
tree::add_child(doc as *mut _xmlNode, pi2);
let out = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert_eq!(out, "<?one?>\n<root></root>\n<?three?>");
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_subset_visibility() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
tree::doc_set_root_element(doc, root);
let child = tree::new_node(ptr::null_mut(), b"child\0" as *const u8 as *const xmlChar);
tree::add_child(root, child);
let mut nodes: Vec<*mut _xmlNode> = vec![root, ptr::null_mut()];
let mut result: *mut xmlChar = ptr::null_mut();
let len = c14n_doc_dump_memory(
doc,
nodes.as_mut_ptr(),
C14nMode::XML_C14N_1_0,
ptr::null(),
0,
&mut result as *mut *mut xmlChar,
);
assert!(len >= 0);
let s = {
let slice = core::slice::from_raw_parts(result, len as usize);
String::from_utf8_lossy(slice).to_string()
};
xmlFreeImpl(result as *mut c_void);
assert_eq!(s, "<root></root>");
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_subset_hidden_parent_xml_lang() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let xml_ns = tree::new_ns(
ptr::null_mut(),
b"http://www.w3.org/XML/1998/namespace\0" as *const u8 as *const xmlChar,
b"xml\0" as *const u8 as *const xmlChar,
);
(*doc).oldNs = xml_ns;
let root = tree::new_node(ptr::null_mut(), b"root\0" as *const u8 as *const xmlChar);
tree::doc_set_root_element(doc, root);
tree::set_prop(
root,
b"lang\0" as *const u8 as *const xmlChar,
b"en\0" as *const u8 as *const xmlChar,
);
let lang_attr = (*root).properties;
(*lang_attr).ns = xml_ns;
let child = tree::new_node(ptr::null_mut(), b"a\0" as *const u8 as *const xmlChar);
tree::add_child(root, child);
let mut nodes: Vec<*mut _xmlNode> = vec![child, ptr::null_mut()];
let mut result: *mut xmlChar = ptr::null_mut();
let len = c14n_doc_dump_memory(
doc,
nodes.as_mut_ptr(),
C14nMode::XML_C14N_1_0,
ptr::null(),
0,
&mut result as *mut *mut xmlChar,
);
assert!(len >= 0);
let s = {
let slice = core::slice::from_raw_parts(result, len as usize);
String::from_utf8_lossy(slice).to_string()
};
xmlFreeImpl(result as *mut c_void);
assert_eq!(s, "<a xml:lang=\"en\"></a>");
tree::free_doc(doc);
}
}
#[test]
fn test_c14n_rebinding_chain_rere_declares() {
unsafe {
let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
let ns1 = tree::new_ns(
ptr::null_mut(),
b"http://u/1\0" as *const u8 as *const xmlChar,
b"p\0" as *const u8 as *const xmlChar,
);
let root = tree::new_node(ptr::null_mut(), b"a\0" as *const u8 as *const xmlChar);
tree::doc_set_root_element(doc, root);
tree::new_ns(
root,
b"http://u/1\0" as *const u8 as *const xmlChar,
b"p\0" as *const u8 as *const xmlChar,
);
let b = tree::new_node(ns1, b"b\0" as *const u8 as *const xmlChar);
tree::add_child(root, b);
let ns2 = tree::new_ns(
ptr::null_mut(),
b"http://u/2\0" as *const u8 as *const xmlChar,
b"p\0" as *const u8 as *const xmlChar,
);
tree::new_ns(
b,
b"http://u/2\0" as *const u8 as *const xmlChar,
b"p\0" as *const u8 as *const xmlChar,
);
(*b).ns = ns2;
let c = tree::new_node(ns1, b"c\0" as *const u8 as *const xmlChar);
tree::add_child(b, c);
tree::new_ns(
c,
b"http://u/1\0" as *const u8 as *const xmlChar,
b"p\0" as *const u8 as *const xmlChar,
);
let out = canonicalize_doc(doc, C14nMode::XML_C14N_1_0, 0);
assert_eq!(
out,
"<a xmlns:p=\"http://u/1\"><p:b xmlns:p=\"http://u/2\"><p:c xmlns:p=\"http://u/1\"></p:c></p:b></a>"
);
tree::free_doc(doc);
}
}
}