Refactored the "helpers" so that they use a common Codec trait; this makes them composable as well.

Werner Kroneman created

Change summary

parsers/src/avatar.rs                |   4 
parsers/src/bob.rs                   |   4 
parsers/src/cert_management.rs       |   4 
parsers/src/component.rs             |   4 
parsers/src/delay.rs                 |   4 
parsers/src/hashes.rs                |   4 
parsers/src/ibb.rs                   |   4 
parsers/src/jid_prep.rs              |   7 
parsers/src/jingle_dtls_srtp.rs      |   4 
parsers/src/legacy_omemo.rs          |  16 +-
parsers/src/media_element.rs         |   9 
parsers/src/openpgp.rs               |   4 
parsers/src/reactions.rs             |   4 
parsers/src/rtt.rs                   |   4 
parsers/src/sasl.rs                  |  10 
parsers/src/util/helpers.rs          | 171 ----------------------
parsers/src/util/macros.rs           |  16 +-
parsers/src/util/mod.rs              |   2 
parsers/src/util/text_node_codecs.rs | 232 ++++++++++++++++++++++++++++++
19 files changed, 285 insertions(+), 222 deletions(-)

Detailed changes

parsers/src/avatar.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use crate::hashes::Sha1HexAttribute;
 use crate::pubsub::PubSubPayload;
-use crate::util::helpers::WhitespaceAwareBase64;
+use crate::util::text_node_codecs::{Codec, WhitespaceAwareBase64};
 
 generate_element!(
     /// Communicates information about an avatar.
@@ -48,7 +48,7 @@ generate_element!(
     Data, "data", AVATAR_DATA,
     text: (
         /// Vector of bytes representing the avatar’s image.
-        data: WhitespaceAwareBase64<Vec<u8>>
+        data: WhitespaceAwareBase64
     )
 );
 

parsers/src/bob.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use crate::hashes::{Algo, Hash};
 use crate::util::error::Error;
-use crate::util::helpers::Base64;
+use crate::util::text_node_codecs::{Base64, Codec};
 use minidom::IntoAttributeValue;
 use std::str::FromStr;
 
@@ -81,7 +81,7 @@ generate_element!(
     ],
     text: (
         /// The actual data.
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 

parsers/src/cert_management.rs πŸ”—

@@ -5,7 +5,7 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
-use crate::util::helpers::Base64;
+use crate::util::text_node_codecs::{Base64, Codec};
 
 generate_elem_id!(
     /// The name of a certificate.
@@ -19,7 +19,7 @@ generate_element!(
     Cert, "x509cert", SASL_CERT,
     text: (
         /// The BER X.509 data.
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 

parsers/src/component.rs πŸ”—

@@ -4,7 +4,7 @@
 // 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/.
 
-use crate::util::helpers::PlainText;
+use crate::util::text_node_codecs::{Codec, OptionalCodec, Text};
 use digest::Digest;
 use sha1::Sha1;
 
@@ -19,7 +19,7 @@ generate_element!(
         ///
         /// If None, it is the successful reply from the server, the stream is now
         /// fully established and both sides can now exchange stanzas.
-        data: PlainText<Option<String>>
+        data: OptionalCodec<Text>
     )
 );
 

parsers/src/delay.rs πŸ”—

@@ -7,7 +7,7 @@
 use crate::date::DateTime;
 use crate::message::MessagePayload;
 use crate::presence::PresencePayload;
-use crate::util::helpers::PlainText;
+use crate::util::text_node_codecs::{Codec, OptionalCodec, Text};
 use jid::Jid;
 
 generate_element!(
@@ -22,7 +22,7 @@ generate_element!(
     ],
     text: (
         /// The optional reason this message got delayed.
-        data: PlainText<Option<String>>
+        data: OptionalCodec<Text>
     )
 );
 

parsers/src/hashes.rs πŸ”—

@@ -5,7 +5,7 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 use crate::util::error::Error;
-use crate::util::helpers::Base64;
+use crate::util::text_node_codecs::{Base64, Codec};
 use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine};
 use minidom::IntoAttributeValue;
 use std::num::ParseIntError;
@@ -105,7 +105,7 @@ generate_element!(
     ],
     text: (
         /// The hash value, as a vector of bytes.
-        hash: Base64<Vec<u8>>
+        hash: Base64
     )
 );
 

parsers/src/ibb.rs πŸ”—

@@ -5,7 +5,7 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 use crate::iq::IqSetPayload;
-use crate::util::helpers::Base64;
+use crate::util::text_node_codecs::{Base64, Codec};
 
 generate_id!(
     /// An identifier matching a stream.
@@ -53,7 +53,7 @@ Data, "data", IBB,
     ],
     text: (
         /// Vector of bytes to be exchanged.
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 

parsers/src/jid_prep.rs πŸ”—

@@ -5,15 +5,14 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 use crate::iq::{IqGetPayload, IqResultPayload};
-use crate::util::helpers::{JidCodec, Text};
-use jid::Jid;
+use crate::util::text_node_codecs::{Codec, JidCodec, Text};
 
 generate_element!(
     /// Request from a client to stringprep/PRECIS a string into a JID.
     JidPrepQuery, "jid", JID_PREP,
     text: (
         /// The potential JID.
-        data: Text<String>
+        data: Text
     )
 );
 
@@ -31,7 +30,7 @@ generate_element!(
     JidPrepResponse, "jid", JID_PREP,
     text: (
         /// The JID.
-        jid: JidCodec<Jid>
+        jid: JidCodec
     )
 );
 

parsers/src/jingle_dtls_srtp.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use crate::hashes::{Algo, Hash};
 use crate::util::error::Error;
-use crate::util::helpers::ColonSeparatedHex;
+use crate::util::text_node_codecs::{Codec, ColonSeparatedHex};
 
 generate_attribute!(
     /// Indicates which of the end points should initiate the TCP connection establishment.
@@ -43,7 +43,7 @@ generate_element!(
     ],
     text: (
         /// Hash value of this fingerprint.
-        value: ColonSeparatedHex<Vec<u8>>
+        value: ColonSeparatedHex
     )
 );
 

parsers/src/legacy_omemo.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use crate::message::MessagePayload;
 use crate::pubsub::PubSubPayload;
-use crate::util::helpers::Base64;
+use crate::util::text_node_codecs::{Base64, Codec};
 
 generate_element!(
     /// Element of the device list
@@ -39,7 +39,7 @@ generate_element!(
     ],
     text: (
         /// Serialized PublicKey
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -49,7 +49,7 @@ generate_element!(
     SignedPreKeySignature, "signedPreKeySignature", LEGACY_OMEMO,
     text: (
         /// Signature bytes
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -58,7 +58,7 @@ generate_element!(
     IdentityKey, "identityKey", LEGACY_OMEMO,
     text: (
         /// Serialized PublicKey
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -82,7 +82,7 @@ generate_element!(
     ],
     text: (
         /// Serialized PublicKey
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -125,7 +125,7 @@ generate_element!(
     IV, "iv", LEGACY_OMEMO,
     text: (
         /// IV bytes
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -151,7 +151,7 @@ generate_element!(
         /// The 16 bytes key and the GCM authentication tag concatenated together
         /// and encrypted using the corresponding long-standing SignalProtocol
         /// session
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -160,7 +160,7 @@ generate_element!(
     Payload, "payload", LEGACY_OMEMO,
     text: (
         /// Encrypted with AES-128 in Galois/Counter Mode (GCM)
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 

parsers/src/media_element.rs πŸ”—

@@ -4,7 +4,7 @@
 // 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/.
 
-use crate::util::helpers::TrimmedPlainText;
+use crate::util::text_node_codecs::{Codec, Text, Trimmed};
 
 generate_element!(
     /// Represents an URI used in a media element.
@@ -21,7 +21,7 @@ generate_element!(
     ],
     text: (
         /// The actual URI contained.
-        uri: TrimmedPlainText<String>
+        uri: Trimmed<Text>
     )
 );
 
@@ -168,7 +168,10 @@ mod tests {
             Error::ParseError(string) => string,
             _ => panic!(),
         };
-        assert_eq!(message, "URI missing in uri.");
+        assert_eq!(
+            message,
+            "The text in the element's text node was empty after trimming."
+        );
     }
 
     #[test]

parsers/src/openpgp.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use crate::date::DateTime;
 use crate::pubsub::PubSubPayload;
-use crate::util::helpers::Base64;
+use crate::util::text_node_codecs::{Base64, Codec};
 
 // TODO: Merge this container with the PubKey struct
 generate_element!(
@@ -14,7 +14,7 @@ generate_element!(
     PubKeyData, "data", OX,
     text: (
         /// Base64 data
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 

parsers/src/reactions.rs πŸ”—

@@ -5,7 +5,7 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 use crate::message::MessagePayload;
-use crate::util::helpers::Text;
+use crate::util::text_node_codecs::{Codec, Text};
 
 generate_element!(
     /// Container for a set of reactions.
@@ -27,7 +27,7 @@ generate_element!(
     Reaction, "reaction", REACTIONS,
     text: (
         /// The text of this reaction.
-        emoji: Text<String>
+        emoji: Text
     )
 );
 

parsers/src/rtt.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use crate::ns;
 use crate::util::error::Error;
-use crate::util::helpers::PlainText;
+use crate::util::text_node_codecs::{Codec, OptionalCodec, Text};
 use crate::Element;
 
 generate_attribute!(
@@ -39,7 +39,7 @@ generate_element!(
     ],
     text: (
         /// Text to insert.
-        text: PlainText<Option<String>>
+        text: OptionalCodec<Text>
     )
 );
 

parsers/src/sasl.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use crate::ns;
 use crate::util::error::Error;
-use crate::util::helpers::Base64;
+use crate::util::text_node_codecs::{Base64, Codec};
 use crate::Element;
 use std::collections::BTreeMap;
 
@@ -52,7 +52,7 @@ generate_element!(
     ],
     text: (
         /// The content of the handshake.
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -63,7 +63,7 @@ generate_element!(
     Challenge, "challenge", SASL,
     text: (
         /// The challenge data.
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -74,7 +74,7 @@ generate_element!(
     Response, "response", SASL,
     text: (
         /// The response data.
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 
@@ -91,7 +91,7 @@ generate_element!(
     Success, "success", SASL,
     text: (
         /// Possible data sent on success.
-        data: Base64<Vec<u8>>
+        data: Base64
     )
 );
 

parsers/src/util/helpers.rs πŸ”—

@@ -1,171 +0,0 @@
-// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
-//
-// 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/.
-
-use crate::util::error::Error;
-use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine};
-use jid::Jid;
-use std::str::FromStr;
-
-/// Codec for text content.
-pub struct Text;
-
-impl Text {
-    pub fn decode(s: &str) -> Result<String, Error> {
-        Ok(s.to_owned())
-    }
-
-    pub fn encode(string: &str) -> Option<String> {
-        Some(string.to_owned())
-    }
-}
-
-/// Codec for plain text content.
-pub struct PlainText;
-
-impl PlainText {
-    pub fn decode(s: &str) -> Result<Option<String>, Error> {
-        Ok(match s {
-            "" => None,
-            text => Some(text.to_owned()),
-        })
-    }
-
-    pub fn encode(string: &Option<String>) -> Option<String> {
-        string.as_ref().map(ToOwned::to_owned)
-    }
-}
-
-/// Codec for trimmed plain text content.
-pub struct TrimmedPlainText;
-
-impl TrimmedPlainText {
-    pub fn decode(s: &str) -> Result<String, Error> {
-        Ok(match s.trim() {
-            "" => return Err(Error::ParseError("URI missing in uri.")),
-            text => text.to_owned(),
-        })
-    }
-
-    pub fn encode(string: &str) -> Option<String> {
-        Some(string.to_owned())
-    }
-}
-
-/// Codec wrapping base64 encode/decode.
-pub struct Base64;
-
-impl Base64 {
-    pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
-        Ok(Base64Engine.decode(s)?)
-    }
-
-    pub fn encode(b: &[u8]) -> Option<String> {
-        Some(Base64Engine.encode(b))
-    }
-}
-
-/// Codec wrapping base64 encode/decode, while ignoring whitespace characters.
-pub struct WhitespaceAwareBase64;
-
-impl WhitespaceAwareBase64 {
-    pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
-        let s: String = s
-            .chars()
-            .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
-            .collect();
-        Ok(Base64Engine.decode(s)?)
-    }
-
-    pub fn encode(b: &[u8]) -> Option<String> {
-        Some(Base64Engine.encode(b))
-    }
-}
-
-/// Codec for bytes of lowercase hexadecimal.
-pub struct Hex;
-
-impl Hex {
-    pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
-        let mut bytes = Vec::with_capacity(s.len() / 2);
-        for i in 0..s.len() / 2 {
-            bytes.push(u8::from_str_radix(&s[2 * i..2 * i + 2], 16)?);
-        }
-        Ok(bytes)
-    }
-
-    pub fn encode(b: &[u8]) -> Option<String> {
-        let mut bytes = String::with_capacity(b.len() * 2);
-        for byte in b {
-            bytes.extend(format!("{:02x}", byte).chars());
-        }
-        Some(bytes)
-    }
-}
-
-/// Codec for colon-separated bytes of uppercase hexadecimal.
-pub struct ColonSeparatedHex;
-
-impl ColonSeparatedHex {
-    pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
-        let mut bytes = vec![];
-        for i in 0..(1 + s.len()) / 3 {
-            let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16)?;
-            if 3 * i + 2 < s.len() {
-                assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
-            }
-            bytes.push(byte);
-        }
-        Ok(bytes)
-    }
-
-    pub fn encode(b: &[u8]) -> Option<String> {
-        let mut bytes = vec![];
-        for byte in b {
-            bytes.push(format!("{:02X}", byte));
-        }
-        Some(bytes.join(":"))
-    }
-}
-
-/// Codec for a JID.
-pub struct JidCodec;
-
-impl JidCodec {
-    pub fn decode(s: &str) -> Result<Jid, Error> {
-        Ok(Jid::from_str(s)?)
-    }
-
-    pub fn encode(jid: &Jid) -> Option<String> {
-        Some(jid.to_string())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn hex() {
-        let value = [0x01, 0xfe, 0xef];
-
-        // Test that we support both lowercase and uppercase as input.
-        let hex = Hex::decode("01feEF").unwrap();
-        assert_eq!(hex, &value);
-
-        // Test that we do output lowercase.
-        let hex = Hex::encode(&value).unwrap();
-        assert_eq!(hex, "01feef");
-    }
-
-    #[test]
-    fn bad_hex() {
-        // No colon supported.
-        Hex::decode("01:fe:EF").unwrap_err();
-
-        // No non-hex character allowed.
-        Hex::decode("01defg").unwrap_err();
-    }
-}

parsers/src/util/macros.rs πŸ”—

@@ -618,13 +618,13 @@ macro_rules! generate_element {
     ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),+$(,)?]) => (
         generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]);
     );
-    ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => (
-        generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>));
+    ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ty )) => (
+        generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [], text: ($(#[$text_meta])* $text_ident: $codec));
     );
-    ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+$(,)?], text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => (
-        generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>));
+    ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+$(,)?], text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ty )) => (
+        generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [], text: ($(#[$text_meta])* $text_ident: $codec));
     );
-    ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*$(,)?], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*$(,)?] $(, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >))*) => (
+    ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*$(,)?], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*$(,)?] $(, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ty ))*) => (
         $(#[$meta])*
         #[derive(Debug, Clone, PartialEq)]
         pub struct $elem {
@@ -638,7 +638,7 @@ macro_rules! generate_element {
             )*
             $(
                 $(#[$text_meta])*
-                pub $text_ident: $text_type,
+                pub $text_ident: <$codec as Codec>::Decoded,
             )*
         }
 
@@ -668,7 +668,7 @@ macro_rules! generate_element {
                         $child_ident: finish_parse_elem!($child_ident: $coucou = $child_name, $name),
                     )*
                     $(
-                        $text_ident: $codec::decode(&elem.text())?,
+                        $text_ident: <$codec>::decode(&elem.text())?,
                     )*
                 })
             }
@@ -684,7 +684,7 @@ macro_rules! generate_element {
                     builder = generate_serialiser!(builder, elem, $child_ident, $coucou, $child_constructor, ($child_name, $child_ns));
                 )*
                 $(
-                    builder = builder.append_all($codec::encode(&elem.$text_ident).map(::minidom::Node::Text).into_iter());
+                    builder = builder.append_all(<$codec>::encode(&elem.$text_ident).map(::minidom::Node::Text).into_iter());
                 )*
 
                 builder.build()

parsers/src/util/mod.rs πŸ”—

@@ -8,7 +8,7 @@
 pub mod error;
 
 /// Various helpers.
-pub(crate) mod helpers;
+pub(crate) mod text_node_codecs;
 
 /// Helper macros to parse and serialise more easily.
 #[macro_use]

parsers/src/util/text_node_codecs.rs πŸ”—

@@ -0,0 +1,232 @@
+// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// 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/.
+
+use crate::util::error::Error;
+use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine};
+use jid::Jid;
+use std::str::FromStr;
+
+/// A trait for codecs that can decode and encode text nodes.
+pub trait Codec {
+    type Decoded;
+
+    /// Decode the given string into the codec’s output.
+    fn decode(s: &str) -> Result<Self::Decoded, Error>;
+
+    /// Encode the given value; return None to not produce a text node at all.
+    fn encode(decoded: &Self::Decoded) -> Option<String>;
+}
+
+/// Codec for text content.
+pub struct Text;
+
+impl Codec for Text {
+    type Decoded = String;
+
+    fn decode(s: &str) -> Result<String, Error> {
+        Ok(s.to_owned())
+    }
+
+    fn encode(decoded: &String) -> Option<String> {
+        Some(decoded.to_owned())
+    }
+}
+
+/// Codec transformer that makes the text optional; a "" string is decoded as None.
+pub struct OptionalCodec<T: Codec>(std::marker::PhantomData<T>);
+
+impl<T> Codec for OptionalCodec<T>
+where
+    T: Codec,
+{
+    type Decoded = Option<T::Decoded>;
+
+    fn decode(s: &str) -> Result<Option<T::Decoded>, Error> {
+        if s.is_empty() {
+            return Ok(None);
+        }
+
+        Ok(Some(T::decode(s)?))
+    }
+
+    fn encode(decoded: &Option<T::Decoded>) -> Option<String> {
+        decoded.as_ref().and_then(T::encode)
+    }
+}
+
+/// Codec that trims whitespace around the text.
+pub struct Trimmed<T: Codec>(std::marker::PhantomData<T>);
+
+impl<T> Codec for Trimmed<T>
+where
+    T: Codec,
+{
+    type Decoded = T::Decoded;
+
+    fn decode(s: &str) -> Result<T::Decoded, Error> {
+        match s.trim() {
+            // TODO: This error message can be a bit opaque when used
+            // in-context; ideally it'd be configurable.
+            "" => Err(Error::ParseError(
+                "The text in the element's text node was empty after trimming.",
+            )),
+            trimmed => T::decode(trimmed),
+        }
+    }
+
+    fn encode(decoded: &T::Decoded) -> Option<String> {
+        T::encode(decoded)
+    }
+}
+
+/// Codec wrapping that encodes/decodes a string as base64.
+pub struct Base64;
+
+impl Codec for Base64 {
+    type Decoded = Vec<u8>;
+
+    fn decode(s: &str) -> Result<Vec<u8>, Error> {
+        Ok(Base64Engine.decode(s)?)
+    }
+
+    fn encode(decoded: &Vec<u8>) -> Option<String> {
+        Some(Base64Engine.encode(decoded))
+    }
+}
+
+/// Codec wrapping base64 encode/decode, while ignoring whitespace characters.
+pub struct WhitespaceAwareBase64;
+
+impl Codec for WhitespaceAwareBase64 {
+    type Decoded = Vec<u8>;
+
+    fn decode(s: &str) -> Result<Self::Decoded, Error> {
+        let s: String = s
+            .chars()
+            .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
+            .collect();
+
+        Ok(Base64Engine.decode(s)?)
+    }
+
+    fn encode(decoded: &Self::Decoded) -> Option<String> {
+        Some(Base64Engine.encode(decoded))
+    }
+}
+
+/// Codec for bytes of lowercase hexadecimal, with a fixed length `N` (in bytes).
+pub struct FixedHex<const N: usize>;
+
+impl<const N: usize> Codec for FixedHex<N> {
+    type Decoded = [u8; N];
+
+    fn decode(s: &str) -> Result<Self::Decoded, Error> {
+        if s.len() != 2 * N {
+            return Err(Error::ParseError("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)?;
+        }
+
+        Ok(bytes)
+    }
+
+    fn encode(decoded: &Self::Decoded) -> Option<String> {
+        let mut bytes = String::with_capacity(N * 2);
+        for byte in decoded {
+            bytes.extend(format!("{:02x}", byte).chars());
+        }
+        Some(bytes)
+    }
+}
+
+/// Codec for colon-separated bytes of uppercase hexadecimal.
+pub struct ColonSeparatedHex;
+
+impl Codec for ColonSeparatedHex {
+    type Decoded = Vec<u8>;
+
+    fn decode(s: &str) -> Result<Self::Decoded, Error> {
+        let mut bytes = vec![];
+        for i in 0..(1 + s.len()) / 3 {
+            let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16)?;
+            if 3 * i + 2 < s.len() {
+                assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
+            }
+            bytes.push(byte);
+        }
+        Ok(bytes)
+    }
+
+    fn encode(decoded: &Self::Decoded) -> Option<String> {
+        let mut bytes = vec![];
+        for byte in decoded {
+            bytes.push(format!("{:02X}", byte));
+        }
+        Some(bytes.join(":"))
+    }
+}
+
+/// Codec for a JID.
+pub struct JidCodec;
+
+impl Codec for JidCodec {
+    type Decoded = Jid;
+
+    fn decode(s: &str) -> Result<Jid, Error> {
+        Ok(Jid::from_str(s)?)
+    }
+
+    fn encode(jid: &Jid) -> Option<String> {
+        Some(jid.to_string())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn fixed_hex() {
+        let value = [0x01, 0xfe, 0xef];
+
+        // Test that we support both lowercase and uppercase as input.
+        let hex = FixedHex::<3>::decode("01feEF").unwrap();
+        assert_eq!(&hex, &value);
+
+        // Test that we do output lowercase.
+        let hex = FixedHex::<3>::encode(&value).unwrap();
+        assert_eq!(hex, "01feef");
+
+        // What if we give it a string that's too long?
+        let err = FixedHex::<3>::decode("01feEF01").unwrap_err();
+        assert_eq!(err.to_string(), "parse error: Invalid length");
+
+        // Too short?
+        let err = FixedHex::<3>::decode("01fe").unwrap_err();
+        assert_eq!(err.to_string(), "parse error: Invalid length");
+
+        // Not-even numbers?
+        let err = FixedHex::<3>::decode("01feE").unwrap_err();
+        assert_eq!(err.to_string(), "parse error: Invalid length");
+
+        // No colon supported.
+        let err = FixedHex::<3>::decode("0:f:EF").unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "integer parsing error: invalid digit found in string"
+        );
+
+        // No non-hex character allowed.
+        let err = FixedHex::<3>::decode("01defg").unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "integer parsing error: invalid digit found in string"
+        );
+    }
+}