mirror of https://gitlab.com/xmpp-rs/xmpp-rs.git
285 lines
7.6 KiB
Rust
285 lines
7.6 KiB
Rust
use std::borrow::Cow;
|
|
use std::fmt::{self, Write};
|
|
use std::marker::PhantomData;
|
|
use std::ops::Deref;
|
|
|
|
use base64::engine::{general_purpose::STANDARD as StandardBase64Engine, Engine as Base64Engine};
|
|
|
|
use crate::{error::Error, FromXmlText, IntoXmlText};
|
|
|
|
/// Represent a way to encode/decode text data into a Rust type.
|
|
///
|
|
/// This trait is used to specify how a given type is serialised into and from
|
|
/// a string. This bypasses the [`FromXmlText`] / [`IntoXmlText`] traits,
|
|
/// allowing to hide the specific implementation for transforming data from
|
|
/// text into Rust types (and back) from a container's public interface.
|
|
pub trait TextCodec<T> {
|
|
/// Decode a string value into the type.
|
|
fn decode(s: &str) -> Result<T, Error>;
|
|
|
|
/// Encode the type as string value.
|
|
///
|
|
/// If this returns `None`, the string value is not emitted at all.
|
|
fn encode(value: T) -> Option<String>;
|
|
}
|
|
|
|
/// Text codec which does no transform.
|
|
pub struct Plain;
|
|
|
|
impl TextCodec<String> for Plain {
|
|
fn decode(s: &str) -> Result<String, Error> {
|
|
Ok(s.to_string())
|
|
}
|
|
|
|
fn encode(value: String) -> Option<String> {
|
|
Some(value)
|
|
}
|
|
}
|
|
|
|
/// Text codec which returns None instead of the empty string.
|
|
pub struct EmptyAsNone;
|
|
|
|
impl TextCodec<Option<String>> for EmptyAsNone {
|
|
fn decode(s: &str) -> Result<Option<String>, Error> {
|
|
if s.len() == 0 {
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(s.to_owned()))
|
|
}
|
|
}
|
|
|
|
fn encode(value: Option<String>) -> Option<String> {
|
|
match value {
|
|
Some(v) if v.len() > 0 => Some(v),
|
|
Some(_) | None => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Text codec which trims whitespace at the beginning and the end of the
|
|
/// text.
|
|
///
|
|
/// Optionally processes the text further using a nested codec.
|
|
pub struct Trimmed<Inner = Plain>(PhantomData<Inner>);
|
|
|
|
impl<Inner: TextCodec<String>> TextCodec<String> for Trimmed<Inner> {
|
|
fn decode(s: &str) -> Result<String, Error> {
|
|
Inner::decode(s.trim())
|
|
}
|
|
|
|
fn encode(decoded: String) -> Option<String> {
|
|
Inner::encode(decoded)
|
|
}
|
|
}
|
|
|
|
/// Trait for preprocessing text data from XML.
|
|
///
|
|
/// This may be used by codecs to allow to customize some of their behaviour.
|
|
pub trait TextFilter {
|
|
/// Process the incoming string and return the result of the processing.
|
|
///
|
|
/// This should avoid copying wherever possible.
|
|
fn preprocess(s: &str) -> Cow<'_, str>;
|
|
}
|
|
|
|
/// Text preprocessor which returns the input unchanged.
|
|
pub struct NoFilter;
|
|
|
|
impl TextFilter for NoFilter {
|
|
fn preprocess(s: &str) -> Cow<'_, str> {
|
|
Cow::Borrowed(s)
|
|
}
|
|
}
|
|
|
|
/// Text preprocessor to remove all whitespace.
|
|
pub struct StripWhitespace;
|
|
|
|
impl TextFilter for StripWhitespace {
|
|
fn preprocess(s: &str) -> Cow<'_, str> {
|
|
let s: String = s
|
|
.chars()
|
|
.filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
|
|
.collect();
|
|
Cow::Owned(s)
|
|
}
|
|
}
|
|
|
|
/// Text codec transforming text to binary using standard base64.
|
|
///
|
|
/// The `Filter` type argument can be used to employ additional preprocessing
|
|
/// of incoming text data. Most interestingly, passing [`StripWhitespace`]
|
|
/// will make the implementation ignore any whitespace within the text.
|
|
pub struct Base64<Filter: TextFilter = NoFilter>(PhantomData<Filter>);
|
|
|
|
impl<Filter: TextFilter> TextCodec<Vec<u8>> for Base64<Filter> {
|
|
fn decode(s: &str) -> Result<Vec<u8>, Error> {
|
|
let value = Filter::preprocess(s);
|
|
Ok(StandardBase64Engine.decode(value.as_ref())?)
|
|
}
|
|
|
|
fn encode(value: Vec<u8>) -> Option<String> {
|
|
Some(StandardBase64Engine.encode(&value))
|
|
}
|
|
}
|
|
|
|
impl<Filter: TextFilter> TextCodec<Option<Vec<u8>>> for Base64<Filter> {
|
|
fn decode(s: &str) -> Result<Option<Vec<u8>>, Error> {
|
|
if s.len() == 0 {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(Self::decode(s)?))
|
|
}
|
|
|
|
fn encode(decoded: Option<Vec<u8>>) -> Option<String> {
|
|
decoded.and_then(Self::encode)
|
|
}
|
|
}
|
|
|
|
/// Text codec transforming text to binary by emitting it as colon-separated
|
|
/// hex value.
|
|
pub struct ColonSeparatedHex;
|
|
|
|
impl TextCodec<Vec<u8>> for ColonSeparatedHex {
|
|
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)
|
|
}
|
|
|
|
fn encode(decoded: Vec<u8>) -> Option<String> {
|
|
let mut bytes = vec![];
|
|
for byte in decoded {
|
|
bytes.push(format!("{:02X}", byte));
|
|
}
|
|
Some(bytes.join(":"))
|
|
}
|
|
}
|
|
|
|
/// Codec for bytes of lowercase hexadecimal, with a fixed length `N` (in bytes).
|
|
pub struct Hex;
|
|
|
|
impl<const N: usize> TextCodec<[u8; N]> for Hex {
|
|
fn decode(s: &str) -> Result<[u8; N], 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: [u8; N]) -> Option<String> {
|
|
let mut bytes = String::with_capacity(N * 2);
|
|
for byte in decoded {
|
|
write!(&mut bytes, "{:02x}", byte).unwrap();
|
|
}
|
|
Some(bytes)
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> TextCodec<Option<[u8; N]>> for Hex {
|
|
fn decode(s: &str) -> Result<Option<[u8; N]>, Error> {
|
|
if s.len() == 0 {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(Self::decode(s)?))
|
|
}
|
|
|
|
fn encode(decoded: Option<[u8; N]>) -> Option<String> {
|
|
decoded.and_then(Self::encode)
|
|
}
|
|
}
|
|
|
|
impl TextCodec<Vec<u8>> for Hex {
|
|
fn decode(s: &str) -> Result<Vec<u8>, Error> {
|
|
if s.len() % 2 != 0 {
|
|
return Err(Error::ParseError("Invalid length"));
|
|
}
|
|
let n = s.len() / 2;
|
|
|
|
let mut bytes = Vec::with_capacity(n);
|
|
bytes.resize(n, 0u8);
|
|
for i in 0..n {
|
|
bytes[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16)?;
|
|
}
|
|
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn encode(decoded: Vec<u8>) -> Option<String> {
|
|
let mut bytes = String::with_capacity(decoded.len() * 2);
|
|
for byte in decoded {
|
|
write!(&mut bytes, "{:02x}", byte).unwrap();
|
|
}
|
|
Some(bytes)
|
|
}
|
|
}
|
|
|
|
/// Shim wrapper around [`String`] which refuses to parse if the string
|
|
/// is empty
|
|
#[derive(Debug, Clone)]
|
|
pub struct NonEmptyString(String);
|
|
|
|
impl NonEmptyString {
|
|
/// Return `Some(s)` if s is not empty, `None` otherwise.
|
|
pub fn new(s: String) -> Option<Self> {
|
|
if s.len() == 0 {
|
|
return None;
|
|
}
|
|
Some(Self(s))
|
|
}
|
|
}
|
|
|
|
impl Deref for NonEmptyString {
|
|
type Target = String;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for NonEmptyString {
|
|
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
|
|
<String as fmt::Display>::fmt(&self.0, f)
|
|
}
|
|
}
|
|
|
|
impl FromXmlText for NonEmptyString {
|
|
fn from_xml_text(s: &str) -> Result<Self, Error> {
|
|
if s.len() == 0 {
|
|
return Err(Error::ParseError("string must not be empty"));
|
|
}
|
|
Ok(Self(s.to_string()))
|
|
}
|
|
}
|
|
|
|
impl IntoXmlText for NonEmptyString {
|
|
fn into_xml_text(self) -> String {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
/// Text codec which rejects empty strings.
|
|
impl TextCodec<String> for NonEmptyString {
|
|
fn decode(s: &str) -> Result<String, Error> {
|
|
if s.len() == 0 {
|
|
return Err(Error::ParseError("string must not be empty"));
|
|
}
|
|
Ok(s.to_owned())
|
|
}
|
|
|
|
fn encode(value: String) -> Option<String> {
|
|
Some(value)
|
|
}
|
|
}
|