414 lines
12 KiB
Rust
414 lines
12 KiB
Rust
// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
|
|
//
|
|
// 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<Self, Error> {
|
|
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<Cow<'_, str>, 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<Self, Error> {
|
|
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<Cow<'_, str>, 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<T> {
|
|
/// Decode a string value into the type.
|
|
fn decode(&self, s: String) -> Result<T, Error>;
|
|
|
|
/// 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<Option<Cow<'x, str>>, 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<F: TextFilter>(self, filter: F) -> Filtered<F, Self, T>
|
|
where
|
|
// placing the bound here (instead of on the `TextCodec<T>` trait
|
|
// itself) preserves object-safety of TextCodec<T>.
|
|
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<F, C, T> {
|
|
filter: F,
|
|
codec: C,
|
|
bound: PhantomData<T>,
|
|
}
|
|
|
|
impl<T, F: TextFilter, C: TextCodec<T>> TextCodec<T> for Filtered<F, C, T> {
|
|
fn decode(&self, s: String) -> Result<T, Error> {
|
|
let s = self.filter.preprocess(s);
|
|
self.codec.decode(s)
|
|
}
|
|
|
|
fn encode<'x>(&self, value: &'x T) -> Result<Option<Cow<'x, str>>, Error> {
|
|
self.codec.encode(value)
|
|
}
|
|
}
|
|
|
|
/// Text codec which does no transform.
|
|
pub struct Plain;
|
|
|
|
impl TextCodec<String> for Plain {
|
|
fn decode(&self, s: String) -> Result<String, Error> {
|
|
Ok(s)
|
|
}
|
|
|
|
fn encode<'x>(&self, value: &'x String) -> Result<Option<Cow<'x, str>>, 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<T>` on `#[xml(text)]`, which does not support
|
|
/// `Option<_>` otherwise.
|
|
pub struct EmptyAsNone;
|
|
|
|
impl<T> TextCodec<Option<T>> for EmptyAsNone
|
|
where
|
|
T: FromXmlText + AsXmlText,
|
|
{
|
|
fn decode(&self, s: String) -> Result<Option<T>, Error> {
|
|
if s.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
Some(T::from_xml_text(s)).transpose()
|
|
}
|
|
}
|
|
|
|
fn encode<'x>(&self, value: &'x Option<T>) -> Result<Option<Cow<'x, str>>, 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<String> for EmptyAsError {
|
|
fn decode(&self, s: String) -> Result<String, Error> {
|
|
if s.is_empty() {
|
|
Err(Error::Other("Empty text node."))
|
|
} else {
|
|
Ok(s)
|
|
}
|
|
}
|
|
|
|
fn encode<'x>(&self, value: &'x String) -> Result<Option<Cow<'x, str>>, 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<Vec<u8>> for Base64 {
|
|
fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
|
|
StandardBase64Engine
|
|
.decode(s.as_bytes())
|
|
.map_err(Error::text_parse_error)
|
|
}
|
|
|
|
fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
|
|
Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value))))
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "base64")]
|
|
impl<'x> TextCodec<Cow<'x, [u8]>> for Base64 {
|
|
fn decode(&self, s: String) -> Result<Cow<'x, [u8]>, 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<Option<Cow<'a, str>>, Error> {
|
|
Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value))))
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "base64")]
|
|
impl<T> TextCodec<Option<T>> for Base64
|
|
where
|
|
Base64: TextCodec<T>,
|
|
{
|
|
fn decode(&self, s: String) -> Result<Option<T>, Error> {
|
|
if s.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(self.decode(s)?))
|
|
}
|
|
|
|
fn encode<'x>(&self, decoded: &'x Option<T>) -> Result<Option<Cow<'x, str>>, 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<const N: usize>;
|
|
|
|
impl<const N: usize> TextCodec<[u8; N]> for FixedHex<N> {
|
|
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<Option<Cow<'x, str>>, 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<T, const N: usize> TextCodec<Option<T>> for FixedHex<N>
|
|
where
|
|
FixedHex<N>: TextCodec<T>,
|
|
{
|
|
fn decode(&self, s: String) -> Result<Option<T>, Error> {
|
|
if s.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(self.decode(s)?))
|
|
}
|
|
|
|
fn encode<'x>(&self, decoded: &'x Option<T>) -> Result<Option<Cow<'x, str>>, 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<Vec<u8>> for ColonSeparatedHex {
|
|
fn decode(&self, s: String) -> Result<Vec<u8>, 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<u8>) -> Result<Option<Cow<'x, str>>, 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(":"))))
|
|
}
|
|
}
|