#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::{string::String, vec::Vec};
use core::fmt;
static HEX_CHARS: &[char] = &[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
];
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ParseMode {
Strict,
Robust,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InputMode {
Text,
Binary,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Options {
line_length_limit: usize,
input_mode: InputMode,
parse_mode: ParseMode,
}
impl Options {
pub fn line_length_limit(mut self, limit: usize) -> Self {
self.line_length_limit = limit;
self
}
pub fn input_mode(mut self, mode: InputMode) -> Self {
self.input_mode = mode;
self
}
pub fn parse_mode(mut self, mode: ParseMode) -> Self {
self.parse_mode = mode;
self
}
}
impl Default for Options {
fn default() -> Self {
Options {
line_length_limit: 76,
input_mode: InputMode::Text,
parse_mode: ParseMode::Robust,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum QuotedPrintableError {
InvalidByte,
LineTooLong,
IncompleteHexOctet,
InvalidHexOctet,
LowercaseHexOctet,
}
impl fmt::Display for QuotedPrintableError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
QuotedPrintableError::InvalidByte => {
write!(
f,
"A unallowed byte was found in the quoted-printable input"
)
}
QuotedPrintableError::LineTooLong => {
write!(
f,
"A line length in the quoted-printed input exceeded 76 bytes"
)
}
QuotedPrintableError::IncompleteHexOctet => {
write!(
f,
"A '=' followed by only one character was found in the input"
)
}
QuotedPrintableError::InvalidHexOctet => {
write!(
f,
"A '=' followed by non-hex characters was found in the input"
)
}
QuotedPrintableError::LowercaseHexOctet => {
write!(f, "A '=' was followed by lowercase hex characters")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for QuotedPrintableError {
fn description(&self) -> &str {
"invalid quoted-printable input"
}
fn cause(&self) -> Option<&dyn std::error::Error> {
None
}
}
#[inline(always)]
pub fn decode<R: AsRef<[u8]>>(input: R, mode: ParseMode) -> Result<Vec<u8>, QuotedPrintableError> {
_decode(input.as_ref(), Options::default().parse_mode(mode))
}
#[inline(always)]
pub fn decode_with_options<R: AsRef<[u8]>>(
input: R,
options: Options,
) -> Result<Vec<u8>, QuotedPrintableError> {
_decode(input.as_ref(), options)
}
fn _decode(input: &[u8], options: Options) -> Result<Vec<u8>, QuotedPrintableError> {
let filtered = input
.iter()
.filter_map(|&c| match c {
b'\t' | b'\r' | b'\n' | b' '..=b'~' => Some(c as char),
_ => None,
})
.collect::<String>();
if options.parse_mode == ParseMode::Strict {
if filtered.len() != input.len() {
return Err(QuotedPrintableError::InvalidByte);
}
let mut last = None;
for b in filtered.bytes() {
if last == None && b == b'\n' {
return Err(QuotedPrintableError::InvalidByte);
}
if (last == Some(b'\r')) != (b == b'\n') {
return Err(QuotedPrintableError::InvalidByte);
}
last = Some(b);
}
if last == Some(b'\r') {
return Err(QuotedPrintableError::InvalidByte);
}
}
let mut decoded = Vec::new();
let mut lines = filtered.lines();
let mut add_line_break = None;
loop {
let mut bytes = match lines.next() {
Some(v) => v.trim_end().bytes(),
None => {
if options.parse_mode == ParseMode::Strict && add_line_break == Some(false) {
return Err(QuotedPrintableError::IncompleteHexOctet);
}
break;
}
};
if options.parse_mode == ParseMode::Strict && bytes.len() > options.line_length_limit {
return Err(QuotedPrintableError::LineTooLong);
}
if add_line_break == Some(true) {
decoded.push(b'\r');
decoded.push(b'\n');
add_line_break = Some(false);
}
loop {
let byte = match bytes.next() {
Some(v) => v,
None => {
add_line_break = Some(true);
break;
}
};
if byte == b'=' {
let upper = match bytes.next() {
Some(v) => v,
None => break,
};
let lower = match bytes.next() {
Some(v) => v,
None => {
if options.parse_mode == ParseMode::Strict {
return Err(QuotedPrintableError::IncompleteHexOctet);
}
decoded.push(byte);
decoded.push(upper);
add_line_break = Some(true);
break;
}
};
let upper_char = upper as char;
let lower_char = lower as char;
if upper_char.is_ascii_hexdigit() && lower_char.is_ascii_hexdigit() {
if options.parse_mode == ParseMode::Strict
&& (upper_char.to_uppercase().next() != Some(upper_char)
|| lower_char.to_uppercase().next() != Some(lower_char))
{
return Err(QuotedPrintableError::LowercaseHexOctet);
}
let combined =
upper_char.to_digit(16).unwrap() << 4 | lower_char.to_digit(16).unwrap();
decoded.push(combined as u8);
} else {
if options.parse_mode == ParseMode::Strict {
return Err(QuotedPrintableError::InvalidHexOctet);
}
decoded.push(byte);
decoded.push(upper);
decoded.push(lower);
}
} else {
decoded.push(byte);
}
}
}
if filtered.ends_with('\n') && add_line_break == Some(true) {
decoded.push(b'\r');
decoded.push(b'\n');
}
Ok(decoded)
}
fn append(
result: &mut String,
to_append: &[char],
bytes_on_line: &mut usize,
limit: usize,
backup_pos: &mut usize,
) {
if *bytes_on_line + to_append.len() > limit {
if *bytes_on_line == limit {
*bytes_on_line = result.len() - *backup_pos;
result.insert_str(*backup_pos, "=\r\n");
} else {
result.push_str("=\r\n");
*bytes_on_line = 0;
}
}
result.extend(to_append);
*bytes_on_line += to_append.len();
*backup_pos = result.len() - to_append.len();
}
fn encode_trailing_space_tab(
result: &mut String,
bytes_on_line: &mut usize,
limit: usize,
backup_pos: &mut usize,
) {
match result.chars().last() {
Some(' ') => {
*bytes_on_line -= 1;
result.pop();
append(result, &['=', '2', '0'], bytes_on_line, limit, backup_pos);
}
Some('\t') => {
*bytes_on_line -= 1;
result.pop();
append(result, &['=', '0', '9'], bytes_on_line, limit, backup_pos);
}
_ => (),
}
}
#[inline(always)]
pub fn encode<R: AsRef<[u8]>>(input: R) -> Vec<u8> {
let encoded_as_string = _encode(
input.as_ref(),
Options::default().input_mode(InputMode::Text),
);
encoded_as_string.into()
}
#[inline(always)]
pub fn encode_binary<R: AsRef<[u8]>>(input: R) -> Vec<u8> {
let encoded_as_string = _encode(
input.as_ref(),
Options::default().input_mode(InputMode::Binary),
);
encoded_as_string.into()
}
fn _encode(input: &[u8], options: Options) -> String {
let limit = options.line_length_limit;
let mut result = String::with_capacity(input.len());
let mut on_line: usize = 0;
let mut backup_pos: usize = 0;
let mut was_cr = false;
let mut it = input.iter().peekable();
while let Some(&byte) = it.next() {
if was_cr {
if byte == b'\n' {
encode_trailing_space_tab(&mut result, &mut on_line, limit, &mut backup_pos);
match options.input_mode {
InputMode::Text => {
result.push_str("\r\n");
on_line = 0;
}
InputMode::Binary => {
append(
&mut result,
&['=', '0', 'D'],
&mut on_line,
limit,
&mut backup_pos,
);
append(
&mut result,
&['=', '0', 'A'],
&mut on_line,
limit,
&mut backup_pos,
);
}
}
was_cr = false;
continue;
}
append(
&mut result,
&['=', '0', 'D'],
&mut on_line,
limit,
&mut backup_pos,
);
}
if byte == b'\r' {
was_cr = true;
continue;
} else {
was_cr = false;
}
if limit - on_line >= 3 && !needs_encoding(byte) {
let mut run_len: usize = 1;
let max_run_len: usize = limit - on_line - 2;
debug_assert!(max_run_len >= run_len);
result.push(byte as char);
while let Some(&&next_byte) = it.peek() {
if run_len == max_run_len {
break;
}
if needs_encoding(next_byte) {
break;
}
run_len += 1;
result.push(next_byte as char);
it.next();
}
on_line += run_len;
backup_pos = result.len();
continue;
}
encode_byte(&mut result, byte, &mut on_line, limit, &mut backup_pos);
}
if was_cr {
append(
&mut result,
&['=', '0', 'D'],
&mut on_line,
limit,
&mut backup_pos,
);
} else {
encode_trailing_space_tab(&mut result, &mut on_line, limit, &mut backup_pos);
}
result
}
#[inline(always)]
fn needs_encoding(c: u8) -> bool {
match c {
b'=' => true,
b'\t' | b' '..=b'~' => false,
_ => true,
}
}
#[inline(always)]
pub fn encode_to_str<R: AsRef<[u8]>>(input: R) -> String {
_encode(
input.as_ref(),
Options::default().input_mode(InputMode::Text),
)
}
#[inline(always)]
pub fn encode_binary_to_str<R: AsRef<[u8]>>(input: R) -> String {
_encode(
input.as_ref(),
Options::default().input_mode(InputMode::Binary),
)
}
#[inline(always)]
pub fn encode_with_options<R: AsRef<[u8]>>(input: R, options: Options) -> String {
_encode(input.as_ref(), options)
}
#[inline]
fn encode_byte(
result: &mut String,
to_append: u8,
on_line: &mut usize,
limit: usize,
backup_pos: &mut usize,
) {
match to_append {
b'=' => append(result, &['=', '3', 'D'], on_line, limit, backup_pos),
b'\t' | b' '..=b'~' => append(result, &[char::from(to_append)], on_line, limit, backup_pos),
_ => append(
result,
&hex_encode_byte(to_append),
on_line,
limit,
backup_pos,
),
}
}
#[inline(always)]
fn hex_encode_byte(byte: u8) -> [char; 3] {
[
'=',
lower_nibble_to_hex(byte >> 4),
lower_nibble_to_hex(byte),
]
}
#[inline(always)]
fn lower_nibble_to_hex(half_byte: u8) -> char {
HEX_CHARS[(half_byte & 0x0F) as usize]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode() {
assert_eq!(
"hello world",
String::from_utf8(decode("hello world", ParseMode::Strict).unwrap()).unwrap()
);
assert_eq!(
"Now's the time for all folk to come to the aid of their country.",
String::from_utf8(
decode(
"Now's the time =\r\nfor all folk to come=\r\n \
to the aid of their country.",
ParseMode::Strict,
)
.unwrap(),
)
.unwrap()
);
assert_eq!(
"\r\nhello=world",
String::from_utf8(decode("=0D=0Ahello=3Dworld", ParseMode::Strict).unwrap()).unwrap()
);
assert_eq!(
"hello world\r\ngoodbye world",
String::from_utf8(decode("hello world\r\ngoodbye world", ParseMode::Strict).unwrap(),)
.unwrap()
);
assert_eq!(
"hello world\r\ngoodbye world",
String::from_utf8(
decode("hello world \r\ngoodbye world ", ParseMode::Strict).unwrap(),
)
.unwrap()
);
assert_eq!(
"hello world\r\ngoodbye world x",
String::from_utf8(
decode(
"hello world \r\ngoodbye world = \r\nx",
ParseMode::Strict,
)
.unwrap(),
)
.unwrap()
);
assert_eq!(true, decode("hello world=x", ParseMode::Strict).is_err());
assert_eq!(
"hello world=x",
String::from_utf8(decode("hello world=x", ParseMode::Robust).unwrap()).unwrap()
);
assert_eq!(true, decode("hello =world=", ParseMode::Strict).is_err());
assert_eq!(
"hello =world",
String::from_utf8(decode("hello =world=", ParseMode::Robust).unwrap()).unwrap()
);
assert_eq!(true, decode("hello world=3d", ParseMode::Strict).is_err());
assert_eq!(
"hello world=",
String::from_utf8(decode("hello world=3d", ParseMode::Robust).unwrap()).unwrap()
);
assert_eq!(true, decode("hello world=3m", ParseMode::Strict).is_err());
assert_eq!(
"hello world=3m",
String::from_utf8(decode("hello world=3m", ParseMode::Robust).unwrap()).unwrap()
);
assert_eq!(true, decode("hello\u{FF}world", ParseMode::Strict).is_err());
assert_eq!(
"helloworld",
String::from_utf8(decode("hello\u{FF}world", ParseMode::Robust).unwrap()).unwrap()
);
assert_eq!(
true,
decode(
"12345678901234567890123456789012345678901234567890123456789012345678901234567",
ParseMode::Strict,
)
.is_err()
);
assert_eq!(
"12345678901234567890123456789012345678901234567890123456789012345678901234567",
String::from_utf8(
decode(
"12345678901234567890123456789012345678901234567890123456789012345678901234567",
ParseMode::Robust,
)
.unwrap(),
)
.unwrap()
);
assert_eq!(
"1234567890123456789012345678901234567890123456789012345678901234567890123456",
String::from_utf8(
decode(
"1234567890123456789012345678901234567890123456789012345678901234567890123456",
ParseMode::Strict,
)
.unwrap(),
)
.unwrap()
);
}
#[test]
fn test_encode() {
assert_eq!("hello, world!", encode_to_str("hello, world!".as_bytes()));
assert_eq!(
"hello,=0Cworld!",
encode_to_str("hello,\u{c}world!".as_bytes())
);
assert_eq!(
"this=00is=C3=BFa=3Dlong=0Dstring=0Athat gets wrapped and stuff, \
woohoo!=C3=\r\n=89",
encode_to_str(
"this\u{0}is\u{FF}a=long\rstring\nthat gets \
wrapped and stuff, woohoo!\u{c9}",
)
);
assert_eq!(
"this=00is=C3=BFa=3Dlong=0Dstring=0Athat just fits in a line, woohoo!=C3=89",
encode_to_str(
"this\u{0}is\u{FF}a=long\rstring\nthat just fits \
in a line, woohoo!\u{c9}",
)
);
assert_eq!(
"this=20\r\nhas linebreaks\r\n built right in.",
encode_to_str("this \r\nhas linebreaks\r\n built right in.")
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY",
)
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=\r\nXY",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY",
)
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=\r\nXXY",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY",
)
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=00Y",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u{0}Y",
)
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=\r\n=00Y",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u{0}Y",
)
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=\r\n=00Y",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u{0}Y",
)
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=\r\n=00Y",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u{0}Y",
)
);
assert_eq!("=0D=3D", encode_to_str("\r="));
assert_eq!("=0D\r\n", encode_to_str("\r\r\n"));
assert_eq!("a=0D\r\nb", encode_to_str("a\r\r\nb"));
assert_eq!("=0D", encode_to_str("\r"));
assert_eq!("=0D=0D", encode_to_str("\r\r"));
assert_eq!("\r\n", encode_to_str("\r\n"));
assert_eq!("trailing spaces =20", encode_to_str("trailing spaces "),);
assert_eq!(
"trailing spaces and crlf =20\r\n",
encode_to_str("trailing spaces and crlf \r\n"),
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=\r\n=09",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\t"
),
);
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=\r\n=20\r\n",
encode_to_str("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \r\n"),
);
}
#[test]
fn test_lower_nibble_to_hex() {
let test_data: &[(u8, char, char)] = &[
(0, '0', '0'),
(1, '0', '1'),
(9, '0', '9'),
(10, '0', 'A'),
(15, '0', 'F'),
(16, '1', '0'),
(255, 'F', 'F'),
];
for &(nr, high, low) in test_data.iter() {
let got_high = lower_nibble_to_hex(nr >> 4);
assert_eq!(high, got_high);
let got_low = lower_nibble_to_hex(nr);
assert_eq!(low, got_low);
}
}
#[test]
fn test_qp_rt() {
let s = b"foo\r\n";
let qp = encode_to_str(s);
let rt = decode(&qp, ParseMode::Strict).unwrap();
assert_eq!(s.as_slice(), rt.as_slice());
}
#[test]
fn test_binary() {
assert_eq!("foo=0D=0A", encode_binary_to_str("foo\r\n"));
assert_eq!(
"foo\r\n",
String::from_utf8(decode("foo=0D=0A", ParseMode::Strict).unwrap()).unwrap()
);
assert_eq!(
"=0D=0A=0D=0A=0D=0A=0D=0A=0D=0A=0D=0A=0D=0A=0D=0A=0D=0A=0D=0A=0D=0A=0D=0A=0D=\r\n=0A=0D=0A=0D=0A",
encode_binary_to_str("\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n")
);
}
#[test]
fn test_three() {
assert_eq!(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=3DX=\r\n=3D=3DY",
encode_to_str(
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=X==Y",
)
);
}
#[test]
fn test_single_cr_nl() {
decode(b"a\ra", ParseMode::Strict).unwrap_err();
decode(b"a\na", ParseMode::Strict).unwrap_err();
decode(b"\na", ParseMode::Strict).unwrap_err();
decode(b"aa\r", ParseMode::Strict).unwrap_err();
decode(b"a\r\na", ParseMode::Strict).unwrap();
}
#[test]
fn test_linebreaks() {
assert_eq!(decode(b"=\r\n", ParseMode::Strict).unwrap(), b"");
assert_eq!(decode(b"=\r\na", ParseMode::Strict).unwrap(), b"a");
assert_eq!(decode(b"a\r\n", ParseMode::Strict).unwrap(), b"a\r\n");
assert_eq!(decode(b"\r\n", ParseMode::Strict).unwrap(), b"\r\n");
assert_eq!(decode(b"a=\r\n", ParseMode::Strict).unwrap(), b"a");
}
}