libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

xml.rs (14641B)


      1 /*
      2 * This file is part of LibEuFin.
      3 * Copyright (C) 2026 Taler Systems S.A.
      4 
      5 * LibEuFin is free software; you can redistribute it and/or modify
      6 * it under the terms of the GNU Affero General Public License as
      7 * published by the Free Software Foundation; either version 3, or
      8 * (at your option) any later version.
      9 
     10 * LibEuFin is distributed in the hope that it will be useful, but
     11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13 * Public License for more details.
     14 
     15 * You should have received a copy of the GNU Affero General Public
     16 * License along with LibEuFin; see the file COPYING.  If not, see
     17 * <http://www.gnu.org/licenses/>
     18 */
     19 
     20 use std::{
     21     fmt::{Display, Write},
     22     str::{FromStr, Utf8Error},
     23 };
     24 
     25 use roxmltree::{Document, Node};
     26 use taler_common::encoding::base64;
     27 
     28 #[macro_export]
     29 macro_rules! xml {
     30     // Trailing comma
     31     ($w:ident => $(,)?) => {{}};
     32     // Logic escape
     33     ($w:ident => @ $logic:expr$(, $($rest:tt)*)?) => {{
     34         ($logic)($w);
     35         $($crate::xml!($w => $($rest)*);)*
     36     }};
     37     // Text element
     38     ($w:ident => $name:tt $($k:literal=$v:tt)* : $content:expr $(, $($rest:tt)*)?) => {{
     39         $w.text(&$name, &[$((&$k, &$v)),*], &$content);
     40         $($crate::xml!($w => $($rest)*);)*
     41     }};
     42     // Nested block
     43     ($w:ident => $name:tt $($k:literal=$v:tt)* { $($body:tt)* }$(, $($rest:tt)*)?) => {{
     44         let name = &$name;
     45         $w.open(&name, &[$((&$k, &$v)),*]);
     46         $crate::xml!($w => $($body)*);
     47         $w.close(&name);
     48         $($crate::xml!($w => $($rest)*);)*
     49     }};
     50     // Empty element
     51     ($w:ident => $name:tt $($k:literal=$v:tt)* $(, $($rest:tt)*)?) => {{
     52         $w.empty(&$name, &[$((&$k, &$v)),*]);
     53         $($crate::xml!($w => $($rest)*);)*
     54     }};
     55     // Root builder
     56     ($name:tt $($k:literal=$v:tt)* { $($body:tt)* }) => {{
     57         let mut writer = $crate::xml::XmlWriter::init();
     58         let w = &mut writer;
     59         let name = &$name;
     60         w.open(&name, &[$((&$k, &$v)),*]);
     61         $crate::xml!(w => $($body)*);
     62         w.close(&name);
     63         writer.finish()
     64     }};
     65 }
     66 
     67 pub struct XmlWriter {
     68     xml: String,
     69 }
     70 
     71 impl XmlWriter {
     72     pub fn init() -> Self {
     73         let mut xml = String::with_capacity(1024);
     74         xml.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
     75         Self { xml }
     76     }
     77 
     78     pub fn open<N: Display>(&mut self, name: N, attrs: &[(&dyn Display, &dyn Display)]) {
     79         self.xml.push('<');
     80         self.write_tag_attrs(name, attrs);
     81         self.xml.push('>');
     82     }
     83 
     84     pub fn close<N: Display>(&mut self, name: N) {
     85         self.xml.push_str("</");
     86         self.xml.write_fmt(format_args!("{name}")).unwrap();
     87         self.xml.push('>');
     88     }
     89 
     90     pub fn empty<N: Display>(&mut self, name: N, attrs: &[(&dyn Display, &dyn Display)]) {
     91         self.xml.push('<');
     92         self.write_tag_attrs(name, attrs);
     93         self.xml.push_str("/>");
     94     }
     95 
     96     pub fn text<N: Display, C: Display>(
     97         &mut self,
     98         name: N,
     99         attrs: &[(&dyn Display, &dyn Display)],
    100         content: C,
    101     ) {
    102         self.open(&name, attrs);
    103         self.write_escaped(content);
    104         self.close(&name);
    105     }
    106 
    107     fn write_tag_attrs<N: Display>(&mut self, name: N, attrs: &[(&dyn Display, &dyn Display)]) {
    108         self.xml.write_fmt(format_args!("{name}")).unwrap();
    109 
    110         for (key, value) in attrs {
    111             self.xml.push(' ');
    112             self.xml.write_fmt(format_args!("{}", *key)).unwrap();
    113             self.xml.push_str("=\"");
    114             self.write_escaped(*value);
    115             self.xml.push('"');
    116         }
    117     }
    118 
    119     fn write_escaped<D: Display>(&mut self, content: D) {
    120         std::fmt::write(self, format_args!("{content}")).unwrap();
    121     }
    122 
    123     pub fn finish(self) -> String {
    124         self.xml
    125     }
    126 }
    127 
    128 /// Write XML text content following XML escape rules
    129 impl std::fmt::Write for XmlWriter {
    130     fn write_str(&mut self, s: &str) -> std::fmt::Result {
    131         // Single pass over bytes. For each special character, bulk-copy
    132         // everything before it, then push the entity. No double-scan,
    133         // no char-at-a-time pushing for clean runs.
    134         let mut start = 0;
    135         for (i, &b) in s.as_bytes().iter().enumerate() {
    136             let entity = match b {
    137                 b'<' => "&lt;",
    138                 b'>' => "&gt;",
    139                 b'&' => "&amp;",
    140                 b'\'' => "&apos;",
    141                 b'"' => "&quot;",
    142                 _ => continue,
    143             };
    144             self.xml.push_str(&s[start..i]); // bulk copy of clean prefix
    145             self.xml.push_str(entity);
    146             start = i + 1;
    147         }
    148         self.xml.push_str(&s[start..]); // bulk copy of clean suffix
    149         Ok(())
    150     }
    151 }
    152 
    153 #[derive(Debug)]
    154 pub enum Error {
    155     Str(Utf8Error),
    156     Xml(roxmltree::Error),
    157     Root(Box<str>, Box<str>),
    158     Parent(Box<str>),
    159     MissingEl(Box<str>),
    160     MissingAttr(Box<str>, Box<str>),
    161     Duplicate(Box<str>, usize),
    162     Parse(Box<str>, Box<str>),
    163 }
    164 
    165 impl Display for Error {
    166     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    167         match self {
    168             Self::Str(e) => e.fmt(f),
    169             Self::Xml(e) => e.fmt(f),
    170             Self::Root(expected, got) => write!(f, "expected root '{expected}' got '{got}'"),
    171             Self::Parent(path) => write!(f, "not parent for element '{path}'"),
    172             Self::MissingEl(path) => write!(f, "missing element '{path}'"),
    173             Self::MissingAttr(path, name) => write!(f, "missing attribute '{name}' on <{path}>"),
    174             Self::Duplicate(path, nb) => write!(f, "expected one '{path}', got {nb}"),
    175             Self::Parse(path, err) => write!(f, "malformed '{path}': {err}"),
    176         }
    177     }
    178 }
    179 
    180 impl std::error::Error for Error {}
    181 
    182 pub type Result<T> = std::result::Result<T, Error>;
    183 
    184 #[derive(Debug, Clone, Copy)]
    185 pub struct Xml<'xml> {
    186     pub node: Node<'xml, 'xml>,
    187 }
    188 
    189 impl<'xml> Xml<'xml> {
    190     pub fn parse<F, R>(raw: &[u8], tag: &str, f: F) -> Result<R>
    191     where
    192         R: 'static,
    193         F: for<'local> FnOnce(Xml<'local>) -> Result<R>,
    194     {
    195         let str = std::str::from_utf8(raw).map_err(Error::Str)?;
    196         let xml = Document::parse(str).map_err(Error::Xml)?;
    197         Self::doc(xml, tag, f)
    198     }
    199 
    200     pub fn doc<F, R>(xml: Document, tag: &str, f: F) -> Result<R>
    201     where
    202         R: 'static,
    203         F: for<'local> FnOnce(Xml<'local>) -> Result<R>,
    204     {
    205         let root = xml.root_element();
    206         if !root.has_tag_name(tag) {
    207             return Err(Error::Root(tag.into(), root.tag_name().name().into()));
    208         }
    209         let node = Xml { node: root };
    210         let res = f(node);
    211         drop(xml);
    212         res
    213     }
    214 
    215     fn path(self, tag: Option<&str>) -> Box<str> {
    216         let mut ancestors = Vec::new();
    217         let mut cur = Some(self.node);
    218         while let Some(n) = cur {
    219             if n.is_element() {
    220                 ancestors.push(n);
    221             }
    222             cur = n.parent();
    223         }
    224         let mut buf = String::new();
    225         for n in ancestors.into_iter().rev() {
    226             // Add prefix if it exists
    227             if let Some(prefix) = n.tag_name().namespace().and_then(|ns| n.lookup_prefix(ns)) {
    228                 buf.push_str(prefix);
    229                 buf.push(':');
    230             }
    231 
    232             buf.push_str(n.tag_name().name());
    233             buf.push('.');
    234         }
    235         match tag {
    236             Some(t) => buf.push_str(t),
    237             None => {
    238                 buf.pop();
    239             }
    240         }
    241         buf.into()
    242     }
    243 
    244     pub fn parse_err(self, err: impl Display) -> Error {
    245         Error::Parse(self.path(None), err.to_string().into_boxed_str())
    246     }
    247 
    248     pub fn parent(self) -> Result<Xml<'xml>> {
    249         Ok(Self {
    250             node: self
    251                 .node
    252                 .parent()
    253                 .ok_or_else(|| Error::Parent(self.path(None)))?,
    254         })
    255     }
    256 
    257     fn children(self, tag: &str, signed: bool) -> impl Iterator<Item = Node<'xml, 'xml>> {
    258         self.node.children().filter(move |n| {
    259             n.has_tag_name(tag) && (!signed || n.attribute("authenticate") == Some("true"))
    260         })
    261     }
    262 
    263     fn opt_inner(self, tag: &str, signed: bool) -> Result<Option<Xml<'xml>>> {
    264         let mut iter = self.children(tag, signed);
    265         match (iter.next(), iter.next()) {
    266             (None, _) => Ok(None),
    267             (Some(_), Some(_)) => Err(Error::Duplicate(self.path(Some(tag)), iter.count() + 2)),
    268             (Some(node), None) => Ok(Some(Xml { node })),
    269         }
    270     }
    271 
    272     fn one_inner(self, tag: &str, signed: bool) -> Result<Xml<'xml>> {
    273         self.opt_inner(tag, signed)
    274             .transpose()
    275             .unwrap_or_else(|| Err(Error::MissingEl(self.path(Some(tag)))))
    276     }
    277 
    278     pub fn many(self, tag: &str) -> impl Iterator<Item = Xml<'xml>> {
    279         self.children(tag, false).map(|node| Xml { node })
    280     }
    281 
    282     pub fn text(self) -> &'xml str {
    283         self.node.text().unwrap_or_default()
    284     }
    285 
    286     pub fn attr(self, name: &str) -> Result<&'xml str> {
    287         self.node
    288             .attribute(name)
    289             .ok_or_else(|| Error::MissingAttr(self.path(None), name.into()))
    290     }
    291 
    292     pub fn opt_attr(self, name: &str) -> Option<&'xml str> {
    293         self.node.attribute(name)
    294     }
    295 }
    296 
    297 pub trait XmlAccess<'xml>: Sized {
    298     type Out<T>;
    299     type Opt<T>;
    300 
    301     fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::Out<T>>;
    302     fn opt_lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>) -> Result<Self::Opt<T>>;
    303 
    304     fn one(self, tag: &'xml str) -> Result<Self::Out<Xml<'xml>>> {
    305         self.lift(|n| n.one_inner(tag, false))
    306     }
    307 
    308     fn one_signed(self, tag: &'xml str) -> Result<Self::Out<Xml<'xml>>> {
    309         self.lift(|n| n.one_inner(tag, true))
    310     }
    311 
    312     fn opt(self, tag: &'xml str) -> Result<Self::Opt<Xml<'xml>>> {
    313         self.opt_lift(|n| n.opt_inner(tag, false))
    314     }
    315 
    316     fn opt_signed(self, tag: &'xml str) -> Result<Self::Opt<Xml<'xml>>> {
    317         self.opt_lift(|n| n.opt_inner(tag, true))
    318     }
    319 
    320     fn parse_attr<T: FromStr>(self, name: &str) -> Result<Self::Out<T>>
    321     where
    322         T::Err: Display,
    323     {
    324         self.lift(|n| n.attr(name)?.parse().map_err(|e| n.parse_err(e)))
    325     }
    326 
    327     fn parse_opt_attr<T: FromStr>(self, name: &str) -> Result<Self::Opt<T>>
    328     where
    329         T::Err: Display,
    330     {
    331         self.opt_lift(|n| {
    332             n.opt_attr(name)
    333                 .map(|it| it.parse().map_err(|e| n.parse_err(e)))
    334                 .transpose()
    335         })
    336     }
    337 
    338     fn decode<T, E: Display>(
    339         self,
    340         lambda: impl FnOnce(&str) -> std::result::Result<T, E>,
    341     ) -> Result<Self::Out<T>> {
    342         // TODO error not a node text ?
    343         self.lift(|n| lambda(n.text()).map_err(|e| n.parse_err(e)))
    344     }
    345 
    346     fn parse<T: FromStr>(self) -> Result<Self::Out<T>>
    347     where
    348         T::Err: Display,
    349     {
    350         self.decode(T::from_str)
    351     }
    352 
    353     fn b64(self) -> Result<Self::Out<Vec<u8>>> {
    354         self.decode(|it| base64::decode(it))
    355     }
    356 }
    357 
    358 impl<'xml> XmlAccess<'xml> for Xml<'xml> {
    359     type Out<T> = T;
    360     type Opt<T> = Option<T>;
    361 
    362     fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::Out<T>> {
    363         f(self)
    364     }
    365 
    366     fn opt_lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>) -> Result<Self::Opt<T>> {
    367         self.lift(f)
    368     }
    369 }
    370 
    371 impl<'xml> XmlAccess<'xml> for Option<Xml<'xml>> {
    372     type Out<T> = Option<T>;
    373     type Opt<T> = Option<T>;
    374 
    375     fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::Out<T>> {
    376         self.map(|it| it.lift(f)).transpose()
    377     }
    378 
    379     fn opt_lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>) -> Result<Self::Opt<T>> {
    380         match self {
    381             Some(xml) => xml.opt_lift(f),
    382             None => Ok(None),
    383         }
    384     }
    385 }
    386 
    387 impl<'xml> XmlAccess<'xml> for Result<Xml<'xml>> {
    388     type Out<T> = T;
    389     type Opt<T> = Option<T>;
    390 
    391     fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::Out<T>> {
    392         self?.lift(f)
    393     }
    394 
    395     fn opt_lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>) -> Result<Self::Opt<T>> {
    396         self.lift(f)
    397     }
    398 }
    399 
    400 impl<'xml> XmlAccess<'xml> for Result<Option<Xml<'xml>>> {
    401     type Out<T> = Option<T>;
    402     type Opt<T> = Option<T>;
    403 
    404     fn lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<T>) -> Result<Self::Out<T>> {
    405         self?.map(|it| it.lift(f)).transpose()
    406     }
    407 
    408     fn opt_lift<T>(self, f: impl FnOnce(Xml<'xml>) -> Result<Option<T>>) -> Result<Self::Opt<T>> {
    409         match self? {
    410             Some(xml) => xml.opt_lift(f),
    411             None => Ok(None),
    412         }
    413     }
    414 }
    415 
    416 #[cfg(test)]
    417 mod test {
    418     use crate::xml::XmlWriter;
    419 
    420     #[test]
    421     pub fn basic() {
    422         assert_eq!(
    423             xml!("ebicsRequest" "version"="H004" {
    424                 "a" {
    425                     "b" {
    426                         "c" "attribute-of"="c" {
    427                             "d" {
    428                                 "e" {
    429                                     "f" "nested"="true" {
    430                                         "g" {
    431                                             "h"
    432                                         }
    433                                     }
    434                                 }
    435                             }
    436                         }
    437                     }
    438                 },
    439                 "one_more"
    440             }),
    441             r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ebicsRequest version="H004"><a><b><c attribute-of="c"><d><e><f nested="true"><g><h/></g></f></e></d></c></b></a><one_more/></ebicsRequest>"#
    442         )
    443     }
    444 
    445     #[test]
    446     pub fn modularity() {
    447         fn module(w: &mut XmlWriter) {
    448             xml!(w => "module");
    449         }
    450         assert_eq!(
    451             xml!("root" { @ module }),
    452             r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><root><module/></root>"#
    453         )
    454     }
    455 
    456     #[test]
    457     pub fn iterable() {
    458         assert_eq!(
    459             xml!("iterable" {
    460                 "endOfDocument" {
    461                     @ |w: &mut XmlWriter| for i in 1..=10 {
    462                         xml!(w => (format_args!("e{i}")) {
    463                             (format_args!("e{i}{i}")): (format_args!("{i}{i}{i}"))
    464                         })
    465                     }
    466                 }
    467             }),
    468             r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><iterable><endOfDocument><e1><e11>111</e11></e1><e2><e22>222</e22></e2><e3><e33>333</e33></e3><e4><e44>444</e44></e4><e5><e55>555</e55></e5><e6><e66>666</e66></e6><e7><e77>777</e77></e7><e8><e88>888</e88></e8><e9><e99>999</e99></e9><e10><e1010>101010</e1010></e10></endOfDocument></iterable>"#
    469         )
    470     }
    471 }