// Copyright (c) 2024 Jonas Schäfer // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. //! Module containing implementations for conversions to/from XML text. use core::marker::PhantomData; use std::borrow::Cow; use crate::{error::Error, AsXmlText, FromXmlText}; #[cfg(feature = "base64")] use base64::engine::{general_purpose::STANDARD as StandardBase64Engine, Engine as _}; macro_rules! convert_via_fromstr_and_display { ($($(#[cfg $cfg:tt])?$t:ty,)+) => { $( $( #[cfg $cfg] )? impl FromXmlText for $t { #[doc = concat!("Parse [`", stringify!($t), "`] from XML text via [`FromStr`][`core::str::FromStr`].")] fn from_xml_text(s: String) -> Result { s.parse().map_err(Error::text_parse_error) } } $( #[cfg $cfg] )? impl AsXmlText for $t { #[doc = concat!("Convert [`", stringify!($t), "`] to XML text via [`Display`][`core::fmt::Display`].\n\nThis implementation never fails.")] fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Owned(self.to_string())) } } )+ } } /// This provides an implementation compliant with xsd::bool. impl FromXmlText for bool { /// Parse a boolean from XML text. /// /// The values `"1"` and `"true"` are considered true. The values `"0"` /// and `"false"` are considered `false`. Any other value is invalid and /// will return an error. fn from_xml_text(s: String) -> Result { match s.as_str() { "1" => "true", "0" => "false", other => other, } .parse() .map_err(Error::text_parse_error) } } /// This provides an implementation compliant with xsd::bool. impl AsXmlText for bool { /// Convert a boolean to XML text. /// /// `true` is converted to `"true"` and `false` is converted to `"false"`. /// This implementation never fails. fn as_xml_text(&self) -> Result, Error> { match self { true => Ok(Cow::Borrowed("true")), false => Ok(Cow::Borrowed("false")), } } } convert_via_fromstr_and_display! { u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, char, core::net::IpAddr, core::net::Ipv4Addr, core::net::Ipv6Addr, core::net::SocketAddr, core::net::SocketAddrV4, core::net::SocketAddrV6, core::num::NonZeroU8, core::num::NonZeroU16, core::num::NonZeroU32, core::num::NonZeroU64, core::num::NonZeroU128, core::num::NonZeroUsize, core::num::NonZeroI8, core::num::NonZeroI16, core::num::NonZeroI32, core::num::NonZeroI64, core::num::NonZeroI128, core::num::NonZeroIsize, #[cfg(feature = "uuid")] uuid::Uuid, #[cfg(feature = "jid")] jid::Jid, #[cfg(feature = "jid")] jid::FullJid, #[cfg(feature = "jid")] jid::BareJid, } /// Represent a way to encode/decode text data into a Rust type. /// /// This trait can be used in scenarios where implementing [`FromXmlText`] /// and/or [`AsXmlText`] on a type is not feasible or sensible, such as the /// following: /// /// 1. The type originates in a foreign crate, preventing the implementation /// of foreign traits. /// /// 2. There is more than one way to convert a value to/from XML. /// /// The codec to use for a text can be specified in the attributes understood /// by `FromXml` and `AsXml` derive macros. See the documentation of the /// [`FromXml`][`macro@crate::FromXml`] derive macro for details. pub trait TextCodec { /// Decode a string value into the type. fn decode(&self, s: String) -> Result; /// Encode the type as string value. /// /// If this returns `None`, the string value is not emitted at all. fn encode<'x>(&self, value: &'x T) -> Result>, Error>; /// Apply a filter to this codec. /// /// Filters preprocess strings before they are handed to the codec for /// parsing, allowing to, for example, make the codec ignore irrelevant /// content by stripping it. // NOTE: The bound on T is needed because any given type A may implement // TextCodec for any number of types. If we pass T down to the `Filtered` // struct, rustc can do type inferrence on which `TextCodec` // implementation the `filtered` method is supposed to have been called // on. fn filtered(self, filter: F) -> Filtered where // placing the bound here (instead of on the `TextCodec` trait // itself) preserves object-safety of TextCodec. Self: Sized, { Filtered { filter, codec: self, bound: PhantomData, } } } /// Wrapper struct to apply a filter to a codec. /// /// You can construct a value of this type via [`TextCodec::filtered`]. // NOTE: see the note on TextCodec::filtered for why we bind `T` here, too. pub struct Filtered { filter: F, codec: C, bound: PhantomData, } impl> TextCodec for Filtered { fn decode(&self, s: String) -> Result { let s = self.filter.preprocess(s); self.codec.decode(s) } fn encode<'x>(&self, value: &'x T) -> Result>, Error> { self.codec.encode(value) } } /// Text codec which does no transform. pub struct Plain; impl TextCodec for Plain { fn decode(&self, s: String) -> Result { Ok(s) } fn encode<'x>(&self, value: &'x String) -> Result>, Error> { Ok(Some(Cow::Borrowed(value.as_str()))) } } /// Text codec which returns `None` if the input to decode is the empty string, instead of /// attempting to decode it. /// /// Particularly useful when parsing `Option` on `#[xml(text)]`, which does not support /// `Option<_>` otherwise. pub struct EmptyAsNone; impl TextCodec> for EmptyAsNone where T: FromXmlText + AsXmlText, { fn decode(&self, s: String) -> Result, Error> { if s.is_empty() { Ok(None) } else { Some(T::from_xml_text(s)).transpose() } } fn encode<'x>(&self, value: &'x Option) -> Result>, Error> { Ok(value .as_ref() .map(AsXmlText::as_xml_text) .transpose()? .map(|v| (!v.is_empty()).then_some(v)) .flatten()) } } /// Text codec which returns None instead of the empty string. pub struct EmptyAsError; impl TextCodec for EmptyAsError { fn decode(&self, s: String) -> Result { if s.is_empty() { Err(Error::Other("Empty text node.")) } else { Ok(s) } } fn encode<'x>(&self, value: &'x String) -> Result>, Error> { if value.is_empty() { Err(Error::Other("Empty text node.")) } else { Ok(Some(Cow::Borrowed(value.as_str()))) } } } /// 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(&self, s: String) -> String; } /// Text preprocessor which returns the input unchanged. pub struct NoFilter; impl TextFilter for NoFilter { fn preprocess(&self, s: String) -> String { s } } /// Text preprocessor to remove all whitespace. pub struct StripWhitespace; impl TextFilter for StripWhitespace { fn preprocess(&self, 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")] pub struct Base64; #[cfg(feature = "base64")] impl TextCodec> for Base64 { fn decode(&self, s: String) -> Result, Error> { StandardBase64Engine .decode(s.as_bytes()) .map_err(Error::text_parse_error) } fn encode<'x>(&self, value: &'x Vec) -> Result>, Error> { Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value)))) } } #[cfg(feature = "base64")] impl<'x> TextCodec> for Base64 { fn decode(&self, s: String) -> Result, Error> { StandardBase64Engine .decode(s.as_bytes()) .map_err(Error::text_parse_error) .map(Cow::Owned) } fn encode<'a>(&self, value: &'a Cow<'x, [u8]>) -> Result>, Error> { Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value)))) } } #[cfg(feature = "base64")] impl TextCodec> for Base64 where Base64: TextCodec, { fn decode(&self, s: String) -> Result, Error> { if s.is_empty() { return Ok(None); } Ok(Some(self.decode(s)?)) } fn encode<'x>(&self, decoded: &'x Option) -> Result>, Error> { decoded .as_ref() .map(|x| self.encode(x)) .transpose() .map(Option::flatten) } } /// Text codec transforming text to binary using hexadecimal nibbles. /// /// The length must be known at compile-time. pub struct FixedHex; impl TextCodec<[u8; N]> for FixedHex { fn decode(&self, s: String) -> Result<[u8; N], Error> { if s.len() != 2 * N { return Err(Error::Other("Invalid length")); } let mut bytes = [0u8; N]; for i in 0..N { bytes[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(Error::text_parse_error)?; } Ok(bytes) } fn encode<'x>(&self, value: &'x [u8; N]) -> Result>, Error> { let mut bytes = String::with_capacity(N * 2); for byte in value { bytes.extend(format!("{:02x}", byte).chars()); } Ok(Some(Cow::Owned(bytes))) } } impl TextCodec> for FixedHex where FixedHex: TextCodec, { fn decode(&self, s: String) -> Result, Error> { if s.is_empty() { return Ok(None); } Ok(Some(self.decode(s)?)) } fn encode<'x>(&self, decoded: &'x Option) -> Result>, Error> { decoded .as_ref() .map(|x| self.encode(x)) .transpose() .map(Option::flatten) } } /// Text codec for colon-separated bytes of uppercase hexadecimal. pub struct ColonSeparatedHex; impl TextCodec> for ColonSeparatedHex { fn decode(&self, s: String) -> Result, Error> { assert_eq!((s.len() + 1) % 3, 0); let mut bytes = Vec::with_capacity((s.len() + 1) / 3); for i in 0..(1 + s.len()) / 3 { let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16).map_err(Error::text_parse_error)?; if 3 * i + 2 < s.len() { assert_eq!(&s[3 * i + 2..3 * i + 3], ":"); } bytes.push(byte); } Ok(bytes) } fn encode<'x>(&self, decoded: &'x Vec) -> Result>, Error> { // TODO: Super inefficient! let mut bytes = Vec::with_capacity(decoded.len()); for byte in decoded { bytes.push(format!("{:02X}", byte)); } Ok(Some(Cow::Owned(bytes.join(":")))) } }