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}