diff --git a/parsers/src/avatar.rs b/parsers/src/avatar.rs index 1a334a1..8aef7c8 100644 --- a/parsers/src/avatar.rs +++ b/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> + data: WhitespaceAwareBase64 ) ); diff --git a/parsers/src/bob.rs b/parsers/src/bob.rs index 8e34ef4..79c4de5 100644 --- a/parsers/src/bob.rs +++ b/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> + data: Base64 ) ); diff --git a/parsers/src/cert_management.rs b/parsers/src/cert_management.rs index c53e774..1e33dc1 100644 --- a/parsers/src/cert_management.rs +++ b/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> + data: Base64 ) ); diff --git a/parsers/src/component.rs b/parsers/src/component.rs index c732933..6bee142 100644 --- a/parsers/src/component.rs +++ b/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> + data: OptionalCodec ) ); diff --git a/parsers/src/delay.rs b/parsers/src/delay.rs index 028ce57..64d7f49 100644 --- a/parsers/src/delay.rs +++ b/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> + data: OptionalCodec ) ); diff --git a/parsers/src/hashes.rs b/parsers/src/hashes.rs index a1e0cef..b899b4a 100644 --- a/parsers/src/hashes.rs +++ b/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> + hash: Base64 ) ); diff --git a/parsers/src/ibb.rs b/parsers/src/ibb.rs index 32f9f08..6ff88ea 100644 --- a/parsers/src/ibb.rs +++ b/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> + data: Base64 ) ); diff --git a/parsers/src/jid_prep.rs b/parsers/src/jid_prep.rs index a6f0f80..1d26a20 100644 --- a/parsers/src/jid_prep.rs +++ b/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 + data: Text ) ); @@ -31,7 +30,7 @@ generate_element!( JidPrepResponse, "jid", JID_PREP, text: ( /// The JID. - jid: JidCodec + jid: JidCodec ) ); diff --git a/parsers/src/jingle_dtls_srtp.rs b/parsers/src/jingle_dtls_srtp.rs index 21d193a..dcf1924 100644 --- a/parsers/src/jingle_dtls_srtp.rs +++ b/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> + value: ColonSeparatedHex ) ); diff --git a/parsers/src/legacy_omemo.rs b/parsers/src/legacy_omemo.rs index cf96914..e2920f0 100644 --- a/parsers/src/legacy_omemo.rs +++ b/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> + data: Base64 ) ); @@ -49,7 +49,7 @@ generate_element!( SignedPreKeySignature, "signedPreKeySignature", LEGACY_OMEMO, text: ( /// Signature bytes - data: Base64> + data: Base64 ) ); @@ -58,7 +58,7 @@ generate_element!( IdentityKey, "identityKey", LEGACY_OMEMO, text: ( /// Serialized PublicKey - data: Base64> + data: Base64 ) ); @@ -82,7 +82,7 @@ generate_element!( ], text: ( /// Serialized PublicKey - data: Base64> + data: Base64 ) ); @@ -125,7 +125,7 @@ generate_element!( IV, "iv", LEGACY_OMEMO, text: ( /// IV bytes - data: Base64> + 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> + data: Base64 ) ); @@ -160,7 +160,7 @@ generate_element!( Payload, "payload", LEGACY_OMEMO, text: ( /// Encrypted with AES-128 in Galois/Counter Mode (GCM) - data: Base64> + data: Base64 ) ); diff --git a/parsers/src/media_element.rs b/parsers/src/media_element.rs index cfe3250..8f3fed5 100644 --- a/parsers/src/media_element.rs +++ b/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 + uri: Trimmed ) ); @@ -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] diff --git a/parsers/src/openpgp.rs b/parsers/src/openpgp.rs index b862e3f..db3f704 100644 --- a/parsers/src/openpgp.rs +++ b/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> + data: Base64 ) ); diff --git a/parsers/src/reactions.rs b/parsers/src/reactions.rs index dd241e4..7638f0d 100644 --- a/parsers/src/reactions.rs +++ b/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 + emoji: Text ) ); diff --git a/parsers/src/rtt.rs b/parsers/src/rtt.rs index adacfdd..02fcec3 100644 --- a/parsers/src/rtt.rs +++ b/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> + text: OptionalCodec ) ); diff --git a/parsers/src/sasl.rs b/parsers/src/sasl.rs index f672182..3ce3bff 100644 --- a/parsers/src/sasl.rs +++ b/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> + data: Base64 ) ); @@ -63,7 +63,7 @@ generate_element!( Challenge, "challenge", SASL, text: ( /// The challenge data. - data: Base64> + data: Base64 ) ); @@ -74,7 +74,7 @@ generate_element!( Response, "response", SASL, text: ( /// The response data. - data: Base64> + data: Base64 ) ); @@ -91,7 +91,7 @@ generate_element!( Success, "success", SASL, text: ( /// Possible data sent on success. - data: Base64> + data: Base64 ) ); diff --git a/parsers/src/util/helpers.rs b/parsers/src/util/helpers.rs deleted file mode 100644 index 752dcfe..0000000 --- a/parsers/src/util/helpers.rs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) 2017 Emmanuel Gil Peyrot -// -// 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 { - Ok(s.to_owned()) - } - - pub fn encode(string: &str) -> Option { - Some(string.to_owned()) - } -} - -/// Codec for plain text content. -pub struct PlainText; - -impl PlainText { - pub fn decode(s: &str) -> Result, Error> { - Ok(match s { - "" => None, - text => Some(text.to_owned()), - }) - } - - pub fn encode(string: &Option) -> Option { - string.as_ref().map(ToOwned::to_owned) - } -} - -/// Codec for trimmed plain text content. -pub struct TrimmedPlainText; - -impl TrimmedPlainText { - pub fn decode(s: &str) -> Result { - Ok(match s.trim() { - "" => return Err(Error::ParseError("URI missing in uri.")), - text => text.to_owned(), - }) - } - - pub fn encode(string: &str) -> Option { - Some(string.to_owned()) - } -} - -/// Codec wrapping base64 encode/decode. -pub struct Base64; - -impl Base64 { - pub fn decode(s: &str) -> Result, Error> { - Ok(Base64Engine.decode(s)?) - } - - pub fn encode(b: &[u8]) -> Option { - Some(Base64Engine.encode(b)) - } -} - -/// Codec wrapping base64 encode/decode, while ignoring whitespace characters. -pub struct WhitespaceAwareBase64; - -impl WhitespaceAwareBase64 { - pub fn decode(s: &str) -> Result, Error> { - let s: String = s - .chars() - .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t') - .collect(); - Ok(Base64Engine.decode(s)?) - } - - pub fn encode(b: &[u8]) -> Option { - Some(Base64Engine.encode(b)) - } -} - -/// Codec for bytes of lowercase hexadecimal. -pub struct Hex; - -impl Hex { - pub fn decode(s: &str) -> Result, 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 { - 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, 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 { - 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 { - Ok(Jid::from_str(s)?) - } - - pub fn encode(jid: &Jid) -> Option { - 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(); - } -} diff --git a/parsers/src/util/macros.rs b/parsers/src/util/macros.rs index ed09410..f7021d0 100644 --- a/parsers/src/util/macros.rs +++ b/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() diff --git a/parsers/src/util/mod.rs b/parsers/src/util/mod.rs index 1d3ee64..8e80667 100644 --- a/parsers/src/util/mod.rs +++ b/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] diff --git a/parsers/src/util/text_node_codecs.rs b/parsers/src/util/text_node_codecs.rs new file mode 100644 index 0000000..e45dec2 --- /dev/null +++ b/parsers/src/util/text_node_codecs.rs @@ -0,0 +1,232 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// 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; + + /// Encode the given value; return None to not produce a text node at all. + fn encode(decoded: &Self::Decoded) -> Option; +} + +/// Codec for text content. +pub struct Text; + +impl Codec for Text { + type Decoded = String; + + fn decode(s: &str) -> Result { + Ok(s.to_owned()) + } + + fn encode(decoded: &String) -> Option { + Some(decoded.to_owned()) + } +} + +/// Codec transformer that makes the text optional; a "" string is decoded as None. +pub struct OptionalCodec(std::marker::PhantomData); + +impl Codec for OptionalCodec +where + T: Codec, +{ + type Decoded = Option; + + fn decode(s: &str) -> Result, Error> { + if s.is_empty() { + return Ok(None); + } + + Ok(Some(T::decode(s)?)) + } + + fn encode(decoded: &Option) -> Option { + decoded.as_ref().and_then(T::encode) + } +} + +/// Codec that trims whitespace around the text. +pub struct Trimmed(std::marker::PhantomData); + +impl Codec for Trimmed +where + T: Codec, +{ + type Decoded = T::Decoded; + + fn decode(s: &str) -> Result { + 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 { + T::encode(decoded) + } +} + +/// Codec wrapping that encodes/decodes a string as base64. +pub struct Base64; + +impl Codec for Base64 { + type Decoded = Vec; + + fn decode(s: &str) -> Result, Error> { + Ok(Base64Engine.decode(s)?) + } + + fn encode(decoded: &Vec) -> Option { + Some(Base64Engine.encode(decoded)) + } +} + +/// Codec wrapping base64 encode/decode, while ignoring whitespace characters. +pub struct WhitespaceAwareBase64; + +impl Codec for WhitespaceAwareBase64 { + type Decoded = Vec; + + fn decode(s: &str) -> Result { + let s: String = s + .chars() + .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t') + .collect(); + + Ok(Base64Engine.decode(s)?) + } + + fn encode(decoded: &Self::Decoded) -> Option { + Some(Base64Engine.encode(decoded)) + } +} + +/// Codec for bytes of lowercase hexadecimal, with a fixed length `N` (in bytes). +pub struct FixedHex; + +impl Codec for FixedHex { + type Decoded = [u8; N]; + + fn decode(s: &str) -> Result { + 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 { + 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; + + fn decode(s: &str) -> Result { + 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 { + 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 { + Ok(Jid::from_str(s)?) + } + + fn encode(jid: &Jid) -> Option { + 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" + ); + } +}