diff options
Diffstat (limited to 'internals/src/encoder/mod.rs')
-rw-r--r-- | internals/src/encoder/mod.rs | 1712 |
1 files changed, 1712 insertions, 0 deletions
diff --git a/internals/src/encoder/mod.rs b/internals/src/encoder/mod.rs new file mode 100644 index 0000000..52a9fb2 --- /dev/null +++ b/internals/src/encoder/mod.rs @@ -0,0 +1,1712 @@ +//! This module provides the encoding buffer. +//! +//! The encoding buffer is the buffer header implementations +//! write there data to. It provides a view special aspects +//! to make it more robust. +//! +//! For example it handles the writing of trailing newlines for headers +//! (if they don't do it) and it fails if you write utf-8 data +//! (in the header) to an buffer which knows that the used mail +//! type doesn't support it. +//! +//! There is also a special `tracing` (cargo) feature which +//! will make it's usage slower, but which will keep track of +//! what data was inserted in which way making debugging and +//! writing tests easier. (Through it should _only_ be enabled +//! for testing and maybe debugging in some cases). +use std::borrow::Cow; +use std::str; + +use failure::Fail; +use soft_ascii_string::{SoftAsciiStr, SoftAsciiChar}; + +use grammar::is_atext; +use ::utils::{ + is_utf8_continuation_byte, + vec_insert_bytes +}; +use ::MailType; +use ::error::{ + EncodingError, EncodingErrorKind, + UNKNOWN, UTF_8, US_ASCII +}; + +#[cfg(feature="traceing")] +#[cfg_attr(test, macro_use)] +mod trace; +#[cfg_attr(test, macro_use)] +mod encodable; + + +#[cfg(feature="traceing")] +pub use self::trace::*; +pub use self::encodable::*; + +/// as specified in RFC 5322 not including CRLF +pub const LINE_LEN_SOFT_LIMIT: usize = 78; +/// as specified in RFC 5322 (mail) + RFC 5321 (smtp) not including CRLF +pub const LINE_LEN_HARD_LIMIT: usize = 998; + + +/// EncodingBuffer for a Mail providing a buffer for encodable traits. +pub struct EncodingBuffer { + mail_type: MailType, + buffer: Vec<u8>, + #[cfg(feature="traceing")] + pub trace: Vec<TraceToken> +} + +impl EncodingBuffer { + + /// Create a new buffer only allowing input compatible with a the specified mail type. + pub fn new(mail_type: MailType) -> Self { + EncodingBuffer { + mail_type, + buffer: Vec::new(), + #[cfg(feature="traceing")] + trace: Vec::new() + } + } + + /// Returns the mail type for which the buffer was created. + pub fn mail_type( &self ) -> MailType { + self.mail_type + } + + /// returns a new EncodingWriter which contains + /// a mutable reference to the current string buffer + /// + pub fn writer(&mut self) -> EncodingWriter { + #[cfg(not(feature="traceing"))] + { + EncodingWriter::new(self.mail_type, &mut self.buffer) + } + #[cfg(feature="traceing")] + { + EncodingWriter::new(self.mail_type, &mut self.buffer, &mut self.trace) + } + } + + /// calls the provided function with a EncodingWriter cleaning up afterwards + /// + /// After calling `func` with the EncodingWriter following cleanup is performed: + /// - if `func` returned an error `handle.undo_header()` is called, this won't + /// undo anything before a `finish_header()` call but will discard partial + /// writes + /// - if `func` succeeded `handle.finish_header()` is called + pub fn write_header_line<FN>(&mut self, func: FN) -> Result<(), EncodingError> + where FN: FnOnce(&mut EncodingWriter) -> Result<(), EncodingError> + { + let mut handle = self.writer(); + match func(&mut handle) { + Ok(()) => { + handle.finish_header(); + Ok(()) + }, + Err(e) => { + handle.undo_header(); + Err(e) + } + } + + } + + pub fn write_blank_line(&mut self) { + //TODO/BENCH push_str vs. extends(&[u8]) + self.buffer.extend("\r\n".as_bytes()); + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::BlankLine); } + } + + /// writes a body to the internal buffer, without verifying it's correctness + pub fn write_body_unchecked(&mut self, body: &impl AsRef<[u8]>) { + let slice = body.as_ref(); + self.buffer.extend(slice); + if !slice.ends_with(b"\r\n") { + self.buffer.extend(b"\r\n"); + } + } + + //TODO impl. a alt. `write_body(body, boundaries)` which: + // - checks the body (us-ascii or mime8bit/internationalized) + // - checks for orphan '\r'/'\n' and 0 bytes + // - check that no string in boundaries appears in the text + // - this probably requires creating a regex for each body + // through as boundaries are "fixed" there might be an more + // efficient algorithm then a regex (i.e. using tries) + + /// # Error + /// + /// This can fail if a body does not contain valid utf8. + pub fn as_str(&self) -> Result<&str, EncodingError> { + str::from_utf8(self.buffer.as_slice()) + .map_err(|err| { + EncodingError::from(( + err.context(EncodingErrorKind::InvalidTextEncoding { + expected_encoding: UTF_8, + got_encoding: UNKNOWN + }), + self.mail_type() + )) + }) + } + + /// Converts the internal buffer into an utf-8 string if possible. + pub fn to_string(&self) -> Result<String, EncodingError> { + Ok(self.as_str()?.to_owned()) + } + + /// Lossy conversion of the internal buffer to an string. + pub fn to_string_lossy(&self) -> Cow<str> { + String::from_utf8_lossy(self.buffer.as_slice()) + } + + /// Return a slice view to the underlying buffer. + pub fn as_slice(&self) -> &[u8] { + &self.buffer + } + +} + + +impl Into<Vec<u8>> for EncodingBuffer { + fn into(self) -> Vec<u8> { + self.buffer + } +} + +impl Into<(MailType, Vec<u8>)> for EncodingBuffer { + fn into(self) -> (MailType, Vec<u8>) { + (self.mail_type, self.buffer) + } +} + +#[cfg(feature="traceing")] +impl Into<(MailType, Vec<u8>, Vec<TraceToken>)> for EncodingBuffer { + fn into(self) -> (MailType, Vec<u8>, Vec<TraceToken>) { + let EncodingBuffer { mail_type, buffer, trace } = self; + (mail_type, buffer, trace) + } +} + +/// A handle providing method to write to the underlying buffer +/// keeping track of newlines the current line length and places +/// where the line can be broken so that the soft line length +/// limit (78) and the hard length limit (998) can be kept. +/// +/// It's basically a string buffer which know how to brake +/// lines at the right place. +/// +/// Note any act of writing a header through `EncodingWriter` +/// has to be concluded by either calling `finish_header` or `undo_header`. +/// If not this handle will panic in _test_ builds when being dropped +/// (and the thread is not already panicing) as writes through the handle are directly +/// writes to the underlying buffer which now contains malformed/incomplete +/// data. (Note that this Handle does not own any Drop types so if +/// needed `forget`-ing it won't leak any memory) +/// +/// +pub struct EncodingWriter<'a> { + buffer: &'a mut Vec<u8>, + #[cfg(feature="traceing")] + trace: &'a mut Vec<TraceToken>, + mail_type: MailType, + line_start_idx: usize, + last_fws_idx: usize, + skipped_cr: bool, + /// if there had ben non WS chars since the last FWS + /// or last line start, if there had been a line + /// start since the last fws. + content_since_fws: bool, + /// represents if there had ben non WS chars before the last FWS + /// on the current line (false if there was no FWS yet on the current + /// line). + content_before_fws: bool, + header_start_idx: usize, + #[cfg(feature="traceing")] + trace_start_idx: usize +} + +#[cfg(feature="traceing")] +impl<'a> Drop for EncodingWriter<'a> { + + fn drop(&mut self) { + use std::thread; + if !thread::panicking() && self.has_unfinished_parts() { + // we really should panic as the back buffer i.e. the mail will contain + // some partially written header which definitely is a bug + panic!("dropped Handle which partially wrote header to back buffer (use `finish_header` or `discard`)") + } + } +} + +impl<'inner> EncodingWriter<'inner> { + + #[cfg(not(feature="traceing"))] + fn new( + mail_type: MailType, + buffer: &'inner mut Vec<u8>, + ) -> Self { + let start_idx = buffer.len(); + EncodingWriter { + buffer, + mail_type, + line_start_idx: start_idx, + last_fws_idx: start_idx, + skipped_cr: false, + content_since_fws: false, + content_before_fws: false, + header_start_idx: start_idx + } + } + + #[cfg(feature="traceing")] + fn new( + mail_type: MailType, + buffer: &'inner mut Vec<u8>, + trace: &'inner mut Vec<TraceToken> + ) -> Self { + let start_idx = buffer.len(); + let trace_start_idx = trace.len(); + EncodingWriter { + buffer, + trace, + mail_type, + line_start_idx: start_idx, + last_fws_idx: start_idx, + skipped_cr: false, + content_since_fws: false, + content_before_fws: false, + header_start_idx: start_idx, + trace_start_idx + } + } + + fn reinit(&mut self) { + let start_idx = self.buffer.len(); + self.line_start_idx = start_idx; + self.last_fws_idx = start_idx; + self.skipped_cr = false; + self.content_since_fws = false; + self.content_before_fws = false; + self.header_start_idx = start_idx; + #[cfg(feature="traceing")] + { self.trace_start_idx = self.trace.len(); } + } + + /// Returns true if this type thinks we are in the process of writing a header. + #[inline] + pub fn has_unfinished_parts(&self) -> bool { + self.buffer.len() != self.header_start_idx + } + + /// Returns the associated mail type. + #[inline] + pub fn mail_type(&self) -> MailType { + self.mail_type + } + + /// Returns true if the current line has content, i.e. any non WS char. + #[inline] + pub fn line_has_content(&self) -> bool { + self.content_before_fws | self.content_since_fws + } + + /// Returns the length of the current line in bytes. + #[inline] + pub fn current_line_byte_length(&self) -> usize { + self.buffer.len() - self.line_start_idx + } + + /// marks the current position a a place where a soft + /// line break (i.e. "\r\n ") can be inserted + /// + /// # Trace (test build only) + /// does push a `MarkFWS` Token + pub fn mark_fws_pos(&mut self) { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::MarkFWS) } + self.content_before_fws |= self.content_since_fws; + self.content_since_fws = false; + self.last_fws_idx = self.buffer.len() + } + + /// writes a ascii char to the underlying buffer + /// + /// # Error + /// - fails if the hard line length limit is breached and the + /// line can not be broken with soft line breaks + /// - buffer would contain a orphan '\r' or '\n' after the write + /// + /// # Trace (test build only) + /// does push `NowChar` and then can push `Text`,`CRLF` + pub fn write_char(&mut self, ch: SoftAsciiChar) -> Result<(), EncodingError> { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::NowChar) } + let mut buffer = [0xff_u8; 4]; + let ch: char = ch.into(); + let slice = ch.encode_utf8(&mut buffer); + self.internal_write_char(slice) + } + + /// writes a ascii str to the underlying buffer + /// + /// # Error + /// - fails if the hard line length limit is breached and the + /// line can not be broken with soft line breaks + /// - buffer would contain a orphan '\r' or '\n' after the write + /// + /// Note that in case of an error part of the content might already + /// have been written to the buffer, therefore it is recommended + /// to call `undo_header` after an error (especially if the + /// handle is doped after this!) + /// + /// # Trace (test build only) + /// does push `NowStr` and then can push `Text`,`CRLF` + /// + pub fn write_str(&mut self, s: &SoftAsciiStr) -> Result<(), EncodingError> { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::NowStr) } + self.internal_write_str(s.as_str()) + } + + + /// writes a utf8 str into a buffer for an internationalized mail + /// + /// # Error (ConditionalWriteResult) + /// - fails with `ConditionFailure` if the underlying MailType + /// is not Internationalized + /// - fails with `GeneralFailure` if the hard line length limit is reached + /// - or if the buffer would contain a orphan '\r' or '\n' after the write + /// + /// Note that in case of an error part of the content might already + /// have been written to the buffer, therefore it is recommended + /// to call `undo_header` after an error (especially if the + /// handle is droped after this!) + /// + /// # Trace (test build only) + /// does push `NowUtf8` and then can push `Text`,`CRLF` + pub fn write_if_utf8<'short>(&'short mut self, s: &str) + -> ConditionalWriteResult<'short, 'inner> + { + if self.mail_type().is_internationalized() { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::NowUtf8) } + self.internal_write_str(s).into() + } else { + ConditionalWriteResult::ConditionFailure(self) + } + } + + pub fn write_utf8(&mut self, s: &str) -> Result<(), EncodingError> { + if self.mail_type().is_internationalized() { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::NowUtf8) } + self.internal_write_str(s) + } else { + let mut err = EncodingError::from(( + EncodingErrorKind::InvalidTextEncoding { + expected_encoding: US_ASCII, + got_encoding: UTF_8 + }, + self.mail_type() + )); + let raw_line = &self.buffer[self.line_start_idx..]; + let mut line = String::from_utf8_lossy(raw_line).into_owned(); + line.push_str(s); + err.set_str_context(line); + Err(err) + } + } + + /// Writes a str assumed to be atext if it is atext given the mail type + /// + /// This method is mainly an optimization as the "is atext" and is + /// "is ascii if MailType is Ascii" aspects are checked at the same + /// time resulting in a str which you know is ascii _if_ the mail + /// type is Ascii and which might be non-us-ascii if the mail type + /// is Internationalized. + /// + /// # Error (ConditionalWriteResult) + /// - fails with `ConditionFailure` if the text is not valid atext, + /// this indirectly also includes the utf8/Internationalization check + /// as the `atext` grammar differs between normal and internationalized + /// mail. + /// - fails with `GeneralFailure` if the hard line length limit is reached and + /// the line can't be broken with soft line breaks + /// - or if buffer would contain a orphan '\r' or '\n' after the write + /// (excluding a tailing `'\r'` as it is still valid if followed by an + /// `'\n'`) + /// + /// Note that in case of an error part of the content might already + /// have been written to the buffer, therefore it is recommended + /// to call `undo_header` after an error (especially if the + /// handle is doped after this!) + /// + /// # Trace (test build only) + /// does push `NowAText` and then can push `Text` + /// + pub fn write_if_atext<'short>(&'short mut self, s: &str) + -> ConditionalWriteResult<'short, 'inner> + { + if s.chars().all( |ch| is_atext( ch, self.mail_type() ) ) { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::NowAText) } + // the ascii or not aspect is already converted by `is_atext` + self.internal_write_str(s).into() + } else { + ConditionalWriteResult::ConditionFailure(self) + } + } + + /// passes the input `s` to the condition evaluation function `cond` and + /// then writes it _without additional checks_ to the buffer if `cond` returned + /// true + /// + pub fn write_if<'short, FN>(&'short mut self, s: &str, cond: FN) + -> ConditionalWriteResult<'short, 'inner> + where FN: FnOnce(&str) -> bool + { + if cond(s) { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::NowCondText) } + // the ascii or not aspect is already converted by `is_atext` + self.internal_write_str(s).into() + } else { + ConditionalWriteResult::ConditionFailure(self) + } + } + + /// writes a string to the encoder without checking if it is compatible + /// with the mail type, if not used correctly this can write Utf8 to + /// an Ascii Mail, which is incorrect but has to be safe wrt. rust's safety. + /// + /// Use it as a replacement for cases similar to following: + /// + /// ```ignore + /// check_if_text_if_valid(text)?; + /// if mail_type.is_internationalized() { + /// handle.write_utf8(text)?; + /// } else { + /// handle.write_str(SoftAsciiStr::from_unchecked(text))?; + /// } + /// ``` + /// + /// ==> instead ==> + /// + /// ```ignore + /// check_if_text_if_valid(text)?; + /// handle.write_str_unchecked(text)?; + /// ``` + /// + /// through is gives a different tracing its roughly equivalent. + /// + pub fn write_str_unchecked( &mut self, s: &str) -> Result<(), EncodingError> { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::NowUnchecked) } + self.internal_write_str(s) + } + + /// like finish_header, but won't start a new line + /// + /// This is meant to be used when _miss-using_ the + /// writer to write a "think", which is not a full + /// header. E.g. for testing if a header component + /// is written correctly. So you _normally_ should + /// not use it. + pub fn commit_partial_header(&mut self) { + #[cfg(feature="traceing")] + { if let Some(&TraceToken::End) = self.trace.last() {} + else { self.trace.push(TraceToken::End) } } + self.reinit(); + } + + /// finishes the writing of a header + /// + /// It makes sure the header ends in "\r\n". + /// If the header ends in a orphan '\r' this + /// method will just "use" it for the "\r\n". + /// + /// If the header ends in a CRLF/start of buffer + /// followed by only WS (' ' or '\t' ) the valid + /// header ending is reached by truncating away + /// the WS padding. This is needed as "blank" lines + /// are not allowed. + /// + /// # Trace (test build only) + /// - can push 0-1 of `[CRLF, TruncateToCRLF]` + /// - then does push `End` + /// - calling `finish_current()` multiple times in a row + /// will not generate multiple `End` tokens, just one + pub fn finish_header(&mut self) { + self.start_new_line(); + #[cfg(feature="traceing")] + { if let Some(&TraceToken::End) = self.trace.last() {} + else { self.trace.push(TraceToken::End) } } + self.reinit(); + } + + /// undoes all writes to the internal buffer + /// since the last `finish_header` or `undo_header` or + /// creation of this handle + /// + /// # Trace (test build only) + /// also removes tokens pushed since the last + /// `finish_header` or `undo_header` or creation of + /// this handle + /// + pub fn undo_header(&mut self) { + self.buffer.truncate(self.header_start_idx); + #[cfg(feature="traceing")] + { self.trace.truncate(self.trace_start_idx); } + self.reinit(); + } + + + + //---------------------------------------------------------------------------------------------/ + //-/////////////////////////// methods only using the public iface /////////////////////////-/ + + /// calls mark_fws_pos and then writes a space + /// + /// This method exists for convenience. + /// + /// Note that it can not fail a you just pushed + /// a place to brake the line before writing a space. + /// + /// Note that currently soft line breaks will not + /// collapse whitespace. As such if you use `write_fws` + /// and then the line is broken at that position it will + /// start with two spaces (one from `\r\n ` and one which + /// had been there before). + pub fn write_fws(&mut self) { + self.mark_fws_pos(); + let _ = self.write_char(SoftAsciiChar::from_unchecked(' ')); + } + + + + //---------------------------------------------------------------------------------------------/ + //-/////////////////////////// private methods ////////////////////////-/ + + /// this might partial write some data and then fail. + /// while we could implement a undo option it makes + /// little sense for the use case the generally available + /// `undo_header` is enough. + fn internal_write_str(&mut self, s: &str) -> Result<(), EncodingError> { + if s.is_empty() { + return Ok(()); + } + //TODO I think I wrote a iterator for this somewhere + let mut start = 0; + // the first byte is never a continuation byte so we start + // scanning at the second byte + for (idx_m1, bch) in s.as_bytes()[1..].iter().enumerate() { + if !is_utf8_continuation_byte(*bch) { + // the idx is 1 smaller then it should so add 1 + let end = idx_m1 + 1; + self.internal_write_char(&s[start..end])?; + start = end; + } + } + + //write last letter + self.internal_write_char(&s[start..])?; + Ok(()) + } + + /// if the line has at last one non-WS char a new line + /// will be started by adding `\r\n` if the current line + /// only consists of WS then a new line will be started by + /// removing the blank line (not that WS are only ' ' and '\r') + fn start_new_line(&mut self) { + if self.line_has_content() { + #[cfg(feature="traceing")] + { self.trace.push(TraceToken::CRLF) } + + self.buffer.push(b'\r'); + self.buffer.push(b'\n'); + } else { + #[cfg(feature="traceing")] + { + if self.buffer.len() > self.line_start_idx { + self.trace.push(TraceToken::TruncateToCRLF); + } + } + // e.g. if we "broke" the line on a tailing space => "\r\n " + // this would not be valid so we cut awy the trailing white space + // be if we have "ab " we do not want to cut away the trailing + // whitespace but just add "\r\n" + self.buffer.truncate(self.line_start_idx); + } + self.line_start_idx = self.buffer.len(); + self.content_since_fws = false; + self.content_before_fws = false; + self.last_fws_idx = self.line_start_idx; + + } + + fn break_line_on_fws(&mut self) -> bool { + if self.content_before_fws && self.last_fws_idx > self.line_start_idx { + //INDEX_SAFE: self.content_before_fws is only true if there is at last one char + // if so self.last_ws_idx does not point at the end of the buffer but inside + let newline = match self.buffer[self.last_fws_idx] { + b' ' | b'\t' => "\r\n", + _ => "\r\n " + }; + + vec_insert_bytes(&mut self.buffer, self.last_fws_idx, newline.as_bytes()); + self.line_start_idx = self.last_fws_idx + 2; + // no need last_fws can be < line_start but + //self.last_fws_idx = self.line_start_idx; + self.content_before_fws = false; + // stays the same: + //self.content_since_fws = self.content_since_fws + true + } else { + false + } + } + + /// # Constraints + /// + /// `unchecked_utf8_char` is expected to be exactly + /// one char, which means it's 1-4 bytes in length. + /// + /// The reason why a slice is expected instead of a + /// char is, that this function will at some point push + /// to a byte buffer requiring a `&[u8]` and many function + /// calling this function can directly produce a &[u8]/&str. + /// + /// # Panic + /// + /// Panics if `unchecked_utf8_char` is empty. + /// If debug assertions are enabled it also panics, if + /// unchecked_utf8_char is more than just one char. + fn internal_write_char(&mut self, unchecked_utf8_char: &str) -> Result<(), EncodingError> { + debug_assert_eq!(unchecked_utf8_char.chars().count(), 1); + + let bch = unchecked_utf8_char.as_bytes()[0]; + if bch == b'\n' { + if self.skipped_cr { + self.start_new_line() + } else { + ec_bail!( + mail_type: self.mail_type(), + kind: Malformed + ); + } + self.skipped_cr = false; + return Ok(()); + } else { + if self.skipped_cr { + ec_bail!( + mail_type: self.mail_type(), + kind: Malformed + ); + } + if bch == b'\r' { + self.skipped_cr = true; + return Ok(()); + } else { + self.skipped_cr = false; + } + } + + if self.current_line_byte_length() >= LINE_LEN_SOFT_LIMIT { + if !self.break_line_on_fws() { + if self.buffer.len() == LINE_LEN_HARD_LIMIT { + ec_bail!( + mail_type: self.mail_type(), + kind: HardLineLengthLimitBreached + ); + } + } + } + + self.buffer.extend(unchecked_utf8_char.as_bytes()); + #[cfg(feature="traceing")] + { + //FIXME[rust/nll]: just use a `if let`-`else` with NLL's + let need_new = + if let Some(&mut TraceToken::Text(ref mut string)) = self.trace.last_mut() { + string.push_str(unchecked_utf8_char); + false + } else { + true + }; + if need_new { + let mut string = String::new(); + string.push_str(unchecked_utf8_char); + self.trace.push(TraceToken::Text(string)) + } + + } + + // we can't allow "blank" lines + if bch != b' ' && bch != b'\t' { + // if there is no fws this is equiv to line_has_content + // else line_has_content = self.content_before_fws|self.content_since_fws + self.content_since_fws = true; + } + Ok(()) + } +} + +pub enum ConditionalWriteResult<'a, 'b: 'a> { + Ok, + ConditionFailure(&'a mut EncodingWriter<'b>), + GeneralFailure(EncodingError) +} + +impl<'a, 'b: 'a> From<Result<(), EncodingError>> for ConditionalWriteResult<'a, 'b> { + fn from(v: Result<(), EncodingError>) -> Self { + match v { + Ok(()) => ConditionalWriteResult::Ok, + Err(e) => ConditionalWriteResult::GeneralFailure(e) + } + } +} + +impl<'a, 'b: 'a> ConditionalWriteResult<'a, 'b> { + + #[inline] + pub fn handle_condition_failure<FN>(self, func: FN) -> Result<(), EncodingError> + where FN: FnOnce(&mut EncodingWriter) -> Result<(), EncodingError> + { + use self::ConditionalWriteResult as CWR; + + match self { + CWR::Ok => Ok(()), + CWR::ConditionFailure(handle) => { + func(handle) + }, + CWR::GeneralFailure(err) => Err(err) + } + } +} + + + + + +#[cfg(test)] +mod test { + + use soft_ascii_string::{ SoftAsciiChar, SoftAsciiStr}; + use ::MailType; + use ::error::EncodingErrorKind; + + use super::TraceToken::*; + use super::{EncodingBuffer as _Encoder}; + + mod test_test_utilities { + use encoder::TraceToken::*; + use super::super::simplify_trace_tokens; + + #[test] + fn does_simplify_tokens_strip_nows() { + let inp = vec![ + NowChar, + Text("h".into()), + CRLF, + NowStr, + Text("y yo".into()), + CRLF, + NowUtf8, + Text(", what's".into()), + CRLF, + NowUnchecked, + Text("up!".into()), + CRLF, + NowAText, + Text("abc".into()) + ]; + let out = simplify_trace_tokens(inp); + assert_eq!(out, vec![ + Text("h".into()), + CRLF, + Text("y yo".into()), + CRLF, + Text(", what's".into()), + CRLF, + Text("up!".into()), + CRLF, + Text("abc".into()) + ]) + + } + + #[test] + fn simplify_does_collapse_text() { + let inp = vec![ + NowChar, + Text("h".into()), + NowStr, + Text("y yo".into()), + NowUtf8, + Text(", what's".into()), + NowUnchecked, + Text(" up! ".into()), + NowAText, + Text("abc".into()) + ]; + let out = simplify_trace_tokens(inp); + assert_eq!(out, vec![ + Text("hy yo, what's up! abc".into()) + ]); + } + + #[test] + fn simplify_works_with_empty_text() { + let inp = vec![ + NowStr, + Text("".into()), + CRLF, + ]; + assert_eq!(simplify_trace_tokens(inp), vec![ + Text("".into()), + CRLF + ]) + } + + #[test] + fn simplify_works_with_trailing_empty_text() { + let inp = vec![ + Text("a".into()), + CRLF, + Text("".into()), + ]; + assert_eq!(simplify_trace_tokens(inp), vec![ + Text("a".into()), + CRLF, + Text("".into()) + ]) + } + + } + + mod EncodableInHeader { + #![allow(non_snake_case)] + use super::super::*; + use self::TraceToken::*; + + #[test] + fn is_implemented_for_closures() { + let closure = enc_func!(|handle: &mut EncodingWriter| { + handle.write_utf8("hy ho") + }); + + let mut encoder = EncodingBuffer::new(MailType::Internationalized); + { + let mut handle = encoder.writer(); + assert_ok!(closure.encode(&mut handle)); + handle.finish_header(); + } + assert_eq!(encoder.trace.as_slice(), &[ + NowUtf8, + Text("hy ho".into()), + CRLF, + End + ]) + } + } + + + mod EncodingBuffer { + #![allow(non_snake_case)] + use super::*; + use super::{ _Encoder as EncodingBuffer }; + + #[test] + fn new_encoder() { + let encoder = EncodingBuffer::new(MailType::Internationalized); + assert_eq!(encoder.mail_type(), MailType::Internationalized); + } + + #[test] + fn write_body_unchecked() { + let mut encoder = EncodingBuffer::new(MailType::Ascii); + let body1 = "una body\r\n"; + let body2 = "another body"; + + encoder.write_body_unchecked(&body1); + encoder.write_blank_line(); + encoder.write_body_unchecked(&body2); + + assert_eq!( + encoder.as_slice(), + concat!( + "una body\r\n", + "\r\n", + "another body\r\n" + ).as_bytes() + ) + } + } + + + mod EncodingWriter { + #![allow(non_snake_case)] + use std::mem; + + use super::*; + use super::{ _Encoder as EncodingBuffer }; + + #[test] + fn commit_partial_and_drop_does_not_panic() { + let mut encoder = EncodingBuffer::new(MailType::Ascii); + { + let mut handle = encoder.writer(); + assert_ok!(handle.write_str(SoftAsciiStr::from_unchecked("12"))); + handle.commit_partial_header(); + } + assert_eq!(encoder.as_slice(), b"12"); + } + + #[test] + fn undo_does_undo() { + let mut encoder = EncodingBuffer::new(MailType::Ascii); + { + let mut handle = encoder.writer(); + assert_ok!( + handle.write_str(SoftAsciiStr::from_unchecked("Header-One: 12"))); + handle.undo_header(); + } + assert_eq!(encoder.as_slice(), b""); + } + + #[test] + fn undo_does_not_undo_to_much() { + let mut encoder = EncodingBuffer::new(MailType::Ascii); + { + let mut handle = encoder.writer(); + assert_ok!(handle.write_str(SoftAsciiStr::from_str("Header-One: 12").unwrap())); + handle.finish_header(); + assert_ok!(handle.write_str(SoftAsciiStr::from_str("ups: sa").unwrap())); + handle.undo_header(); + } + assert_eq!(encoder.as_slice(), b"Header-One: 12\r\n"); + } + + #[test] + fn finish_adds_crlf_if_needed() { + let mut encoder = EncodingBuffer::new(MailType::Ascii); + { + let mut handle = encoder.writer(); + assert_ok!(handle.write_str(SoftAsciiStr::from_str("Header-One: 12").unwrap())); + handle.finish_header(); + } + assert_eq!(encoder.as_slice(), b"Header-One: 12\r\n"); + } + + #[test] + fn finish_does_not_add_crlf_if_not_needed() { + let mut encoder = EncodingBuffer::new(MailType::Ascii); + { + let mut handle = encoder.writer(); + assert_ok!(handle.write_str(SoftAsciiStr::from_str("Header-One: 12\r\n").unwrap())); + handle.finish_header(); + } + assert_eq!(encoder.as_slice(), b"Header-One: 12\r\n"); + } + + #[test] + fn finish_does_truncat_if_needed() { + let mut encoder = EncodingBuffer::new(MailType::Ascii); + { + let mut handle = encoder.writer(); + assert_ok!(handle.write_str(SoftAsciiStr::from_str("Header-One: 12\r\n ").unwrap())); + handle.finish_header(); + } + assert_eq!(encoder.as_slice(), b"Header-One: 12\r\n"); + } + + + #[test] + fn finish_can_handle_fws() { + let mut en |