text.rs

  1// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7//! Module containing implementations for conversions to/from XML text.
  8
  9#[cfg(feature = "base64")]
 10use core::marker::PhantomData;
 11
 12use std::borrow::Cow;
 13
 14use crate::{error::Error, AsXmlText, FromXmlText};
 15
 16#[cfg(feature = "base64")]
 17use base64::engine::{general_purpose::STANDARD as StandardBase64Engine, Engine as _};
 18#[cfg(feature = "jid")]
 19use jid;
 20#[cfg(feature = "uuid")]
 21use uuid;
 22
 23macro_rules! convert_via_fromstr_and_display {
 24    ($($(#[cfg(feature = $feature:literal)])?$t:ty,)+) => {
 25        $(
 26            $(
 27                #[cfg(feature = $feature)]
 28                #[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
 29            )?
 30            impl FromXmlText for $t {
 31                fn from_xml_text(s: String) -> Result<Self, Error> {
 32                    s.parse().map_err(Error::text_parse_error)
 33                }
 34            }
 35
 36            $(
 37                #[cfg(feature = $feature)]
 38                #[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
 39            )?
 40            impl AsXmlText for $t {
 41                fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
 42                    Ok(Cow::Owned(self.to_string()))
 43                }
 44            }
 45        )+
 46    }
 47}
 48
 49/// This provides an implementation compliant with xsd::bool.
 50impl FromXmlText for bool {
 51    fn from_xml_text(s: String) -> Result<Self, Error> {
 52        match s.as_str() {
 53            "1" => "true",
 54            "0" => "false",
 55            other => other,
 56        }
 57        .parse()
 58        .map_err(Error::text_parse_error)
 59    }
 60}
 61
 62/// This provides an implementation compliant with xsd::bool.
 63impl AsXmlText for bool {
 64    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
 65        match self {
 66            true => Ok(Cow::Borrowed("true")),
 67            false => Ok(Cow::Borrowed("false")),
 68        }
 69    }
 70}
 71
 72convert_via_fromstr_and_display! {
 73    u8,
 74    u16,
 75    u32,
 76    u64,
 77    u128,
 78    usize,
 79    i8,
 80    i16,
 81    i32,
 82    i64,
 83    i128,
 84    isize,
 85    f32,
 86    f64,
 87    std::net::IpAddr,
 88    std::net::Ipv4Addr,
 89    std::net::Ipv6Addr,
 90    std::net::SocketAddr,
 91    std::net::SocketAddrV4,
 92    std::net::SocketAddrV6,
 93    std::num::NonZeroU8,
 94    std::num::NonZeroU16,
 95    std::num::NonZeroU32,
 96    std::num::NonZeroU64,
 97    std::num::NonZeroU128,
 98    std::num::NonZeroUsize,
 99    std::num::NonZeroI8,
100    std::num::NonZeroI16,
101    std::num::NonZeroI32,
102    std::num::NonZeroI64,
103    std::num::NonZeroI128,
104    std::num::NonZeroIsize,
105
106    #[cfg(feature = "uuid")]
107    uuid::Uuid,
108
109    #[cfg(feature = "jid")]
110    jid::Jid,
111    #[cfg(feature = "jid")]
112    jid::FullJid,
113    #[cfg(feature = "jid")]
114    jid::BareJid,
115}
116
117/// Represent a way to encode/decode text data into a Rust type.
118///
119/// This trait can be used in scenarios where implementing [`FromXmlText`]
120/// and/or [`AsXmlText`] on a type is not feasible or sensible, such as the
121/// following:
122///
123/// 1. The type originates in a foreign crate, preventing the implementation
124///    of foreign traits.
125///
126/// 2. There is more than one way to convert a value to/from XML.
127///
128/// The codec to use for a text can be specified in the attributes understood
129/// by `FromXml` and `AsXml` derive macros. See the documentation of the
130/// [`FromXml`][`macro@crate::FromXml`] derive macro for details.
131pub trait TextCodec<T> {
132    /// Decode a string value into the type.
133    fn decode(s: String) -> Result<T, Error>;
134
135    /// Encode the type as string value.
136    ///
137    /// If this returns `None`, the string value is not emitted at all.
138    fn encode(value: &T) -> Result<Option<Cow<'_, str>>, Error>;
139}
140
141/// Text codec which does no transform.
142pub struct Plain;
143
144impl TextCodec<String> for Plain {
145    fn decode(s: String) -> Result<String, Error> {
146        Ok(s)
147    }
148
149    fn encode(value: &String) -> Result<Option<Cow<'_, str>>, Error> {
150        Ok(Some(Cow::Borrowed(value.as_str())))
151    }
152}
153
154/// Text codec which returns None instead of the empty string.
155pub struct EmptyAsNone;
156
157impl TextCodec<Option<String>> for EmptyAsNone {
158    fn decode(s: String) -> Result<Option<String>, Error> {
159        if s.is_empty() {
160            Ok(None)
161        } else {
162            Ok(Some(s))
163        }
164    }
165
166    fn encode(value: &Option<String>) -> Result<Option<Cow<'_, str>>, Error> {
167        Ok(match value.as_ref() {
168            Some(v) if !v.is_empty() => Some(Cow::Borrowed(v.as_str())),
169            Some(_) | None => None,
170        })
171    }
172}
173
174/// Text codec which returns None instead of the empty string.
175pub struct EmptyAsError;
176
177impl TextCodec<String> for EmptyAsError {
178    fn decode(s: String) -> Result<String, Error> {
179        if s.is_empty() {
180            Err(Error::Other("Empty text node."))
181        } else {
182            Ok(s)
183        }
184    }
185
186    fn encode(value: &String) -> Result<Option<Cow<'_, str>>, Error> {
187        if value.is_empty() {
188            Err(Error::Other("Empty text node."))
189        } else {
190            Ok(Some(Cow::Borrowed(value.as_str())))
191        }
192    }
193}
194
195/// Trait for preprocessing text data from XML.
196///
197/// This may be used by codecs to allow to customize some of their behaviour.
198pub trait TextFilter {
199    /// Process the incoming string and return the result of the processing.
200    fn preprocess(s: String) -> String;
201}
202
203/// Text preprocessor which returns the input unchanged.
204pub struct NoFilter;
205
206impl TextFilter for NoFilter {
207    fn preprocess(s: String) -> String {
208        s
209    }
210}
211
212/// Text preprocessor to remove all whitespace.
213pub struct StripWhitespace;
214
215impl TextFilter for StripWhitespace {
216    fn preprocess(s: String) -> String {
217        let s: String = s
218            .chars()
219            .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
220            .collect();
221        s
222    }
223}
224
225/// Text codec transforming text to binary using standard base64.
226///
227/// The `Filter` type argument can be used to employ additional preprocessing
228/// of incoming text data. Most interestingly, passing [`StripWhitespace`]
229/// will make the implementation ignore any whitespace within the text.
230#[cfg(feature = "base64")]
231#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
232pub struct Base64<Filter: TextFilter = NoFilter>(PhantomData<Filter>);
233
234#[cfg(feature = "base64")]
235#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
236impl<Filter: TextFilter> TextCodec<Vec<u8>> for Base64<Filter> {
237    fn decode(s: String) -> Result<Vec<u8>, Error> {
238        let value = Filter::preprocess(s);
239        StandardBase64Engine
240            .decode(value.as_bytes())
241            .map_err(Error::text_parse_error)
242    }
243
244    fn encode(value: &Vec<u8>) -> Result<Option<Cow<'_, str>>, Error> {
245        Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value))))
246    }
247}
248
249#[cfg(feature = "base64")]
250#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
251impl<Filter: TextFilter> TextCodec<Option<Vec<u8>>> for Base64<Filter> {
252    fn decode(s: String) -> Result<Option<Vec<u8>>, Error> {
253        if s.is_empty() {
254            return Ok(None);
255        }
256        Ok(Some(Self::decode(s)?))
257    }
258
259    fn encode(decoded: &Option<Vec<u8>>) -> Result<Option<Cow<'_, str>>, Error> {
260        decoded
261            .as_ref()
262            .map(Self::encode)
263            .transpose()
264            .map(Option::flatten)
265    }
266}
267
268/// Text codec transforming text to binary using hexadecimal nibbles.
269///
270/// The length must be known at compile-time.
271pub struct FixedHex<const N: usize>;
272
273impl<const N: usize> TextCodec<[u8; N]> for FixedHex<N> {
274    fn decode(s: String) -> Result<[u8; N], Error> {
275        if s.len() != 2 * N {
276            return Err(Error::Other("Invalid length"));
277        }
278
279        let mut bytes = [0u8; N];
280        for i in 0..N {
281            bytes[i] =
282                u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(Error::text_parse_error)?;
283        }
284
285        Ok(bytes)
286    }
287
288    fn encode(value: &[u8; N]) -> Result<Option<Cow<'_, str>>, Error> {
289        let mut bytes = String::with_capacity(N * 2);
290        for byte in value {
291            bytes.extend(format!("{:02x}", byte).chars());
292        }
293        Ok(Some(Cow::Owned(bytes)))
294    }
295}
296
297impl<T, const N: usize> TextCodec<Option<T>> for FixedHex<N>
298where
299    FixedHex<N>: TextCodec<T>,
300{
301    fn decode(s: String) -> Result<Option<T>, Error> {
302        if s.is_empty() {
303            return Ok(None);
304        }
305        Ok(Some(Self::decode(s)?))
306    }
307
308    fn encode(decoded: &Option<T>) -> Result<Option<Cow<'_, str>>, Error> {
309        decoded
310            .as_ref()
311            .map(Self::encode)
312            .transpose()
313            .map(Option::flatten)
314    }
315}
316
317/// Text codec for colon-separated bytes of uppercase hexadecimal.
318pub struct ColonSeparatedHex;
319
320impl TextCodec<Vec<u8>> for ColonSeparatedHex {
321    fn decode(s: String) -> Result<Vec<u8>, Error> {
322        assert_eq!((s.len() + 1) % 3, 0);
323        let mut bytes = Vec::with_capacity((s.len() + 1) / 3);
324        for i in 0..(1 + s.len()) / 3 {
325            let byte =
326                u8::from_str_radix(&s[3 * i..3 * i + 2], 16).map_err(Error::text_parse_error)?;
327            if 3 * i + 2 < s.len() {
328                assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
329            }
330            bytes.push(byte);
331        }
332        Ok(bytes)
333    }
334
335    fn encode(decoded: &Vec<u8>) -> Result<Option<Cow<'_, str>>, Error> {
336        // TODO: Super inefficient!
337        let mut bytes = Vec::with_capacity(decoded.len());
338        for byte in decoded {
339            bytes.push(format!("{:02X}", byte));
340        }
341        Ok(Some(Cow::Owned(bytes.join(":"))))
342    }
343}