// 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 base64::{engine::general_purpose::STANDARD as Base64Engine, Engine}; use jid::Jid; use std::str::FromStr; use xso::error::Error; /// 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::Other( "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> { Base64Engine.decode(s).map_err(Error::text_parse_error) } 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(); Base64Engine.decode(s).map_err(Error::text_parse_error) } 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::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(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).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(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 { Jid::from_str(s).map_err(Error::text_parse_error) } 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(), "Invalid length"); // Too short? let err = FixedHex::<3>::decode("01fe").unwrap_err(); assert_eq!(err.to_string(), "Invalid length"); // Not-even numbers? let err = FixedHex::<3>::decode("01feE").unwrap_err(); assert_eq!(err.to_string(), "Invalid length"); // No colon supported. let err = FixedHex::<3>::decode("0:f:EF").unwrap_err(); assert_eq!( err.to_string(), "text parse error: invalid digit found in string" ); // No non-hex character allowed. let err = FixedHex::<3>::decode("01defg").unwrap_err(); assert_eq!( err.to_string(), "text parse error: invalid digit found in string" ); } }