xso: add support for base64 text codec

Jonas Schรคfer created

Change summary

parsers/Cargo.toml |  2 
xso/Cargo.toml     |  4 +
xso/src/text.rs    | 74 ++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 78 insertions(+), 2 deletions(-)

Detailed changes

parsers/Cargo.toml ๐Ÿ”—

@@ -24,7 +24,7 @@ chrono = { version = "0.4.5", default-features = false, features = ["std"] }
 # same repository dependencies
 jid = { version = "0.10", features = ["minidom"], path = "../jid" }
 minidom = { version = "0.15", path = "../minidom" }
-xso = { version = "0.0.2", features = ["macros", "minidom", "panicking-into-impl", "jid"] }
+xso = { version = "0.0.2", features = ["macros", "minidom", "panicking-into-impl", "jid", "base64"] }
 
 [features]
 # Build xmpp-parsers to make components instead of clients.

xso/Cargo.toml ๐Ÿ”—

@@ -14,13 +14,15 @@ rxml = { version = "0.11.0", default-features = false }
 minidom = { version = "^0.15" }
 xso_proc = { version = "0.0.2", optional = true }
 
-# optional dependencies to provide text conversion to/from types from these crates
+# optional dependencies to provide text conversion to/from types from/using
+# these crates
 # NOTE: because we don't have public/private dependencies yet and cargo
 # defaults to picking the highest matching version by default, the only
 # sensible thing we can do here is to depend on the least version of the most
 # recent semver of each crate.
 jid = { version = "^0.10", optional = true }
 uuid = { version = "^1", optional = true }
+base64 = { version = "^0.22", optional = true }
 
 [features]
 macros = [ "dep:xso_proc" ]

xso/src/text.rs ๐Ÿ”—

@@ -6,8 +6,13 @@
 
 //! Module containing implementations for conversions to/from XML text.
 
+#[cfg(feature = "base64")]
+use core::marker::PhantomData;
+
 use crate::{error::Error, FromXmlText, IntoXmlText};
 
+#[cfg(feature = "base64")]
+use base64::engine::{general_purpose::STANDARD as StandardBase64Engine, Engine as _};
 #[cfg(feature = "jid")]
 use jid;
 #[cfg(feature = "uuid")]
@@ -160,3 +165,72 @@ impl TextCodec<Option<String>> for EmptyAsNone {
         })
     }
 }
+
+/// Trait for preprocessing text data from XML.
+///
+/// This may be used by codecs to allow to customize some of their behaviour.
+pub trait TextFilter {
+    /// Process the incoming string and return the result of the processing.
+    fn preprocess(s: String) -> String;
+}
+
+/// Text preprocessor which returns the input unchanged.
+pub struct NoFilter;
+
+impl TextFilter for NoFilter {
+    fn preprocess(s: String) -> String {
+        s
+    }
+}
+
+/// Text preprocessor to remove all whitespace.
+pub struct StripWhitespace;
+
+impl TextFilter for StripWhitespace {
+    fn preprocess(s: String) -> String {
+        let s: String = s
+            .chars()
+            .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
+            .collect();
+        s
+    }
+}
+
+/// Text codec transforming text to binary using standard base64.
+///
+/// The `Filter` type argument can be used to employ additional preprocessing
+/// of incoming text data. Most interestingly, passing [`StripWhitespace`]
+/// will make the implementation ignore any whitespace within the text.
+#[cfg(feature = "base64")]
+#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
+pub struct Base64<Filter: TextFilter = NoFilter>(PhantomData<Filter>);
+
+#[cfg(feature = "base64")]
+#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
+impl<Filter: TextFilter> TextCodec<Vec<u8>> for Base64<Filter> {
+    fn decode(s: String) -> Result<Vec<u8>, Error> {
+        let value = Filter::preprocess(s);
+        Ok(StandardBase64Engine
+            .decode(value.as_str().as_bytes())
+            .map_err(Error::text_parse_error)?)
+    }
+
+    fn encode(value: Vec<u8>) -> Result<Option<String>, Error> {
+        Ok(Some(StandardBase64Engine.encode(&value)))
+    }
+}
+
+#[cfg(feature = "base64")]
+#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
+impl<Filter: TextFilter> TextCodec<Option<Vec<u8>>> for Base64<Filter> {
+    fn decode(s: String) -> Result<Option<Vec<u8>>, Error> {
+        if s.len() == 0 {
+            return Ok(None);
+        }
+        Ok(Some(Self::decode(s)?))
+    }
+
+    fn encode(decoded: Option<Vec<u8>>) -> Result<Option<String>, Error> {
+        decoded.map(Self::encode).transpose().map(Option::flatten)
+    }
+}