xmpp-rs/xso/src/text.rs

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)
}
}