From da60dc53703cbae972033edd78abac7b52ffc7af Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Fri, 27 Mar 2026 08:37:08 +0000 Subject: [PATCH] mailparsing: switch parser input to byte slices There's a bit of fuss around migrating the `char` nom parsing function to `tag`, which is required for operating on bytes. We need to jump through some hoops in order to be able to show what tag was expected if there is a mismatch. That improves the output of our parse error type to have less redundant information, so overall I'd say that that is a plus. A number of 8-bit related tests now fail and some continue to fail from an earlier commit in this refactoring series. We can now start to tackle whether those need to be adjusted to the amended API surface, or whether we need to add some supporting context for those, on a case by case basis in follow-up commits. --- crates/kumod/src/http_server/inject_v1.rs | 2 +- crates/mailparsing/src/header.rs | 24 +- crates/mailparsing/src/mimepart.rs | 6 +- crates/mailparsing/src/nom_utils.rs | 62 +-- crates/mailparsing/src/rfc5322_parser.rs | 519 +++++++++++++--------- crates/mailparsing/src/strings.rs | 5 - 6 files changed, 353 insertions(+), 265 deletions(-) diff --git a/crates/kumod/src/http_server/inject_v1.rs b/crates/kumod/src/http_server/inject_v1.rs index 3a3e9d61..775db3e7 100644 --- a/crates/kumod/src/http_server/inject_v1.rs +++ b/crates/kumod/src/http_server/inject_v1.rs @@ -713,7 +713,7 @@ impl InjectV1Request { let opts = mailparsing::AttachmentOptions { file_name: a.file_name.clone().map(|s| s.into()), inline: a.content_id.is_some(), - content_id: a.content_id.clone(), + content_id: a.content_id.clone().map(Into::into), }; let decoded_data; diff --git a/crates/mailparsing/src/header.rs b/crates/mailparsing/src/header.rs index 52d7c106..5e19e9b7 100644 --- a/crates/mailparsing/src/header.rs +++ b/crates/mailparsing/src/header.rs @@ -184,57 +184,57 @@ impl<'a> Header<'a> { } pub fn as_content_transfer_encoding(&self) -> Result { - Parser::parse_content_transfer_encoding_header(self.get_raw_value_string()?) + Parser::parse_content_transfer_encoding_header(self.get_raw_value()) } pub fn as_content_disposition(&self) -> Result { - Parser::parse_content_transfer_encoding_header(self.get_raw_value_string()?) + Parser::parse_content_transfer_encoding_header(self.get_raw_value()) } pub fn as_content_type(&self) -> Result { - Parser::parse_content_type_header(self.get_raw_value_string()?) + Parser::parse_content_type_header(self.get_raw_value()) } /// Parse the header into a mailbox-list (as defined in /// RFC 5322), which is how the `From` and `Resent-From`, /// headers are defined. pub fn as_mailbox_list(&self) -> Result { - Parser::parse_mailbox_list_header(self.get_raw_value_string()?) + Parser::parse_mailbox_list_header(self.get_raw_value()) } /// Parse the header into a mailbox (as defined in /// RFC 5322), which is how the `Sender` and `Resent-Sender` /// headers are defined. pub fn as_mailbox(&self) -> Result { - Parser::parse_mailbox_header(self.get_raw_value_string()?) + Parser::parse_mailbox_header(self.get_raw_value()) } pub fn as_address_list(&self) -> Result { - Parser::parse_address_list_header(self.get_raw_value_string()?) + Parser::parse_address_list_header(self.get_raw_value()) } pub fn as_message_id(&self) -> Result { - Parser::parse_msg_id_header(self.get_raw_value_string()?) + Parser::parse_msg_id_header(self.get_raw_value()) } pub fn as_content_id(&self) -> Result { - Parser::parse_content_id_header(self.get_raw_value_string()?) + Parser::parse_content_id_header(self.get_raw_value()) } pub fn as_message_id_list(&self) -> Result> { - Parser::parse_msg_id_header_list(self.get_raw_value_string()?) + Parser::parse_msg_id_header_list(self.get_raw_value()) } pub fn as_unstructured(&self) -> Result { - Parser::parse_unstructured_header(self.get_raw_value_string()?) + Parser::parse_unstructured_header(self.get_raw_value()) } pub fn as_authentication_results(&self) -> Result { - Parser::parse_authentication_results_header(self.get_raw_value_string()?) + Parser::parse_authentication_results_header(self.get_raw_value()) } pub fn as_arc_authentication_results(&self) -> Result { - Parser::parse_arc_authentication_results_header(self.get_raw_value_string()?) + Parser::parse_arc_authentication_results_header(self.get_raw_value()) } pub fn as_date(&self) -> Result> { diff --git a/crates/mailparsing/src/mimepart.rs b/crates/mailparsing/src/mimepart.rs index 87676256..8dbdc03f 100644 --- a/crates/mailparsing/src/mimepart.rs +++ b/crates/mailparsing/src/mimepart.rs @@ -834,7 +834,7 @@ impl<'a> MimePart<'a> { headers.set_content_disposition(cd)?; if let Some(id) = &opts.content_id { - headers.set_content_id(MessageID(id.to_string()))?; + headers.set_content_id(MessageID(id.clone()))?; } } @@ -1142,7 +1142,7 @@ impl<'a> MimePart<'a> { if to_fix.contains(MessageConformance::MISSING_MESSAGE_ID_HEADER) { if let Some(message_id) = &settings.message_id { msg.headers_mut() - .set_message_id(MessageID(message_id.clone()))?; + .set_message_id(MessageID(message_id.clone().into()))?; } } @@ -1243,7 +1243,7 @@ pub struct AttachmentOptions { #[serde(default)] pub inline: bool, #[serde(default)] - pub content_id: Option, + pub content_id: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/mailparsing/src/nom_utils.rs b/crates/mailparsing/src/nom_utils.rs index 64270d61..8d9ef671 100644 --- a/crates/mailparsing/src/nom_utils.rs +++ b/crates/mailparsing/src/nom_utils.rs @@ -1,11 +1,12 @@ +use bstr::ByteSlice; use nom::error::{ContextError, ErrorKind}; use nom_locate::LocatedSpan; use std::fmt::{Debug, Write}; -pub(crate) type Span<'a> = LocatedSpan<&'a str>; +pub(crate) type Span<'a> = LocatedSpan<&'a [u8]>; pub(crate) type IResult<'a, A, B> = nom::IResult>>; -pub fn make_span(s: &'_ str) -> Span<'_> { +pub fn make_span(s: &'_ [u8]) -> Span<'_> { Span::new(s) } @@ -81,20 +82,22 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { match err { nom::Err::Error(e) => { let mut result = String::new(); - for (i, (span, kind)) in e.errors.iter().enumerate() { + let mut lines_shown = vec![]; + + for (span, kind) in e.errors.iter() { if input.is_empty() { match kind { ParseErrorKind::Char(c) => { - write!(&mut result, "{i}: expected '{c}', got empty input\n\n") + write!(&mut result, "Error expected '{c}', got empty input\n\n") } ParseErrorKind::Context(s) => { - write!(&mut result, "{i}: in {s}, got empty input\n\n") + write!(&mut result, "Error in {s}, got empty input\n\n") } ParseErrorKind::External { kind, reason } => { - write!(&mut result, "{i}: {reason} {kind:?}, got empty input\n\n") + write!(&mut result, "Error {reason} {kind:?}, got empty input\n\n") } ParseErrorKind::Nom(e) => { - write!(&mut result, "{i}: in {e:?}, got empty input\n\n") + write!(&mut result, "Error in {e:?}, got empty input\n\n") } } .ok(); @@ -102,21 +105,33 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { } let line_number = span.location_line(); - let line = std::str::from_utf8(span.get_line_beginning()) - .unwrap_or(""); + let input_line = span.get_line_beginning(); // Remap \t in particular, because it can render as multiple // columns and defeat the column number calculation provided // by the Span type - let line: String = line - .chars() - .map(|c| match c { + let mut line = String::new(); + for (start, end, c) in input_line.char_indices() { + let c = match c { '\t' => '\u{2409}', '\r' => '\u{240d}', '\n' => '\u{240a}', - _ => c, - }) - .collect(); + c => c, + }; + + if c == std::char::REPLACEMENT_CHARACTER { + let bytes = &input_line[start..end]; + for b in bytes.iter() { + line.push_str(&format!("\\x{b:02X}")); + } + } else { + line.push(c); + } + } + let column = span.get_utf8_column(); + + lines_shown.push(line_number); + let mut caret = " ".repeat(column.saturating_sub(1)); caret.push('^'); for _ in 1..span.fragment().len() { @@ -128,7 +143,7 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { if let Some(actual) = span.fragment().chars().next() { write!( &mut result, - "{i}: at line {line_number}:\n\ + "Error at line {line_number}:\n\ {line}\n\ {caret}\n\ expected '{expected}', found {actual}\n\n", @@ -136,7 +151,7 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { } else { write!( &mut result, - "{i}: at line {line_number}:\n\ + "Error at line {line_number}:\n\ {line}\n\ {caret}\n\ expected '{expected}', got end of input\n\n", @@ -144,17 +159,12 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { } } ParseErrorKind::Context(context) => { - write!( - &mut result, - "{i}: at line {line_number}, in {context}:\n\ - {line}\n\ - {caret}\n\n", - ) + write!(&mut result, "while parsing {context}\n") } - ParseErrorKind::External { kind, reason } => { + ParseErrorKind::External { kind: _, reason } => { write!( &mut result, - "{i}: at line {line_number}, {reason} {kind:?}:\n\ + "Error at line {line_number}, {reason}:\n\ {line}\n\ {caret}\n\n", ) @@ -162,7 +172,7 @@ pub fn explain_nom(input: Span, err: nom::Err>>) -> String { ParseErrorKind::Nom(nom_err) => { write!( &mut result, - "{i}: at line {line_number}, in {nom_err:?}:\n\ + "Error at line {line_number}, in {nom_err:?}:\n\ {line}\n\ {caret}\n\n", ) diff --git a/crates/mailparsing/src/rfc5322_parser.rs b/crates/mailparsing/src/rfc5322_parser.rs index 00d6a019..82e16c76 100644 --- a/crates/mailparsing/src/rfc5322_parser.rs +++ b/crates/mailparsing/src/rfc5322_parser.rs @@ -4,16 +4,16 @@ use crate::{MailParsingError, Result, SharedString}; use bstr::{BStr, BString, ByteSlice, ByteVec}; use charset_normalizer_rs::Encoding; use nom::branch::alt; -use nom::bytes::complete::{tag, take_while, take_while1}; -use nom::character::complete::{char, satisfy}; +use nom::bytes::complete::{take_while, take_while1, take_while_m_n}; use nom::combinator::{all_consuming, map, opt, recognize}; use nom::error::context; use nom::multi::{many0, many1, separated_list1}; use nom::sequence::{delimited, preceded, separated_pair, terminated}; -use nom::Parser as _; +use nom::{Compare, Input, Parser as _}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt::Debug; +use std::marker::PhantomData; impl MailParsingError { pub fn from_nom(input: Span, err: nom::Err>>) -> Self { @@ -21,55 +21,119 @@ impl MailParsingError { } } -fn is_utf8_non_ascii(c: char) -> bool { - let c = c as u32; +/// Like nom::bytes::complete::tag, except that we print what the tag +/// was expecting if there was an error. +/// I feel like this should be the default behavior TBH. +fn tag(tag: &'static str) -> TagParser { + TagParser { + tag, + e: PhantomData, + } +} + +/// Struct to support displaying better errors for tag() +struct TagParser { + tag: &'static str, + e: PhantomData, +} + +/// All this fuss to show what we expected for the TagParser impl +impl + nom::error::FromExternalError> nom::Parser + for TagParser +where + I: Input + Compare<&'static str> + nom::AsBytes, +{ + type Output = I; + type Error = Error; + + fn process( + &mut self, + i: I, + ) -> nom::PResult { + use nom::error::ErrorKind; + use nom::{CompareResult, Err, Mode}; + + let tag_len = self.tag.input_len(); + + match i.compare(self.tag) { + CompareResult::Ok => Ok((i.take_from(tag_len), OM::Output::bind(|| i.take(tag_len)))), + CompareResult::Incomplete => Err(Err::Error(OM::Error::bind(|| { + Error::from_external_error( + i, + ErrorKind::Fail, + format!( + "expected \"{}\" but ran out of input", + self.tag.escape_debug() + ), + ) + }))), + + CompareResult::Error => { + let available = i.take(i.input_len().min(tag_len)); + Err(Err::Error(OM::Error::bind(|| { + Error::from_external_error( + i, + ErrorKind::Fail, + format!( + "expected \"{}\" but found {:?}", + self.tag.escape_debug(), + BStr::new(available.as_bytes()) + ), + ) + }))) + } + } + } +} + +fn is_utf8_non_ascii(c: u8) -> bool { c == 0 || c >= 0x80 } // ctl = { '\u{00}'..'\u{1f}' | "\u{7f}" } -fn is_ctl(c: char) -> bool { +fn is_ctl(c: u8) -> bool { match c { - '\u{00}'..='\u{1f}' | '\u{7f}' => true, + b'\x00'..=b'\x1f' | b'\x7f' => true, _ => false, } } -fn not_angle(c: char) -> bool { +fn not_angle(c: u8) -> bool { match c { - '<' | '>' => false, + b'<' | b'>' => false, _ => true, } } // char = { '\u{01}'..'\u{7f}' } -fn is_char(c: char) -> bool { +fn is_char(c: u8) -> bool { match c { - '\u{01}'..='\u{ff}' => true, + 0x01..=0xff => true, _ => false, } } -fn is_especial(c: char) -> bool { +fn is_especial(c: u8) -> bool { match c { - '(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '/' | '[' | ']' | '?' | '.' | '=' => true, + b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'/' | b'[' | b']' | b'?' + | b'.' | b'=' => true, _ => false, } } -fn is_token(c: char) -> bool { - is_char(c) && c != ' ' && !is_especial(c) && !is_ctl(c) +fn is_token(c: u8) -> bool { + is_char(c) && c != b' ' && !is_especial(c) && !is_ctl(c) } // vchar = { '\u{21}'..'\u{7e}' | utf8_non_ascii } -fn is_vchar(c: char) -> bool { - let u = c as u32; - (0x21..=0x7e).contains(&u) || is_utf8_non_ascii(c) +fn is_vchar(c: u8) -> bool { + (0x21..=0x7e).contains(&c) || is_utf8_non_ascii(c) } -fn is_atext(c: char) -> bool { +fn is_atext(c: u8) -> bool { match c { - '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_' - | '`' | '{' | '|' | '}' | '~' => true, + b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'/' | b'=' | b'?' + | b'^' | b'_' | b'`' | b'{' | b'|' | b'}' | b'~' => true, c => c.is_ascii_alphanumeric() || is_utf8_non_ascii(c), } } @@ -78,64 +142,64 @@ fn atext(input: Span) -> IResult { context("atext", take_while1(is_atext)).parse(input) } -fn is_obs_no_ws_ctl(c: char) -> bool { +fn is_obs_no_ws_ctl(c: u8) -> bool { match c { - '\u{01}'..='\u{08}' | '\u{0b}'..='\u{0c}' | '\u{0e}'..='\u{1f}' | '\u{7f}' => true, + 0x01..=0x08 | 0x0b..=0x0c | 0x0e..=0x1f | 0x7f => true, _ => false, } } -fn is_obs_ctext(c: char) -> bool { +fn is_obs_ctext(c: u8) -> bool { is_obs_no_ws_ctl(c) } // ctext = { '\u{21}'..'\u{27}' | '\u{2a}'..'\u{5b}' | '\u{5d}'..'\u{7e}' | obs_ctext | utf8_non_ascii } -fn is_ctext(c: char) -> bool { +fn is_ctext(c: u8) -> bool { match c { - '\u{21}'..='\u{27}' | '\u{2a}'..='\u{5b}' | '\u{5d}'..='\u{7e}' => true, + 0x21..=0x27 | 0x2a..=0x5b | 0x5d..=0x7e => true, c => is_obs_ctext(c) || is_utf8_non_ascii(c), } } // dtext = { '\u{21}'..'\u{5a}' | '\u{5e}'..'\u{7e}' | obs_dtext | utf8_non_ascii } // obs_dtext = { obs_no_ws_ctl | quoted_pair } -fn is_dtext(c: char) -> bool { +fn is_dtext(c: u8) -> bool { match c { - '\u{21}'..='\u{5a}' | '\u{5e}'..='\u{7e}' => true, + 0x21..=0x5a | 0x5e..=0x7e => true, c => is_obs_no_ws_ctl(c) || is_utf8_non_ascii(c), } } // qtext = { "\u{21}" | '\u{23}'..'\u{5b}' | '\u{5d}'..'\u{7e}' | obs_qtext | utf8_non_ascii } // obs_qtext = { obs_no_ws_ctl } -fn is_qtext(c: char) -> bool { +fn is_qtext(c: u8) -> bool { match c { - '\u{21}' | '\u{23}'..='\u{5b}' | '\u{5d}'..='\u{7e}' => true, + 0x21 | 0x23..=0x5b | 0x5d..=0x7e => true, c => is_obs_no_ws_ctl(c) || is_utf8_non_ascii(c), } } -fn is_tspecial(c: char) -> bool { +fn is_tspecial(c: u8) -> bool { match c { - '(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '\\' | '"' | '/' | '[' | ']' | '?' - | '=' => true, + b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'\\' | b'"' | b'/' | b'[' + | b']' | b'?' | b'=' => true, _ => false, } } -fn is_attribute_char(c: char) -> bool { +fn is_attribute_char(c: u8) -> bool { match c { - ' ' | '*' | '\'' | '%' => false, + b' ' | b'*' | b'\'' | b'%' => false, _ => is_char(c) && !is_ctl(c) && !is_tspecial(c), } } fn wsp(input: Span) -> IResult { - context("wsp", take_while1(|c| c == ' ' || c == '\t')).parse(input) + context("wsp", take_while1(|c| c == b' ' || c == b'\t')).parse(input) } fn newline(input: Span) -> IResult { - context("newline", recognize(preceded(opt(char('\r')), char('\n')))).parse(input) + context("newline", recognize(preceded(opt(tag("\r")), tag("\n")))).parse(input) } // fws = { ((wsp* ~ "\r"? ~ "\n")* ~ wsp+) | obs_fws } @@ -163,7 +227,7 @@ fn obs_fws(input: Span) -> IResult { fn mailbox_list(input: Span) -> IResult { let (loc, mailboxes) = context( "mailbox_list", - alt((separated_list1(char(','), mailbox), obs_mbox_list)), + alt((separated_list1(tag(","), mailbox), obs_mbox_list)), ) .parse(input)?; Ok((loc, MailboxList(mailboxes))) @@ -174,11 +238,11 @@ fn obs_mbox_list(input: Span) -> IResult> { let (loc, entries) = context( "obs_mbox_list", many1(preceded( - many0(preceded(opt(cfws), char(','))), + many0(preceded(opt(cfws), tag(","))), ( mailbox, many0(preceded( - char(','), + tag(","), alt((map(mailbox, Some), map(cfws, |_| None))), )), ), @@ -221,7 +285,7 @@ fn address_list(input: Span) -> IResult { context( "address_list", alt(( - map(separated_list1(char(','), address), AddressList), + map(separated_list1(tag(","), address), AddressList), obs_address_list, )), ) @@ -233,11 +297,11 @@ fn obs_address_list(input: Span) -> IResult { let (loc, entries) = context( "obs_address_list", many1(preceded( - many0(preceded(opt(cfws), char(','))), + many0(preceded(opt(cfws), tag(","))), ( address, many0(preceded( - char(','), + tag(","), alt((map(address, Some), map(cfws, |_| None))), )), ), @@ -269,7 +333,7 @@ fn group(input: Span) -> IResult { let (loc, (name, _, group_list, _)) = context( "group", terminated( - (display_name, char(':'), opt(group_list), char(';')), + (display_name, tag(":"), opt(group_list), tag(";")), opt(cfws), ), ) @@ -301,7 +365,7 @@ fn obs_group_list(input: Span) -> IResult { context( "obs_group_list", map( - terminated(many1(preceded(opt(cfws), char(','))), opt(cfws)), + terminated(many1(preceded(opt(cfws), tag(","))), opt(cfws)), |_| MailboxList(vec![]), ), ) @@ -336,7 +400,7 @@ fn phrase(input: Span) -> IResult { map(cfws, |_| None), map(encoded_word, Option::Some), map(word, Option::Some), - map(char('.'), |_dot| Some(BString::from("."))), + map(tag("."), |_dot| Some(BString::from("."))), ))), ), ) @@ -358,7 +422,7 @@ fn angle_addr(input: Span) -> IResult { alt(( delimited( opt(cfws), - delimited(char('<'), addr_spec, char('>')), + delimited(tag("<"), addr_spec, tag(">")), opt(cfws), ), obs_angle_addr, @@ -373,7 +437,7 @@ fn obs_angle_addr(input: Span) -> IResult { "obs_angle_addr", delimited( opt(cfws), - delimited(char('<'), preceded(obs_route, addr_spec), char('>')), + delimited(tag("<"), preceded(obs_route, addr_spec), tag(">")), opt(cfws), ), ) @@ -387,12 +451,12 @@ fn obs_route(input: Span) -> IResult { "obs_route", recognize(terminated( ( - many0(alt((cfws, recognize(char(','))))), - recognize(char('@')), + many0(alt((cfws, recognize(tag(","))))), + recognize(tag("@")), recognize(domain), - many0((char(','), opt(cfws), opt((char('@'), domain)))), + many0((tag(","), opt(cfws), opt((tag("@"), domain)))), ), - char(':'), + tag(":"), )), ) .parse(input) @@ -401,7 +465,7 @@ fn obs_route(input: Span) -> IResult { // addr_spec = { local_part ~ "@" ~ domain } fn addr_spec(input: Span) -> IResult { let (loc, (local_part, domain)) = - context("addr_spec", separated_pair(local_part, char('@'), domain)).parse(input)?; + context("addr_spec", separated_pair(local_part, tag("@"), domain)).parse(input)?; Ok(( loc, AddrSpec { @@ -411,7 +475,7 @@ fn addr_spec(input: Span) -> IResult { )) } -fn parse_with<'a, R, F>(text: &'a str, parser: F) -> Result +fn parse_with<'a, R, F>(text: &'a [u8], parser: F) -> Result where F: Fn(Span<'a>) -> IResult<'a, Span<'a>, R>, { @@ -426,7 +490,7 @@ where #[test] fn test_addr_spec() { k9::snapshot!( - parse_with("darth.vader@a.galaxy.far.far.away", addr_spec), + parse_with("darth.vader@a.galaxy.far.far.away".as_bytes(), addr_spec), r#" Ok( AddrSpec { @@ -438,7 +502,10 @@ Ok( ); k9::snapshot!( - parse_with("\"darth.vader\"@a.galaxy.far.far.away", addr_spec), + parse_with( + "\"darth.vader\"@a.galaxy.far.far.away".as_bytes(), + addr_spec + ), r#" Ok( AddrSpec { @@ -450,19 +517,18 @@ Ok( ); k9::snapshot!( - parse_with("\"darth\".vader@a.galaxy.far.far.away", addr_spec), + parse_with( + "\"darth\".vader@a.galaxy.far.far.away".as_bytes(), + addr_spec + ), r#" Err( HeaderParse( - "0: at line 1: + "Error at line 1, expected "@" but found ".": "darth".vader@a.galaxy.far.far.away ^___________________________ -expected '@', found . - -1: at line 1, in addr_spec: -"darth".vader@a.galaxy.far.far.away -^__________________________________ +while parsing addr_spec ", ), ) @@ -470,7 +536,7 @@ expected '@', found . ); k9::snapshot!( - parse_with("a@[127.0.0.1]", addr_spec), + parse_with("a@[127.0.0.1]".as_bytes(), addr_spec), r#" Ok( AddrSpec { @@ -482,7 +548,7 @@ Ok( ); k9::snapshot!( - parse_with("a@[IPv6::1]", addr_spec), + parse_with("a@[IPv6::1]".as_bytes(), addr_spec), r#" Ok( AddrSpec { @@ -508,11 +574,11 @@ fn word(input: Span) -> IResult { // obs_local_part = { word ~ (dot ~ word)* } fn obs_local_part(input: Span) -> IResult { let (loc, (word, dotted_words)) = - context("obs_local_part", (word, many0((char('.'), word)))).parse(input)?; + context("obs_local_part", (word, many0((tag("."), word)))).parse(input)?; let mut result = word; - for (dot, w) in dotted_words { - result.push_char(dot); + for (_dot, w) in dotted_words { + result.push(b'.'); result.push_str(&w); } @@ -532,11 +598,11 @@ fn domain(input: Span) -> IResult { // obs_domain = { atom ~ ( dot ~ atom)* } fn obs_domain(input: Span) -> IResult { let (loc, (atom, dotted_atoms)) = - context("obs_domain", (atom, many0((char('.'), atom)))).parse(input)?; + context("obs_domain", (atom, many0((tag("."), atom)))).parse(input)?; let mut result = atom; - for (dot, w) in dotted_atoms { - result.push_char(dot); + for (_dot, w) in dotted_atoms { + result.push(b'.'); result.push_str(&w); } @@ -550,12 +616,12 @@ fn domain_literal(input: Span) -> IResult { delimited( opt(cfws), delimited( - char('['), + tag("["), ( - many0((opt(fws), alt((satisfy(is_dtext), quoted_pair)))), + many0((opt(fws), alt((take_while_m_n(1, 1, is_dtext), quoted_pair)))), opt(fws), ), - char(']'), + tag("]"), ), opt(cfws), ), @@ -568,7 +634,7 @@ fn domain_literal(input: Span) -> IResult { if let Some(a) = a { result.push_str(&a); } - result.push_char(b); + result.push_str(b); } if let Some(t) = trailer { result.push_str(&t); @@ -580,7 +646,7 @@ fn domain_literal(input: Span) -> IResult { // dot_atom_text = @{ atext ~ ("." ~ atext)* } fn dot_atom_text(input: Span) -> IResult { let (loc, (a, b)) = - context("dot_atom_text", (atext, many0(preceded(char('.'), atext)))).parse(input)?; + context("dot_atom_text", (atext, many0(preceded(tag("."), atext)))).parse(input)?; let mut result: BString = (*a).into(); for item in b { result.push(b'.'); @@ -599,7 +665,7 @@ fn dot_atom(input: Span) -> IResult { #[test] fn test_dot_atom() { k9::snapshot!( - parse_with("hello", dot_atom), + parse_with("hello".as_bytes(), dot_atom), r#" Ok( "hello", @@ -608,7 +674,7 @@ Ok( ); k9::snapshot!( - parse_with("hello.there", dot_atom), + parse_with("hello.there".as_bytes(), dot_atom), r#" Ok( "hello.there", @@ -617,11 +683,11 @@ Ok( ); k9::snapshot!( - parse_with("hello.", dot_atom), + parse_with("hello.".as_bytes(), dot_atom), r#" Err( HeaderParse( - "0: at line 1, in Eof: + "Error at line 1, in Eof: hello. ^ @@ -632,7 +698,7 @@ hello. ); k9::snapshot!( - parse_with("(wat)hello", dot_atom), + parse_with("(wat)hello".as_bytes(), dot_atom), r#" Ok( "hello", @@ -657,7 +723,7 @@ fn cfws(input: Span) -> IResult { fn comment(input: Span) -> IResult { context( "comment", - recognize((char('('), many0((opt(fws), ccontent)), opt(fws), char(')'))), + recognize((tag("("), many0((opt(fws), ccontent)), opt(fws), tag(")"))), ) .parse(input) } @@ -666,17 +732,8 @@ fn comment(input: Span) -> IResult { #[test] fn test_comment() { k9::snapshot!( - parse_with("(wat)", comment), - r#" -Ok( - LocatedSpan { - offset: 0, - line: 1, - fragment: "(wat)", - extra: (), - }, -) -"# + BStr::new(&parse_with("(wat)".as_bytes(), comment).unwrap()), + "(wat)" ); } @@ -685,7 +742,7 @@ fn ccontent(input: Span) -> IResult { context( "ccontent", recognize(alt(( - recognize(satisfy(is_ctext)), + recognize(take_while_m_n(1, 1, is_ctext)), recognize(quoted_pair), comment, recognize(encoded_word), @@ -694,17 +751,21 @@ fn ccontent(input: Span) -> IResult { .parse(input) } -fn is_quoted_pair(c: char) -> bool { +fn is_quoted_pair(c: u8) -> bool { match c { - '\u{00}' | '\r' | '\n' | ' ' => true, + 0x00 | b'\r' | b'\n' | b' ' => true, c => is_obs_no_ws_ctl(c) || is_vchar(c), } } // quoted_pair = { ( "\\" ~ (vchar | wsp)) | obs_qp } // obs_qp = { "\\" ~ ( "\u{00}" | obs_no_ws_ctl | "\r" | "\n") } -fn quoted_pair(input: Span) -> IResult { - context("quoted_pair", preceded(char('\\'), satisfy(is_quoted_pair))).parse(input) +fn quoted_pair(input: Span) -> IResult { + context( + "quoted_pair", + preceded(tag("\\"), take_while_m_n(1, 1, is_quoted_pair)), + ) + .parse(input) } // encoded_word = { "=?" ~ charset ~ ("*" ~ language)? ~ "?" ~ encoding ~ "?" ~ encoded_text ~ "?=" } @@ -715,10 +776,10 @@ fn encoded_word(input: Span) -> IResult { tag("=?"), ( charset, - opt(preceded(char('*'), language)), - char('?'), + opt(preceded(tag("*"), language)), + tag("?"), encoding, - char('?'), + tag("?"), encoded_text, ), tag("?="), @@ -727,7 +788,7 @@ fn encoded_word(input: Span) -> IResult { .parse(input)?; let bytes = match *encoding.fragment() { - "B" | "b" => data_encoding::BASE64_MIME + b"B" | b"b" => data_encoding::BASE64_MIME .decode(text.as_bytes()) .map_err(|err| { make_context_error( @@ -735,12 +796,12 @@ fn encoded_word(input: Span) -> IResult { format!("encoded_word: base64 decode failed: {err:#}"), ) })?, - "Q" | "q" => { + b"Q" | b"q" => { // for rfc2047 header encoding, _ can be used to represent a space let munged = text.replace("_", " "); // The quoted_printable crate will unhelpfully strip trailing space // from the decoded input string, and we must track and restore it - let had_trailing_space = munged.ends_with(' '); + let had_trailing_space = munged.ends_with_str(" "); let mut decoded = quoted_printable::decode(munged, quoted_printable::ParseMode::Robust) .map_err(|err| { make_context_error( @@ -754,6 +815,7 @@ fn encoded_word(input: Span) -> IResult { decoded } encoding => { + let encoding = BStr::new(encoding); return Err(make_context_error( input, format!( @@ -763,17 +825,27 @@ fn encoded_word(input: Span) -> IResult { } }; - let charset = Encoding::by_name(&*charset).ok_or_else(|| { + let charset_name = charset.to_str().map_err(|err| { make_context_error( input, - format!("encoded_word: unsupported charset '{charset}'"), + format!( + "encoded_word: charset {} is not UTF-8: {err}", + BStr::new(*charset) + ), + ) + })?; + + let charset = Encoding::by_name(&*charset_name).ok_or_else(|| { + make_context_error( + input, + format!("encoded_word: unsupported charset '{charset_name}'"), ) })?; let decoded = charset.decode_simple(&bytes).map_err(|err| { make_context_error( input, - format!("encoded_word: failed to decode as '{charset}': {err}"), + format!("encoded_word: failed to decode as '{charset_name}': {err}"), ) })?; @@ -782,24 +854,24 @@ fn encoded_word(input: Span) -> IResult { // charset = @{ (!"*" ~ token)+ } fn charset(input: Span) -> IResult { - context("charset", take_while1(|c| c != '*' && is_token(c))).parse(input) + context("charset", take_while1(|c| c != b'*' && is_token(c))).parse(input) } // language = @{ token+ } fn language(input: Span) -> IResult { - context("language", take_while1(|c| c != '*' && is_token(c))).parse(input) + context("language", take_while1(|c| c != b'*' && is_token(c))).parse(input) } // encoding = @{ token+ } fn encoding(input: Span) -> IResult { - context("encoding", take_while1(|c| c != '*' && is_token(c))).parse(input) + context("encoding", take_while1(|c| c != b'*' && is_token(c))).parse(input) } // encoded_text = @{ (!( " " | "?") ~ vchar)+ } fn encoded_text(input: Span) -> IResult { context( "encoded_text", - take_while1(|c| is_vchar(c) && c != ' ' && c != '?'), + take_while1(|c| is_vchar(c) && c != b' ' && c != b'?'), ) .parse(input) } @@ -811,9 +883,9 @@ fn quoted_string(input: Span) -> IResult { delimited( opt(cfws), delimited( - char('"'), + tag("\""), (many0((opt(fws), qcontent)), opt(fws)), - char('"'), + tag("\""), ), opt(cfws), ), @@ -825,7 +897,7 @@ fn quoted_string(input: Span) -> IResult { if let Some(a) = a { result.push_str(&a); } - result.push_char(b); + result.push_str(b); } if let Some(t) = trailer { result.push_str(&t); @@ -834,8 +906,12 @@ fn quoted_string(input: Span) -> IResult { } // qcontent = { qtext | quoted_pair } -fn qcontent(input: Span) -> IResult { - context("qcontent", alt((satisfy(is_qtext), quoted_pair))).parse(input) +fn qcontent(input: Span) -> IResult { + context( + "qcontent", + alt((take_while_m_n(1, 1, is_qtext), quoted_pair)), + ) + .parse(input) } fn content_id(input: Span) -> IResult { @@ -852,14 +928,19 @@ fn relaxed_msg_id(input: Span) -> IResult { let (loc, id) = context( "msg_id", delimited( - preceded(opt(cfws), char('<')), - many0(satisfy(not_angle)), - preceded(char('>'), opt(cfws)), + preceded(opt(cfws), tag("<")), + many0(take_while_m_n(1, 1, not_angle)), + preceded(tag(">"), opt(cfws)), ), ) .parse(input)?; - Ok((loc, MessageID(id.into_iter().collect()))) + let mut result = BString::default(); + for item in id.into_iter() { + result.push_str(*item); + } + + Ok((loc, MessageID(result))) } // msg_id_list = { msg_id+ } @@ -896,14 +977,18 @@ fn strict_msg_id(input: Span) -> IResult { let (loc, (left, _, right)) = context( "msg_id", delimited( - preceded(opt(cfws), char('<')), - (id_left, char('@'), id_right), - preceded(char('>'), opt(cfws)), + preceded(opt(cfws), tag("<")), + (id_left, tag("@"), id_right), + preceded(tag(">"), opt(cfws)), ), ) .parse(input)?; - Ok((loc, MessageID(format!("{left}@{right}")))) + let mut result: BString = left.into(); + result.push_char('@'); + result.push_str(right); + + Ok((loc, MessageID(result))) } // obs_unstruct = { (( "\r"* ~ "\n"* ~ ((encoded_word | obs_utext)~ "\r"* ~ "\n"*)+) | fws)+ } @@ -911,7 +996,7 @@ fn unstructured(input: Span) -> IResult { #[derive(Debug)] enum Word { Encoded(BString), - UText(char), + UText(BString), Fws, } @@ -919,13 +1004,13 @@ fn unstructured(input: Span) -> IResult { "unstructured", many0(alt(( preceded( - map(take_while(|c| c == '\r' || c == '\n'), |_| Word::Fws), + map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws), terminated( alt(( map(encoded_word, Word::Encoded), - map(obs_utext, Word::UText), + map(obs_utext, |s| Word::UText((*s).into())), )), - map(take_while(|c| c == '\r' || c == '\n'), |_| Word::Fws), + map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws), ), ), map(fws, |_| Word::Fws), @@ -959,7 +1044,7 @@ fn unstructured(input: Span) -> IResult { } } Word::UText(c) => match processed.last_mut() { - Some(ProcessedWord::Text(prior)) => prior.push_char(c), + Some(ProcessedWord::Text(prior)) => prior.push_str(c), _ => processed.push(ProcessedWord::Text(c.to_string().into())), }, } @@ -985,10 +1070,10 @@ fn arc_authentication_results(input: Span) -> IResult IResult { fn no_result(input: Span) -> IResult> { context( "no_result", - map((opt(cfws), char(';'), opt(cfws), tag("none")), |_| vec![]), + map((opt(cfws), tag(";"), opt(cfws), tag("none")), |_| vec![]), ) .parse(input) } @@ -1039,7 +1124,7 @@ fn resinfo(input: Span) -> IResult { map( ( opt(cfws), - char(';'), + tag(";"), methodspec, opt(preceded(cfws, reasonspec)), opt(many1(propspec)), @@ -1067,7 +1152,7 @@ fn methodspec(input: Span) -> IResult, BString)> { opt(cfws), (keyword, opt(methodversion)), opt(cfws), - char('='), + tag("="), opt(cfws), keyword, ), @@ -1083,7 +1168,7 @@ fn keyword(input: Span) -> IResult { context( "keyword", map( - take_while1(|c: char| c.is_ascii_alphanumeric() || c == '+' || c == '-'), + take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'+' || c == b'-'), |s: Span| (*s).into(), ), ) @@ -1094,7 +1179,7 @@ fn methodversion(input: Span) -> IResult { context( "methodversion", preceded( - (opt(cfws), char('/'), opt(cfws)), + (opt(cfws), tag("/"), opt(cfws)), nom::character::complete::u32, ), ) @@ -1105,7 +1190,7 @@ fn reasonspec(input: Span) -> IResult { context( "reason", map( - (tag("reason"), opt(cfws), char('='), opt(cfws), value), + (tag("reason"), opt(cfws), tag("="), opt(cfws), value), |(_, _, _, _, value)| value, ), ) @@ -1120,19 +1205,19 @@ fn propspec(input: Span) -> IResult { opt(cfws), keyword, opt(cfws), - char('.'), + tag("."), opt(cfws), keyword, opt(cfws), - char('='), + tag("="), opt(cfws), alt(( - map(preceded(char('@'), domain), |d| { + map(preceded(tag("@"), domain), |d| { let mut at_dom = BString::from("@"); at_dom.push_str(d); at_dom }), - map(separated_pair(local_part, char('@'), domain), |(u, d)| { + map(separated_pair(local_part, tag("@"), domain), |(u, d)| { let mut result: BString = u.into(); result.push(b'@'); result.push_str(d); @@ -1153,16 +1238,16 @@ fn propspec(input: Span) -> IResult { } // obs_utext = @{ "\u{00}" | obs_no_ws_ctl | vchar } -fn obs_utext(input: Span) -> IResult { +fn obs_utext(input: Span) -> IResult { context( "obs_utext", - satisfy(|c| c == '\u{00}' || is_obs_no_ws_ctl(c) || is_vchar(c)), + take_while_m_n(1, 1, |c| c == 0x00 || is_obs_no_ws_ctl(c) || is_vchar(c)), ) .parse(input) } -fn is_mime_token(c: char) -> bool { - is_char(c) && c != ' ' && !is_ctl(c) && !is_tspecial(c) +fn is_mime_token(c: u8) -> bool { + is_char(c) && c != b' ' && !is_ctl(c) && !is_tspecial(c) } // mime_token = { (!(" " | ctl | tspecials) ~ char)+ } @@ -1182,7 +1267,7 @@ fn content_type(input: Span) -> IResult { ( mime_token, opt(cfws), - char('/'), + tag("/"), opt(cfws), mime_token, opt(cfws), @@ -1194,7 +1279,7 @@ fn content_type(input: Span) -> IResult { // In the meantime, there are implementations that assume // that the `;` is optional, so we therefore allow them // to be optional here in our implementation - preceded(opt(char(';')), opt(cfws)), + preceded(opt(tag(";")), opt(cfws)), terminated(parameter, opt(cfws)), )), ), @@ -1202,14 +1287,11 @@ fn content_type(input: Span) -> IResult { ) .parse(input)?; - let value = format!("{mime_type}/{mime_subtype}"); - Ok(( - loc, - MimeParameters { - value: value.into(), - parameters, - }, - )) + let mut value: BString = (*mime_type).into(); + value.push_char('/'); + value.push_str(mime_subtype); + + Ok((loc, MimeParameters { value, parameters })) } fn content_transfer_encoding(input: Span) -> IResult { @@ -1228,7 +1310,7 @@ fn content_transfer_encoding(input: Span) -> IResult { // In the meantime, there are implementations that assume // that the `;` is optional, so we therefore allow them // to be optional here in our implementation - preceded(opt(char(';')), opt(cfws)), + preceded(opt(tag(";")), opt(cfws)), terminated(parameter, opt(cfws)), )), ), @@ -1268,7 +1350,7 @@ fn param_with_unquoted_rfc2047(input: Span) -> IResult { context( "param_with_unquoted_rfc2047", map( - (attribute, opt(cfws), char('='), opt(cfws), encoded_word), + (attribute, opt(cfws), tag("="), opt(cfws), encoded_word), |(name, _, _, _, value)| MimeParameter { name: name.as_bytes().into(), value: value.as_bytes().into(), @@ -1289,9 +1371,9 @@ fn param_with_quoted_rfc2047(input: Span) -> IResult { ( attribute, opt(cfws), - char('='), + tag("="), opt(cfws), - delimited(char('"'), encoded_word, char('"')), + delimited(tag("\""), encoded_word, tag("\"")), ), |(name, _, _, _, value)| MimeParameter { name: name.as_bytes().into(), @@ -1313,17 +1395,17 @@ fn extended_param_with_charset(input: Span) -> IResult { ( attribute, opt(section), - char('*'), + tag("*"), opt(cfws), - char('='), + tag("="), opt(cfws), opt(mime_charset), - char('\''), + tag("'"), opt(mime_language), - char('\''), + tag("'"), map( recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))), - |s: Span| s.to_string(), + |s: Span| (*s).into(), ), ), |(name, section, _, _, _, _, mime_charset, _, mime_language, _, value)| MimeParameter { @@ -1332,7 +1414,7 @@ fn extended_param_with_charset(input: Span) -> IResult { mime_charset: mime_charset.map(|s| s.as_bytes().into()), mime_language: mime_language.map(|s| s.as_bytes().into()), encoding: MimeParameterEncoding::Rfc2231, - value: value.into(), + value, }, ), ) @@ -1346,15 +1428,15 @@ fn extended_param_no_charset(input: Span) -> IResult { ( attribute, opt(section), - opt(char('*')), + opt(tag("*")), opt(cfws), - char('='), + tag("="), opt(cfws), alt(( quoted_string, map( recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))), - |s: Span| s.to_string().into(), + |s: Span| (*s).into(), ), )), ), @@ -1378,7 +1460,7 @@ fn extended_param_no_charset(input: Span) -> IResult { fn mime_charset(input: Span) -> IResult { context( "mime_charset", - take_while1(|c| is_mime_token(c) && c != '\''), + take_while1(|c| is_mime_token(c) && c != b'\''), ) .parse(input) } @@ -1386,7 +1468,7 @@ fn mime_charset(input: Span) -> IResult { fn mime_language(input: Span) -> IResult { context( "mime_language", - take_while1(|c| is_mime_token(c) && c != '\''), + take_while1(|c| is_mime_token(c) && c != b'\''), ) .parse(input) } @@ -1395,9 +1477,8 @@ fn ext_octet(input: Span) -> IResult { context( "ext_octet", recognize(( - char('%'), - satisfy(|c| c.is_ascii_hexdigit()), - satisfy(|c| c.is_ascii_hexdigit()), + tag("%"), + take_while_m_n(2, 2, |b: u8| b.is_ascii_hexdigit()), )), ) .parse(input) @@ -1405,11 +1486,7 @@ fn ext_octet(input: Span) -> IResult { // section = { "*" ~ ASCII_DIGIT+ } fn section(input: Span) -> IResult { - context( - "section", - preceded(char('*'), nom::character::complete::u32), - ) - .parse(input) + context("section", preceded(tag("*"), nom::character::complete::u32)).parse(input) } // regular_parameter = { attribute ~ cfws? ~ "=" ~ cfws? ~ value } @@ -1417,7 +1494,7 @@ fn regular_parameter(input: Span) -> IResult { context( "regular_parameter", map( - (attribute, opt(cfws), char('='), opt(cfws), value), + (attribute, opt(cfws), tag("="), opt(cfws), value), |(name, _, _, _, value)| MimeParameter { name: name.as_bytes().into(), value: value.as_bytes().into(), @@ -1448,47 +1525,49 @@ fn value(input: Span) -> IResult { pub struct Parser; impl Parser { - pub fn parse_mailbox_list_header(text: &str) -> Result { + pub fn parse_mailbox_list_header(text: &[u8]) -> Result { parse_with(text, mailbox_list) } - pub fn parse_mailbox_header(text: &str) -> Result { + pub fn parse_mailbox_header(text: &[u8]) -> Result { parse_with(text, mailbox) } - pub fn parse_address_list_header(text: &str) -> Result { + pub fn parse_address_list_header(text: &[u8]) -> Result { parse_with(text, address_list) } - pub fn parse_msg_id_header(text: &str) -> Result { + pub fn parse_msg_id_header(text: &[u8]) -> Result { parse_with(text, msg_id) } - pub fn parse_msg_id_header_list(text: &str) -> Result> { + pub fn parse_msg_id_header_list(text: &[u8]) -> Result> { parse_with(text, msg_id_list) } - pub fn parse_content_id_header(text: &str) -> Result { + pub fn parse_content_id_header(text: &[u8]) -> Result { parse_with(text, content_id) } - pub fn parse_content_type_header(text: &str) -> Result { + pub fn parse_content_type_header(text: &[u8]) -> Result { parse_with(text, content_type) } - pub fn parse_content_transfer_encoding_header(text: &str) -> Result { + pub fn parse_content_transfer_encoding_header(text: &[u8]) -> Result { parse_with(text, content_transfer_encoding) } - pub fn parse_unstructured_header(text: &str) -> Result { + pub fn parse_unstructured_header(text: &[u8]) -> Result { parse_with(text, unstructured) } - pub fn parse_authentication_results_header(text: &str) -> Result { + pub fn parse_authentication_results_header(text: &[u8]) -> Result { parse_with(text, authentication_results) } - pub fn parse_arc_authentication_results_header(text: &str) -> Result { + pub fn parse_arc_authentication_results_header( + text: &[u8], + ) -> Result { parse_with(text, arc_authentication_results) } } @@ -1549,7 +1628,7 @@ pub struct AuthenticationResults { /// Emits a value that was parsed by `value`, into target fn emit_value_token(value: &[u8], target: &mut Vec) { - let use_quoted_string = !value.iter().all(|&c| is_mime_token(c as char) || c == b'@'); + let use_quoted_string = !value.iter().all(|&c| is_mime_token(c) || c == b'@'); if use_quoted_string { target.push(b'"'); for (start, end, c) in value.char_indices() { @@ -1625,7 +1704,7 @@ impl AddrSpec { } pub fn parse(email: &str) -> Result { - parse_with(email, addr_spec) + parse_with(email.as_bytes(), addr_spec) } } @@ -1633,10 +1712,7 @@ impl EncodeHeaderValue for AddrSpec { fn encode_value(&self) -> SharedString<'static> { let mut result: Vec = vec![]; - let needs_quoting = !self - .local_part - .iter() - .all(|&c| is_atext(c as char) || c == b'.'); + let needs_quoting = !self.local_part.iter().all(|&c| is_atext(c) || c == b'.'); if needs_quoting { result.push(b'"'); // RFC5321 4.1.2 qtextSMTP: @@ -1714,22 +1790,28 @@ pub struct Mailbox { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] -pub struct MessageID(pub String); +pub struct MessageID(pub BString); impl EncodeHeaderValue for MessageID { fn encode_value(&self) -> SharedString<'static> { - format!("<{}>", self.0).into() + let mut result = Vec::::with_capacity(self.0.len() + 2); + result.push(b'<'); + result.push_str(&self.0); + result.push(b'>'); + result.into() } } impl EncodeHeaderValue for Vec { fn encode_value(&self) -> SharedString<'static> { - let mut result = String::new(); + let mut result = BString::default(); for id in self { if !result.is_empty() { result.push_str("\r\n\t"); } - result.push_str(&format!("<{}>", id.0)); + result.push(b'<'); + result.push_str(&id.0); + result.push(b'>'); } result.into() } @@ -1960,12 +2042,12 @@ impl EncodeHeaderValue for MimeParameters { result.push_str(&format!(";\r\n\t{name}=\"{encoded}\"")); } MimeParameterEncoding::None | MimeParameterEncoding::Rfc2231 => { - let needs_encoding = value.chars().any(|c| !is_mime_token(c) || !c.is_ascii()); + let needs_encoding = value.iter().any(|&c| !is_mime_token(c) || !c.is_ascii()); // Prefer to use quoted_string representation when possible, as it doesn't // require any RFC 2231 encoding let use_quoted_string = value - .chars() - .all(|c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii()); + .iter() + .all(|&c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii()); let mut params = vec![]; let mut chars = value.char_indices().peekable(); @@ -1994,7 +2076,10 @@ impl EncodeHeaderValue for MimeParameters { encoded.push(b'\\'); } encoded.push_str(s); - } else if is_mime_token(c) && (!needs_encoding || c != '%') { + } else if (c as u32) <= 0xff + && is_mime_token(c as u32 as u8) + && (!needs_encoding || c != '%') + { encoded.push_str(s); } else { for b in s.bytes() { @@ -2175,12 +2260,17 @@ fn test_qp_encode() { /// string is enclosed in quotes. fn quote_string(s: impl AsRef<[u8]>) -> BString { let s = s.as_ref(); - if s.chars().any(|c| !is_atext(c)) { + + if s.iter().any(|&c| !is_atext(c)) { let mut result = Vec::::with_capacity(s.len() + 4); result.push(b'"'); for (start, end, c) in s.char_indices() { - if !c.is_ascii_whitespace() && !is_qtext(c) && !is_atext(c) { - result.push(b'\\'); + let c = c as u32; + if c <= 0xff { + let c = c as u8; + if !c.is_ascii_whitespace() && !is_qtext(c) && !is_atext(c) { + result.push(b'\\'); + } } result.push_str(&s[start..end]); } @@ -2357,19 +2447,12 @@ Some( InvalidHeaderValueDuringGet { header_name: "Sender", error: HeaderParse( - "0: at line 1: + "Error at line 1, expected "@" but found ".": hello..there@docomo.ne.jp ^___________________ -expected '@', found . - -1: at line 1, in addr_spec: -hello..there@docomo.ne.jp -^________________________ - -2: at line 1, in mailbox: -hello..there@docomo.ne.jp -^________________________ +while parsing addr_spec +while parsing mailbox ", ), } diff --git a/crates/mailparsing/src/strings.rs b/crates/mailparsing/src/strings.rs index a8f09daa..ff965bd4 100644 --- a/crates/mailparsing/src/strings.rs +++ b/crates/mailparsing/src/strings.rs @@ -107,11 +107,6 @@ impl SharedString<'_> { String::from_utf8_lossy(self.as_bytes()) } - #[deprecated = "use either as_bytes() or to_str() instead"] - pub fn as_str(&self) -> &str { - self.to_str().expect("SharedString is not UTF-8") - } - pub fn len(&self) -> usize { match self { Self::Owned(s) => s.len(),