use core::fmt::Write as _;
pub(crate) struct Rules {
pub order: fn(&str) -> Option<&'static [&'static str]>,
pub forbidden_path: fn(&str) -> Option<&'static str>,
pub forbidden_attribute: fn(&str) -> Option<&'static str>,
}
#[derive(Clone)]
pub(crate) struct Node {
name: String,
attrs: Vec<(String, String)>,
text: Option<String>,
children: Vec<Node>,
}
pub(crate) struct Xml {
stack: Vec<Node>,
rules: &'static Rules,
}
impl Xml {
pub fn new(root: &str, attrs: Vec<(String, String)>, rules: &'static Rules) -> Self {
Self {
stack: vec![Node {
name: root.to_owned(),
attrs,
text: None,
children: Vec::new(),
}],
rules,
}
}
fn push(&mut self, node: Node) {
self.stack
.last_mut()
.expect("the root is never popped")
.children
.push(node);
}
pub fn leaf(&mut self, name: &str, attrs: &[(&str, &str)], text: &str) {
self.push(Node {
name: name.to_owned(),
attrs: attrs
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect(),
text: Some(text.to_owned()),
children: Vec::new(),
});
}
pub fn group(&mut self, name: &str, f: impl FnOnce(&mut Self)) {
self.stack.push(Node {
name: name.to_owned(),
attrs: Vec::new(),
text: None,
children: Vec::new(),
});
f(self);
let node = self.stack.pop().expect("group pushed a node");
if !node.children.is_empty() {
self.push(node);
}
}
#[cfg(feature = "cii")]
pub fn group_required(&mut self, name: &str, f: impl FnOnce(&mut Self)) {
self.stack.push(Node {
name: name.to_owned(),
attrs: Vec::new(),
text: None,
children: Vec::new(),
});
f(self);
let node = self.stack.pop().expect("group pushed a node");
self.push(node);
}
pub fn finish(mut self) -> (String, Vec<String>) {
let root = self.stack.pop().expect("the root");
debug_assert!(self.stack.is_empty(), "unbalanced group()");
let mut out = String::with_capacity(4096);
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
let mut dropped = Vec::new();
let name = root.name.clone();
render(&root, &name, 0, &mut out, &mut dropped, self.rules);
(out, dropped)
}
}
pub(crate) fn path_matches(path: &str, context: &str, relative: &str) -> bool {
let floating = context.starts_with("//") || !context.starts_with('/');
let ctx = context.trim_start_matches('/');
if floating {
let needle = format!("{ctx}/{relative}");
return path == needle || path.ends_with(&format!("/{needle}"));
}
let Some((head, rest)) = path.split_once('/') else {
return false;
};
local(head) == local(ctx) && rest == relative
}
fn local(name: &str) -> &str {
name.rsplit(':').next().unwrap_or(name)
}
fn order_children(parent: &str, children: &mut Vec<Node>, dropped: &mut Vec<String>, r: &Rules) {
let Some(seq) = (r.order)(local(parent)) else {
return; };
children.retain(|c| {
let known = seq.contains(&local(&c.name));
if !known {
dropped.push(format!("{}/{}", local(parent), c.name));
}
known
});
children.sort_by_key(|c| {
seq.iter()
.position(|e| *e == local(&c.name))
.unwrap_or(usize::MAX)
});
}
fn render(
node: &Node,
path: &str,
depth: usize,
out: &mut String,
dropped: &mut Vec<String>,
r: &Rules,
) {
for _ in 0..depth {
out.push_str(" ");
}
let _ = write!(out, "<{}", node.name);
for (k, v) in &node.attrs {
if let Some(rule) = (r.forbidden_attribute)(k) {
dropped.push(format!("{path}/@{k} ({rule})"));
continue;
}
let _ = write!(out, " {k}=\"");
escape(v, out);
out.push('"');
}
if node.children.is_empty() {
if node.text.is_none() {
let _ = writeln!(out, "/>");
return;
}
out.push('>');
escape(node.text.as_deref().unwrap_or_default(), out);
let _ = writeln!(out, "</{}>", node.name);
return;
}
let _ = writeln!(out, ">");
let mut kids = node.children.clone();
order_children(&node.name, &mut kids, dropped, r);
for c in &kids {
let child_path = if path.is_empty() {
c.name.clone()
} else {
format!("{path}/{}", c.name)
};
if let Some(rule) = (r.forbidden_path)(&child_path) {
dropped.push(format!("{child_path} ({rule})"));
continue;
}
render(c, &child_path, depth + 1, out, dropped, r);
}
for _ in 0..depth {
out.push_str(" ");
}
let _ = writeln!(out, "</{}>", node.name);
}
pub(crate) fn escape(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c if (c as u32) < 0x20 && !matches!(c, '\t' | '\n' | '\r') => {}
c => out.push(c),
}
}
}
pub(crate) fn base64(bytes: &[u8]) -> String {
const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for c in bytes.chunks(3) {
let b = [c[0], *c.get(1).unwrap_or(&0), *c.get(2).unwrap_or(&0)];
let n = u32::from(b[0]) << 16 | u32::from(b[1]) << 8 | u32::from(b[2]);
out.push(A[(n >> 18 & 63) as usize] as char);
out.push(A[(n >> 12 & 63) as usize] as char);
out.push(if c.len() > 1 {
A[(n >> 6 & 63) as usize] as char
} else {
'='
});
out.push(if c.len() > 2 {
A[(n & 63) as usize] as char
} else {
'='
});
}
out
}
pub(crate) fn decode_base64(s: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(s.len() / 4 * 3);
let mut acc: u32 = 0;
let mut bits = 0u32;
for c in s.bytes() {
let v = match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
b'=' => break,
_ => continue, };
acc = acc << 6 | u32::from(v);
bits += 6;
if bits >= 8 {
bits -= 8;
#[allow(clippy::cast_possible_truncation)]
out.push((acc >> bits) as u8);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
static NO_RULES: Rules = Rules {
order: |_| None,
forbidden_path: |_| None,
forbidden_attribute: |_| None,
};
#[test]
fn base64_matches_rfc_4648_vectors() {
for (plain, encoded) in [
(&b""[..], ""),
(b"f", "Zg=="),
(b"fo", "Zm8="),
(b"foo", "Zm9v"),
(b"foob", "Zm9vYg=="),
(b"fooba", "Zm9vYmE="),
(b"foobar", "Zm9vYmFy"),
] {
assert_eq!(base64(plain), encoded);
assert_eq!(decode_base64(encoded), plain, "round trip {encoded}");
}
}
#[test]
fn decoding_ignores_the_whitespace_xml_permits() {
assert_eq!(decode_base64("Zm9v\n YmFy"), b"foobar");
}
#[test]
fn text_is_escaped() {
let mut s = String::new();
escape("x<y & \"z\" 'q'\u{7}", &mut s);
assert_eq!(s, "x<y & "z" 'q'");
}
#[test]
fn empty_groups_vanish() {
let mut x = Xml::new("Root", vec![], &NO_RULES);
x.group("a:Empty", |_| {});
let (xml, dropped) = x.finish();
assert!(!xml.contains("Empty"), "{xml}");
assert!(dropped.is_empty());
}
#[cfg(feature = "cii")]
#[test]
fn a_required_group_survives_being_empty() {
let mut x = Xml::new("Root", vec![], &NO_RULES);
x.group_required("a:Mandatory", |_| {});
x.group("a:Optional", |_| {});
let (xml, dropped) = x.finish();
assert!(xml.contains("<a:Mandatory/>"), "{xml}");
assert!(!xml.contains("Optional"), "{xml}");
assert!(dropped.is_empty());
}
#[test]
fn an_anchored_context_matches_only_at_the_root() {
assert!(path_matches("Invoice/cbc:UUID", "/ubl:Invoice", "cbc:UUID"));
assert!(path_matches(
"ubl:Invoice/cbc:UUID",
"/ubl:Invoice",
"cbc:UUID"
));
assert!(!path_matches(
"Invoice/cac:Party/cbc:UUID",
"/ubl:Invoice",
"cbc:UUID"
));
assert!(!path_matches(
"CreditNote/cbc:UUID",
"/ubl:Invoice",
"cbc:UUID"
));
}
#[test]
fn a_floating_context_matches_at_any_depth() {
assert!(path_matches(
"Invoice/cac:Party/cbc:X",
"//cac:Party",
"cbc:X"
));
assert!(path_matches("cac:Party/cbc:X", "//cac:Party", "cbc:X"));
assert!(path_matches("A/B/cac:Party/cbc:X", "cac:Party", "cbc:X"));
assert!(!path_matches(
"Invoice/cac:MyParty/cbc:X",
"//cac:Party",
"cbc:X"
));
}
#[test]
fn an_unknown_parent_keeps_the_writers_order() {
let mut x = Xml::new("Root", vec![], &NO_RULES);
x.leaf("a:Second", &[], "2");
x.leaf("a:First", &[], "1");
let (xml, dropped) = x.finish();
assert!(xml.find("Second").unwrap() < xml.find("First").unwrap());
assert!(dropped.is_empty());
}
}