From ad537941cdf67a7b87891389cd06336cfd4fbc63 Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:35:37 +0100 Subject: [PATCH 1/2] Search for MIME boundaries with memchr::memmem find_from_u8 scanned byte by byte, and on a multipart message it is where parse_mail spends its time: sampling a parse of a 767 KiB message with four base64 attachments put 96% of the cycles in find_from_u8_line_prefix. The loop's speed also depended on where the linker happened to place it. The same instructions ran at half speed on x86-64 when the loop straddled a 64-byte boundary, so unrelated changes in a dependent crate -- a version bump, a different rustc -- moved parse time by 2x with no change to this code. memmem is a vectorised search with runtime CPU dispatch: faster at every input size, and laid out independently of whoever calls it. Measured from a dependent crate on the message above: full parse 1.10 ms -> 0.76 ms; a structure-only parse that decodes no bodies 0.37 ms -> 0.03 ms. Semantics are unchanged: the first occurrence of `key` at or after `ix_start`, None when there is none. The two asserts stay. memchr's MSRV is 1.61. Signed-off-by: kurok <22548029+kurok@users.noreply.github.com> --- Cargo.toml | 1 + src/lib.rs | 22 +--------------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b7f3aa1..fe28f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ maintenance = { status = "passively-maintained" } [dependencies] data-encoding = "2.6.0" +memchr = "2.7.0" quoted_printable = "0.5.0" charset = "0.1.3" diff --git a/src/lib.rs b/src/lib.rs index 6a1653c..6ac294f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,27 +117,7 @@ pub(crate) fn find_from(line: &str, ix_start: usize, key: &str) -> Option fn find_from_u8(line: &[u8], ix_start: usize, key: &[u8]) -> Option { assert!(!key.is_empty()); assert!(ix_start <= line.len()); - if line.len() < key.len() { - return None; - } - let ix_end = line.len() - key.len(); - if ix_start <= ix_end { - for i in ix_start..=ix_end { - if line[i] == key[0] { - let mut success = true; - for j in 1..key.len() { - if line[i + j] != key[j] { - success = false; - break; - } - } - if success { - return Some(i); - } - } - } - } - None + memchr::memmem::find(&line[ix_start..], key).map(|v| ix_start + v) } #[test] From 2bc2d5d2e87a26959aed424b9cb7637bc3b1b36d Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:04:25 +0100 Subject: [PATCH 2/2] Strip base64 whitespace with memchr instead of one byte at a time decode_base64 removed whitespace with iter().filter().cloned().collect(): a test and a bounds-checked push for every byte of the body. On a message with large base64 attachments that filter is where the parse spends its time -- sampling a full parse of a 767 KiB message put 78% of the cycles in it, more than in the base64 decode itself. Whitespace is now located with memchr and the runs between are copied whole. A base64 body is almost entirely 76-byte lines ending in CRLF, so the common case is one search and one memcpy per line. Tabs and form feeds are rare enough that they get a second search over each run rather than a place in the first. The set of bytes removed is unchanged -- exactly what u8::is_ascii_whitespace names -- and a test checks the new function against the filter it replaces over every byte value, alone and next to whitespace, including the vertical tab that is_ascii_whitespace does not include. Signed-off-by: kurok <22548029+kurok@users.noreply.github.com> --- src/body.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/src/body.rs b/src/body.rs index dcb6a84..8cbe3de 100644 --- a/src/body.rs +++ b/src/body.rs @@ -137,14 +137,36 @@ impl<'a> BinaryBody<'a> { } fn decode_base64(body: &[u8]) -> Result, MailParseError> { - let cleaned = body - .iter() - .filter(|c| !c.is_ascii_whitespace()) - .cloned() - .collect::>(); + let cleaned = strip_ascii_whitespace(body); Ok(data_encoding::BASE64_MIME_PERMISSIVE.decode(&cleaned)?) } +/// Copy `body` without its ASCII whitespace -- the same bytes `u8::is_ascii_whitespace` +/// names: space, tab, newline, form feed, carriage return. +/// +/// Whitespace is located with a vectorised search and the runs between are copied +/// whole, rather than testing and pushing one byte at a time. A base64 body is +/// almost entirely 76-byte lines ending in CRLF, so the common case is one search +/// and one copy per line. Tabs and form feeds are rare enough that they get a second +/// search over each run instead of a place in the first. +fn strip_ascii_whitespace(body: &[u8]) -> Vec { + let mut cleaned = Vec::with_capacity(body.len()); + let mut rest = body; + loop { + let end = memchr::memchr3(b'\r', b'\n', b' ', rest).unwrap_or(rest.len()); + let mut run = &rest[..end]; + while let Some(j) = memchr::memchr2(b'\t', 0x0c, run) { + cleaned.extend_from_slice(&run[..j]); + run = &run[j + 1..]; + } + cleaned.extend_from_slice(run); + if end == rest.len() { + return cleaned; + } + rest = &rest[end + 1..]; + } +} + fn decode_quoted_printable(body: &[u8]) -> Result, MailParseError> { Ok(quoted_printable::decode( body, @@ -161,3 +183,47 @@ fn get_body_as_string(body: &[u8], ctype: &ParsedContentType) -> Result Vec { + body.iter() + .filter(|c| !c.is_ascii_whitespace()) + .cloned() + .collect() + } + + #[test] + fn matches_the_filter_it_replaces() { + let cases: &[&[u8]] = &[ + b"", + b" ", + b" \t\r\n\x0c", + b"abc", + b" abc ", + b"ab cd\tef\ngh\rij\x0ckl", + b"\x0b", // vertical tab is NOT ascii whitespace; must be kept + b"a\x0bb", + b"\t\tab\x0c\x0ccd", + b"QUJD\r\nREVG\r\n", + b"QUJD REVG\tR0hJ\x0cSktM", + ]; + for case in cases { + assert_eq!(strip_ascii_whitespace(case), reference(case), "{:?}", case); + } + // every byte value, alone and next to whitespace + for b in 0u8..=255 { + let single = [b]; + assert_eq!( + strip_ascii_whitespace(&single), + reference(&single), + "{:?}", + b + ); + let mixed = [b' ', b, b'\t', b, b'\r', b'\n', b]; + assert_eq!(strip_ascii_whitespace(&mixed), reference(&mixed), "{:?}", b); + } + } +}