use std::borrow::Cow;
use std::str;
#[derive(Clone, Copy, Debug)]
pub struct Utf7 {
shift_in: u8,
shift_in_escaped: &'static str,
base64: base64::Config,
ch_63: u8,
indirect: &'static [u8],
}
pub const STD: Utf7 = Utf7 {
shift_in: b'+',
shift_in_escaped: "+-",
base64: base64::STANDARD_NO_PAD,
ch_63: b'/',
indirect: b"~\\+",
};
pub const IMAP: Utf7 = Utf7 {
shift_in: b'&',
shift_in_escaped: "&-",
base64: base64::IMAP_MUTF7,
ch_63: b',',
indirect: b"&",
};
impl Utf7 {
pub fn decode<'a>(&self, s: &'a str) -> Cow<'a, str> {
let bytes = s.as_bytes();
let mut transformed = String::new();
let mut utf16 = Vec::new();
let mut utf16_ne = Vec::new();
let mut split_points = bytes
.iter()
.copied()
.enumerate()
.scan(false, |in_encoded, (ix, ch)| {
if *in_encoded {
*in_encoded = self.is_base64_char(ch);
Some((ix, false))
} else if self.shift_in == ch {
*in_encoded = true;
Some((ix, true))
} else {
Some((ix, false))
}
})
.filter(|&(_, is_start)| is_start)
.map(|(ix, _)| ix)
.chain(std::iter::once(s.len()));
let mut split_start = split_points.next().unwrap();
let mut prefix = Some(&bytes[..split_start]);
for split_end in split_points {
let chunk = &bytes[split_start + 1..split_end];
split_start = split_end;
if let Some(prefix) = prefix.take() {
transformed.push_str(
str::from_utf8(prefix).expect("Invalidated UTF-8?"),
);
}
let base64_end = chunk
.iter()
.copied()
.enumerate()
.find(|&(_, ch)| !self.is_base64_char(ch));
let (base64_end, unencoded_start) = match base64_end {
None => (chunk.len(), chunk.len()),
Some((ix, b'-')) => (ix, ix + 1),
Some((ix, _)) => (ix, ix),
};
if 0 == base64_end {
transformed.push(self.shift_in.into());
} else {
utf16.clear();
if base64::decode_config_buf(
&chunk[..base64_end],
self.base64.decode_allow_trailing_bits(true),
&mut utf16,
)
.is_err()
{
transformed.push(self.shift_in.into());
transformed.push_str(
str::from_utf8(chunk).expect("Invalidated UTF-8?"),
);
continue;
}
utf16_ne.clear();
utf16_ne.extend(
utf16
.chunks(2)
.filter(|chunk| 2 == chunk.len())
.map(|c| u16::from_be_bytes([c[0], c[1]])),
);
transformed.push_str(&String::from_utf16_lossy(&utf16_ne));
}
transformed.push_str(
str::from_utf8(&chunk[unencoded_start..])
.expect("Invalidated UTF-8?"),
);
}
if transformed.is_empty() {
Cow::Borrowed(s)
} else {
Cow::Owned(transformed)
}
}
pub fn encode<'a>(&self, s: &'a str) -> Cow<'a, str> {
let mut transformed = String::new();
let mut direct_start = 0;
let mut direct_end = 0;
for (ix, byte) in s.as_bytes().iter().copied().enumerate() {
if self.is_direct(byte) {
if ix != direct_end {
self.encode_group(
&mut transformed,
s,
direct_start,
direct_end,
ix,
);
direct_start = ix;
}
direct_end = ix + 1;
} else if self.shift_in == byte {
self.encode_group(
&mut transformed,
s,
direct_start,
direct_end,
ix,
);
transformed.push_str(self.shift_in_escaped);
direct_start = ix + 1;
direct_end = ix + 1;
}
}
if transformed.is_empty() && direct_end == s.len() {
Cow::Borrowed(s)
} else {
self.encode_group(
&mut transformed,
s,
direct_start,
direct_end,
s.len(),
);
Cow::Owned(transformed)
}
}
fn encode_group(
&self,
dst: &mut String,
src: &str,
direct_start: usize,
direct_end: usize,
indirect_end: usize,
) {
dst.push_str(&src[direct_start..direct_end]);
if direct_end < indirect_end {
let mut buf =
Vec::<u8>::with_capacity((indirect_end - direct_end) * 2);
for unit in src[direct_end..indirect_end].encode_utf16() {
buf.extend_from_slice(&unit.to_be_bytes());
}
dst.push(self.shift_in.into());
dst.push_str(&base64::encode_config(&buf, self.base64));
dst.push('-');
}
}
fn is_direct(&self, byte: u8) -> bool {
(b' '..0x7F).contains(&byte) && !self.indirect.contains(&byte)
}
fn is_base64_char(&self, ch: u8) -> bool {
ch.is_ascii_lowercase()
|| ch.is_ascii_uppercase()
|| ch.is_ascii_digit()
|| b'+' == ch
|| self.ch_63 == ch
}
}
#[cfg(test)]
mod test {
use proptest::prelude::*;
use super::*;
#[test]
fn imap_encode() {
assert_eq!("INBOX", IMAP.encode("INBOX"));
assert_eq!("Lost &- Found", IMAP.encode("Lost & Found"));
assert_eq!(
"~peter/mail/&U,BTFw-/&ZeVnLIqe-",
IMAP.encode("~peter/mail/台北/日本語")
);
assert_eq!("&Jjo-!", IMAP.encode("☺!"));
assert_eq!("&U,BTF2XlZyyKng-", IMAP.encode("台北日本語"));
assert_eq!("&AADYANwA,+AAoQCh-", IMAP.encode("\x00𐀀¢¡¡"));
}
#[test]
fn imap_decode() {
assert_eq!("INBOX", IMAP.decode("INBOX"));
assert_eq!("Lost & Found", IMAP.decode("Lost &- Found"));
assert_eq!(
"~peter/mail/台北/日本語",
IMAP.decode("~peter/mail/&U,BTFw-/&ZeVnLIqe-")
);
assert_eq!("☺!", IMAP.decode("&Jjo-!"));
assert_eq!("台北日本語", IMAP.decode("&U,BTF2XlZyyKng-"));
assert_eq!("\x00𐀀¢¡¡", IMAP.decode("&AADYANwA,+AAoQCh-"));
}
#[test]
fn std_encode() {
assert_eq!("hello world", STD.encode("hello world"));
assert_eq!(
"+AH4-peter+AFw-lost+-found",
STD.encode("~peter\\lost+found")
);
assert_eq!("Hi Mom +Jjo-!", STD.encode("Hi Mom ☺!"));
assert_eq!("+ZeVnLIqe-", STD.encode("日本語"));
assert_eq!("A+ImIDkQ-.", STD.encode("A≢Α.")); assert_eq!("Item 3 is +AKM-1.", STD.encode("Item 3 is £1."));
assert_eq!("+AADYANwA/+AAoQCh-", STD.encode("\x00𐀀¢¡¡"));
}
#[test]
fn std_decode() {
assert_eq!("hello world", STD.decode("hello world"));
assert_eq!(
"~peter\\lost+found",
STD.decode("+AH4-peter+AFw-lost+-found")
);
assert_eq!("Hi Mom ☺!", STD.decode("Hi Mom +Jjo-!"));
assert_eq!("日本語", STD.decode("+ZeVnLIqe-"));
assert_eq!("A≢Α.", STD.decode("A+ImIDkQ."));
assert_eq!("Item 3 is £1.", STD.decode("Item 3 is +AKM-1."));
assert_eq!("\x00𐀀¢¡¡", STD.decode("+AADYANwA/+AAoQCh-"));
}
#[test]
fn decode_pathological() {
assert_eq!("hello+", STD.decode("hello+"));
assert_eq!("hello+.", STD.decode("hello+."));
assert_eq!("hello+ä", STD.decode("hello+ä"));
assert_eq!("hello~", STD.decode("hello+AH4"));
assert_eq!("¡", IMAP.decode("&AA¡"));
}
proptest! {
#[test]
fn encoding_is_reversible(s in ".*") {
assert_eq!(s, STD.decode(&STD.encode(&s)));
assert_eq!(s, IMAP.decode(&IMAP.encode(&s)));
}
#[test]
fn decoding_never_fails(s in ".*") {
STD.decode(&s);
IMAP.decode(&s);
}
}
}