use core::fmt;
use crate::base64;
use crate::error::{Error, Field};
use crate::limits::MAX_MSG_DECODED;
#[derive(Clone, Copy, Debug)]
pub struct Text<'a>(Repr<'a>);
#[derive(Clone, Copy, Debug)]
enum Repr<'a> {
Plain(&'a str),
Base64 {
encoded: &'a str,
decoded_len: usize,
},
}
impl<'a> Text<'a> {
pub(crate) fn plain(s: &'a str, field: Field, max_decoded: usize) -> Result<Self, Error> {
if s.len() > max_decoded {
return Err(Error::TooLong(field));
}
if s.chars().any(char::is_control) {
return Err(Error::ControlCharacter(field));
}
Ok(Text(Repr::Plain(s)))
}
pub(crate) fn encoded(
s: &'a str,
field: Field,
max_encoded: usize,
max_decoded: usize,
) -> Result<Self, Error> {
if s.len() > max_encoded {
return Err(Error::TooLong(field));
}
let decoded_len = base64::decoded_len(s.as_bytes()).ok_or(Error::InvalidBase64(field))?;
if decoded_len > max_decoded {
return Err(Error::TooLong(field));
}
let mut buf = [0u8; MAX_MSG_DECODED];
let n = base64::decode(s.as_bytes(), &mut buf).ok_or(Error::InvalidBase64(field))?;
let text = core::str::from_utf8(&buf[..n]).map_err(|_| Error::InvalidUtf8(field))?;
if text.chars().any(char::is_control) {
return Err(Error::ControlCharacter(field));
}
Ok(Text(Repr::Base64 {
encoded: s,
decoded_len,
}))
}
pub fn len(&self) -> usize {
match self.0 {
Repr::Plain(s) => s.len(),
Repr::Base64 { decoded_len, .. } => decoded_len,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn decode_into<'b>(&self, buf: &'b mut [u8]) -> Option<&'b str> {
let out = buf.get_mut(..self.len())?;
match self.0 {
Repr::Plain(s) => out.copy_from_slice(s.as_bytes()),
Repr::Base64 { encoded, .. } => {
base64::decode(encoded.as_bytes(), out)?;
}
}
core::str::from_utf8(out).ok()
}
pub fn as_plain(&self) -> Option<&'a str> {
match self.0 {
Repr::Plain(s) => Some(s),
Repr::Base64 { .. } => None,
}
}
pub(crate) fn write_base64<W: fmt::Write + ?Sized>(&self, w: &mut W) -> fmt::Result {
match self.0 {
Repr::Plain(s) => base64::encode(s.as_bytes(), w),
Repr::Base64 { encoded, .. } => w.write_str(encoded),
}
}
pub(crate) fn base64_len(&self) -> usize {
match self.0 {
Repr::Plain(s) => base64::encoded_len(s.len()),
Repr::Base64 { encoded, .. } => encoded.len(),
}
}
}
impl fmt::Display for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buf = [0u8; MAX_MSG_DECODED];
f.write_str(self.decode_into(&mut buf).ok_or(fmt::Error)?)
}
}
impl PartialEq for Text<'_> {
fn eq(&self, other: &Self) -> bool {
let (mut a, mut b) = ([0u8; MAX_MSG_DECODED], [0u8; MAX_MSG_DECODED]);
self.decode_into(&mut a) == other.decode_into(&mut b)
}
}
impl Eq for Text<'_> {}
#[cfg(feature = "alloc")]
pub fn sanitize(s: &str, max_bytes: usize) -> alloc::string::String {
const ELLIPSIS: char = '…';
let mut out = alloc::string::String::with_capacity(s.len().min(max_bytes));
let fits = s.len() <= max_bytes;
let budget = if fits {
max_bytes
} else {
max_bytes.saturating_sub(ELLIPSIS.len_utf8())
};
for c in s.chars() {
let c = if c.is_control() { ' ' } else { c };
if out.len() + c.len_utf8() > budget {
break;
}
out.push(c);
}
if !fits && max_bytes >= ELLIPSIS.len_utf8() {
out.push(ELLIPSIS);
}
out
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
#[test]
fn sanitize_replaces_and_truncates() {
assert_eq!(sanitize("a\nb\tc\u{85}d", 100), "a b c d");
assert_eq!(sanitize("héllo wörld", 8), "héll…");
assert!(sanitize("héllo wörld", 8).len() <= 8);
assert_eq!(sanitize("abc", 3), "abc");
}
}