#![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', ]; /// A flag that allows control over the decoding strictness. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ParseMode { /// Perform strict checking over the input, and return an error if any /// input appears malformed. Strict, /// Perform robust parsing, and gracefully handle any malformed input. This /// can result in the decoded output being different than what was intended. Robust, } /// A flag that controls how to treat the input when encoding. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InputMode { /// Treat the input as text, and don't encode CRLF pairs. Text, /// Treat the input as binary, and encode all CRLF pairs. Binary, } /// Options to control encoding and decoding behaviour. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Options { /// Line length at which to wrap when encoding. Also /// determines if input is valid or not when decoding in /// strict mode. line_length_limit: usize, /// How to treat the input when encoding. input_mode: InputMode, /// How strict to be while decoding. 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, } } } /// An error type that represents different kinds of decoding errors. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum QuotedPrintableError { /// A byte was found in the input that was outside of the allowed range. The /// allowed range is the horizontal tab (ASCII 0x09), CR/LF characters (ASCII /// 0x0D and 0x0A), and anything in the ASCII range 0x20 to 0x7E, inclusive. InvalidByte, /// Lines where found in the input that exceeded 76 bytes in length, excluding /// the terminating CRLF. LineTooLong, /// An '=' character was found in the input without the proper number of /// hex-characters following it. This includes '=' characters followed /// by a single character and then the CRLF pair, for example. IncompleteHexOctet, /// An '=' character was found with two following characters, but they were /// not hex characters. '=Hi' for example would be an invalid encoding. InvalidHexOctet, /// An '=' character was found with two following hex characters, but the /// hex characters were lowercase rather than uppercase. The spec explicitly /// requires uppercase hex to be used, so this is considered an error. 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 } } /// Decodes a piece of quoted-printable data. /// This implementation is equivalent to `decode_with_options` with an /// `Options` that uses the provided `ParseMode`. #[inline(always)] pub fn decode>(input: R, mode: ParseMode) -> Result, QuotedPrintableError> { _decode(input.as_ref(), Options::default().parse_mode(mode)) } /// Decodes a piece of quoted-printable data. /// /// The quoted-printable transfer-encoding is defined in IETF RFC 2045, section /// 6.7. This function attempts to decode input that is conformant with that /// spec. Note that quoted-printable encoding is independent of charset, and so /// this function returns a Vec of bytes upon success. It is up to the caller /// to convert that to a String if desired; the charset required to do so must /// come from somewhere else. /// /// # Examples /// /// ``` /// use quoted_printable::{decode, ParseMode}; /// let decoded = decode("hello=3Dworld=0D=0A".as_bytes(), ParseMode::Robust).unwrap(); /// assert_eq!("hello=world\r\n", String::from_utf8(decoded).unwrap()); /// ``` /// /// # Errors /// /// If this function is called with the `ParseMode::Strict` option, then it may return /// a `QuotedPrintableError` if it detects that the input does not strictly conform /// to the quoted-printable spec. If this function is called with `ParseMode::Robust`, /// then it will attempt to gracefully handle any errors that arise. This might /// result in input bytes being stripped out and ignored in some cases. Refer /// to IETF RFC 2045, section 6.7 for details on what constitutes valid and /// invalid input, and what a "robust" implementation would do in the face of /// invalid input. #[inline(always)] pub fn decode_with_options>( input: R, options: Options, ) -> Result, QuotedPrintableError> { _decode(input.as_ref(), options) } fn _decode(input: &[u8], options: Options) -> Result, QuotedPrintableError> { let filtered = input .iter() .filter_map(|&c| match c { b'\t' | b'\r' | b'\n' | b' '..=b'~' => Some(c as char), _ => None, }) .collect::(); 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) { // the filtered.lines() call above ignores trailing newlines instead // of returning an empty string in the last element. So if there was // a trailing newline, let's tack on the CRLF to carry that through // the decoder. 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 { // We're already at the max length, so inserting the '=' in the soft // line break would put us over. Instead, we insert the soft line // break at the backup pos, which is just before the last thing // appended. *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, ) { // If the last character before a CRLF was a space or tab, then encode it // since "Octets with values of 9 and 32 ... MUST NOT be so represented // at the end of an encoded line." We can just pop it off the end of the // result and append the encoded version. The encoded version may end up // getting bumped to a new line, but in that case we know that the soft // line break '=' will always fit because we're removing one char before // calling append. 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); } _ => (), } } /// Encodes some bytes into quoted-printable format, treating the input as text. /// /// The quoted-printable transfer-encoding is defined in IETF RFC 2045, section /// 6.7. This function encodes a set of raw bytes into a format conformant with /// that spec. The output contains CRLF pairs as needed so that each line is /// wrapped to 76 characters or less (not including the CRLF). /// /// # Examples /// /// ``` /// use quoted_printable::encode; /// let encoded = encode("hello, \u{20ac} zone!"); /// assert_eq!("hello, =E2=82=AC zone!", String::from_utf8(encoded).unwrap()); /// ``` #[inline(always)] pub fn encode>(input: R) -> Vec { let encoded_as_string = _encode( input.as_ref(), Options::default().input_mode(InputMode::Text), ); encoded_as_string.into() } /// Encodes some bytes into quoted-printable format, treating the input as binary. /// /// The quoted-printable transfer-encoding is defined in IETF RFC 2045, section /// 6.7. This function encodes a set of raw bytes into a format conformant with /// that spec. The output contains CRLF pairs as needed so that each line is /// wrapped to 76 characters or less (not including the CRLF). /// /// # Examples /// /// ``` /// use quoted_printable::encode_binary; /// let encoded = encode_binary("hello, \u{20ac} zone!\r\n"); /// assert_eq!("hello, =E2=82=AC zone!=0D=0A", String::from_utf8(encoded).unwrap()); /// ``` #[inline(always)] pub fn encode_binary>(input: R) -> Vec { 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; } // encode the CR ('\r') we skipped over before append( &mut result, &['=', '0', 'D'], &mut on_line, limit, &mut backup_pos, ); } if byte == b'\r' { // remember we had a CR ('\r') but do not encode it yet was_cr = true; continue; } else { was_cr = false; } // look for runs of characters that don't need encoding - this // is very common (QP is normally used on ASCII text where // most characters don't need encoding) and much faster than // calling encode_byte on each character. To keep this from // completely reimplementing append(), only do this if we have // at least 3 characters left on the line and don't try to // deal with the line-ending stuff. if limit - on_line >= 3 && !needs_encoding(byte) { // peek ahead up to max line length and copy the run directly into the output let mut run_len: usize = 1; let max_run_len: usize = limit - on_line - 2; debug_assert!(max_run_len >= run_len); // add the char to result directly - safe because we know we're not at the line length limit result.push(byte as char); // look ahead for a run of characters we can put directly into the result while let Some(&&next_byte) = it.peek() { if run_len == max_run_len { break; } if needs_encoding(next_byte) { break; } run_len += 1; // add the next char to result directly - this is safe // because we're not close to the line length limit result.push(next_byte as char); // consume the byte so we don't see it again it.next(); } // update counters for where we are in the line and what was last appended on_line += run_len; backup_pos = result.len(); continue; } encode_byte(&mut result, byte, &mut on_line, limit, &mut backup_pos); } // we haven't yet encoded the last CR ('\r') so do it now 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, } } /// Encodes some bytes into quoted-printable format. /// /// The difference to `encode` is that this function returns a `String`. /// /// The quoted-printable transfer-encoding is defined in IETF RFC 2045, section /// 6.7. This function encodes a set of raw bytes into a format conformant with /// that spec. The output contains CRLF pairs as needed so that each line is /// wrapped to 76 characters or less (not including the CRLF). /// /// # Examples /// /// ``` /// use quoted_printable::encode_to_str; /// let encoded = encode_to_str("hello, \u{20ac} zone!"); /// assert_eq!("hello, =E2=82=AC zone!", encoded); /// ``` #[inline(always)] pub fn encode_to_str>(input: R) -> String { _encode( input.as_ref(), Options::default().input_mode(InputMode::Text), ) } /// Encodes some bytes into quoted-printable format. /// /// The difference to `encode_binary` is that this function returns a `String`. /// /// The quoted-printable transfer-encoding is defined in IETF RFC 2045, section /// 6.7. This function encodes a set of raw bytes into a format conformant with /// that spec. The output contains CRLF pairs as needed so that each line is /// wrapped to 76 characters or less (not including the CRLF). /// /// # Examples /// /// ``` /// use quoted_printable::encode_binary_to_str; /// let encoded = encode_binary_to_str("hello, \u{20ac} zone!\r\n"); /// assert_eq!("hello, =E2=82=AC zone!=0D=0A", encoded); /// ``` #[inline(always)] pub fn encode_binary_to_str>(input: R) -> String { _encode( input.as_ref(), Options::default().input_mode(InputMode::Binary), ) } /// Encodes some bytes into quoted-printable format, using the provided options. /// /// The quoted-printable transfer-encoding is defined in IETF RFC 2045, section /// 6.7. This function encodes a set of raw bytes into a format conformant with /// that spec. The output contains CRLF pairs as needed so that each line is /// wrapped to 76 characters or less (not including the CRLF). /// /// # Examples /// /// ``` /// use quoted_printable::{encode_with_options, Options}; /// let encoded = encode_with_options("hello, \u{20ac} zone!", Options::default()); /// assert_eq!("hello, =E2=82=AC zone!", encoded); /// ``` #[inline(always)] pub fn encode_with_options>(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.") ); // Test that soft line breaks get inserted at the right place 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", ) ); // Test that soft line breaks don't break up an encoded octet 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); } } // from https://github.com/staktrace/quoted-printable/issues/13 #[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() { // this test enters the fast path for encoding runs of // characters that don't need encoding with three characters // left on the line and the next character needing encoding - // checks for potential off-by-one mistake in that loop assert_eq!( "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=3DX=\r\n=3D=3DY", encode_to_str( "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=X==Y", ) ); } #[test] fn test_single_cr_nl() { // RFC2045 6.7 // Control characters other than TAB, or CR and LF as // parts of CRLF pairs, must not appear. The same is true // for octets with decimal values greater than 126. 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() { // https://github.com/staktrace/quoted-printable/issues/26 // ensure we add the trailing CRLF only if it's not a soft // linebreak 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"); } }