summaryrefslogtreecommitdiffstats
path: root/internals/src/encoder/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'internals/src/encoder/mod.rs')
-rw-r--r--internals/src/encoder/mod.rs1712
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