#![doc = include_str!("../Readme.md")]
use libxml::bindings::{
xmlC14NDocDumpMemory, xmlChar, xmlDocPtr, xmlFree, xmlFreeDoc, xmlNodeSet, xmlReadDoc,
};
use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::iter::once;
use std::ptr::null;
use thiserror::Error;
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct CanonicalizationOptions {
pub mode: CanonicalizationMode,
pub keep_comments: bool,
pub inclusive_ns_prefixes: Vec<String>,
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub enum CanonicalizationMode {
Canonical1_0,
#[default]
ExclusiveCanonical1_0,
Canonical1_1,
}
impl CanonicalizationMode {
fn to_c_int(self) -> c_int {
c_int::from(match self {
CanonicalizationMode::Canonical1_0 => 0,
CanonicalizationMode::ExclusiveCanonical1_0 => 1,
CanonicalizationMode::Canonical1_1 => 2,
})
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Error)]
#[error("canonicalization error ({0})")]
pub struct CanonicalizationErrorCode(i32);
pub fn canonicalize_xml(
document: &str,
options: CanonicalizationOptions,
) -> Result<String, CanonicalizationErrorCode> {
let document = read_document(document);
unsafe {
let (output, return_code) = canonicalize_document_to_c_pointer(options, document);
let result = if return_code < 0 {
Err(CanonicalizationErrorCode(return_code))
} else {
let c_str = CStr::from_ptr(output as *const _);
let str_slice: &str = c_str.to_str().unwrap();
Ok(str_slice.to_owned())
};
let free_function = xmlFree.unwrap();
free_function(output as *mut c_void);
xmlFreeDoc(document);
result
}
}
unsafe fn canonicalize_document_to_c_pointer(
options: CanonicalizationOptions,
document: xmlDocPtr,
) -> (*const xmlChar, c_int) {
let nodes = null::<xmlNodeSet>() as *mut _;
let mut ns_list_c = to_xml_string_vec(options.inclusive_ns_prefixes);
let with_comments = c_int::from(options.keep_comments);
let mut output = null::<xmlChar>() as *mut xmlChar;
let return_code = xmlC14NDocDumpMemory(
document,
nodes,
options.mode.to_c_int(),
ns_list_c.as_mut_ptr(),
with_comments,
(&mut output) as *mut _,
);
free_xml_string_vec(ns_list_c);
(output, return_code)
}
fn to_xml_string_vec(vec: Vec<String>) -> Vec<*mut xmlChar> {
vec.into_iter()
.map(|s| CString::new(s).unwrap().into_raw() as *mut xmlChar)
.chain(once(std::ptr::null_mut()))
.collect()
}
unsafe fn free_xml_string_vec(vec: Vec<*mut xmlChar>) {
for s in vec {
if !s.is_null() {
let _ = CString::from_raw(s as *mut c_char);
}
}
}
fn read_document(document: &str) -> xmlDocPtr {
unsafe {
let c_document = CString::new(document).unwrap();
let url = CString::default();
let encoding = null();
xmlReadDoc(
c_document.as_ptr() as *const xmlChar,
url.as_ptr(),
encoding,
c_int::from(0),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_1_1_example_3_1_no_comment() {
let input = include_str!("samples/canonical_1_1/3_1_input.xml");
let expected = include_str!("samples/canonical_1_1/3_1_output_no_comment.xml");
let canonicalized = canonicalize_xml(
input,
CanonicalizationOptions {
mode: CanonicalizationMode::Canonical1_1,
keep_comments: false,
inclusive_ns_prefixes: vec![],
},
)
.unwrap();
assert_eq!(canonicalized, expected)
}
#[test]
fn canonical_1_1_example_3_1() {
let input = include_str!("samples/canonical_1_1/3_1_input.xml");
let expected = include_str!("samples/canonical_1_1/3_1_output.xml");
let canonicalized = canonicalize_xml(
input,
CanonicalizationOptions {
mode: CanonicalizationMode::Canonical1_1,
keep_comments: true,
inclusive_ns_prefixes: vec![],
},
)
.unwrap();
assert_eq!(canonicalized, expected)
}
#[test]
fn canonical_1_1_example_3_2() {
let input = include_str!("samples/canonical_1_1/3_2_input.xml");
let expected = include_str!("samples/canonical_1_1/3_2_output.xml");
let canonicalized = canonicalize_xml(
input,
CanonicalizationOptions {
mode: CanonicalizationMode::Canonical1_1,
keep_comments: true,
inclusive_ns_prefixes: vec![],
},
)
.unwrap();
assert_eq!(canonicalized, expected.trim())
}
#[test]
fn canonical_exclusive_example_1() {
let input = include_str!("samples/canonical_exclusive/1_input.xml");
let expected = include_str!("samples/canonical_exclusive/1_output.xml");
let canonicalized = canonicalize_xml(
input,
CanonicalizationOptions {
mode: CanonicalizationMode::ExclusiveCanonical1_0,
keep_comments: true,
inclusive_ns_prefixes: vec![],
},
)
.unwrap();
assert_eq!(canonicalized, expected.trim())
}
#[test]
fn canonical_exclusive_example_2() {
let input = include_str!("samples/canonical_exclusive/2_input.xml");
let expected = include_str!("samples/canonical_exclusive/2_output.xml");
let canonicalized = canonicalize_xml(
input,
CanonicalizationOptions {
mode: CanonicalizationMode::ExclusiveCanonical1_0,
keep_comments: true,
inclusive_ns_prefixes: ["stay1".to_string(), "stay2".to_string()].to_vec(),
},
)
.unwrap();
assert_eq!(canonicalized, expected.trim())
}
#[test]
fn invalid_xml() {
let input = "<invalid xml";
let canonicalized = canonicalize_xml(
input,
CanonicalizationOptions {
mode: CanonicalizationMode::Canonical1_0,
keep_comments: false,
inclusive_ns_prefixes: vec![],
},
);
assert!(canonicalized.is_err())
}
#[test]
fn display_error() {
let formatted = format!("{}", CanonicalizationErrorCode(-1));
let expected = "canonicalization error (-1)";
assert_eq!(formatted, expected);
}
}