use crate::part::ParsedPart;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct InlineYEncBlock {
pub begin_offset: u32,
pub begin_length: u32,
pub filename: String,
pub file_size: u64,
pub part: Option<u32>,
pub total_parts: Option<u32>,
pub part_begin: Option<u64>,
pub part_end: Option<u64>,
pub data: Vec<u8>,
pub crc32_verified: bool,
pub is_encoding_problem: bool,
}
#[must_use = "the scanned yEnc blocks must be used"]
pub fn scan_inline_yencode(raw: &[u8], part: &ParsedPart) -> Vec<InlineYEncBlock> {
let (offset_u32, length_u32) = part.body_range;
let offset = offset_u32 as usize;
let length = length_u32 as usize;
let end = match offset.checked_add(length) {
Some(e) if e <= raw.len() => e,
_ => return Vec::new(),
};
let body = &raw[offset..end];
let mut results = Vec::new();
let mut pos = 0usize;
while pos < body.len() {
let ybegin_rel = match find_ybegin(body, pos) {
Some(r) => r,
None => break, };
let slice = &body[ybegin_rel..];
let (block, yend_rel_in_slice, is_error) = decode_one_block(slice);
let abs_begin = offset_u32.saturating_add(u32::try_from(ybegin_rel).unwrap_or(u32::MAX));
let block_len = u32::try_from(yend_rel_in_slice).unwrap_or(u32::MAX);
results.push(InlineYEncBlock {
begin_offset: abs_begin,
begin_length: block_len,
filename: block.metadata.filename,
file_size: block.metadata.size,
part: block.part,
total_parts: block.metadata.total_parts,
part_begin: block.part_begin,
part_end: block.part_end,
data: block.data,
crc32_verified: block.crc32_verified,
is_encoding_problem: is_error,
});
pos = ybegin_rel + yend_rel_in_slice.max(1);
}
results
}
fn find_ybegin(body: &[u8], start: usize) -> Option<usize> {
debug_assert!(
start == 0 || body.get(start - 1) == Some(&b'\n'),
"find_ybegin: start must be a line-boundary offset"
);
let needle = b"=ybegin ";
let mut pos = start;
while pos < body.len() {
if body[pos..].starts_with(needle) {
return Some(pos);
}
match body[pos..].iter().position(|&b| b == b'\n') {
Some(rel) => pos += rel + 1,
None => break,
}
}
None
}
fn decode_one_block(slice: &[u8]) -> (yencoding::DecodedPart, usize, bool) {
match yencoding::decode(slice) {
Ok(part) => {
match find_yend_end(slice) {
Some(consumed) => (part, consumed, false),
None => {
debug_assert!(
false,
"find_yend_end returned None after successful decode — logic error"
);
let consumed = find_line_end(slice, 0);
(part, consumed, true)
}
}
}
Err(e) => {
let sentinel = make_error_sentinel(e);
let consumed = find_line_end(slice, 0);
(sentinel, consumed, true)
}
}
}
fn find_yend_end(slice: &[u8]) -> Option<usize> {
let needle = b"=yend";
let mut pos = 0;
while pos < slice.len() {
let rest = &slice[pos..];
if rest.starts_with(needle) {
let after = rest.get(needle.len()).copied();
match after {
None | Some(b' ') | Some(b'\r') | Some(b'\n') => {
return Some(find_line_end(slice, pos));
}
_ => {} }
}
match rest.iter().position(|&b| b == b'\n') {
Some(rel) => pos += rel + 1,
None => break,
}
}
None
}
fn find_line_end(slice: &[u8], pos: usize) -> usize {
match slice[pos..].iter().position(|&b| b == b'\n') {
Some(rel) => pos + rel + 1,
None => slice.len(),
}
}
fn make_error_sentinel(_err: yencoding::YencError) -> yencoding::DecodedPart {
let filename = String::new();
yencoding::DecodedPart {
data: Vec::new(),
metadata: yencoding::YencMetadata {
filename,
size: 0,
line_length: 128,
total_parts: None,
},
part: None,
part_begin: None,
part_end: None,
crc32_verified: false,
whole_file_crc32: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::part::{ParsedPart, TransferEncoding};
fn make_part(prefix: &[u8], body_bytes: &[u8]) -> (Vec<u8>, ParsedPart) {
let mut raw = prefix.to_vec();
let body_offset = raw.len();
raw.extend_from_slice(body_bytes);
let part = ParsedPart {
part_id: "1".to_owned(),
content_type: "text/plain".to_owned(),
charset: Some("utf-8".to_owned()),
transfer_encoding: TransferEncoding::Identity,
disposition: None,
filename: None,
cid: None,
header_range: (0u32, body_offset as u32),
body_range: (body_offset as u32, body_bytes.len() as u32),
children: vec![],
is_encoding_problem: false,
};
(raw, part)
}
const BLOCK_012: &[u8] =
b"=ybegin line=128 size=3 name=hi.bin\r\n*+,\r\n=yend size=3 crc32=0854897f\r\n";
const BLOCK_345: &[u8] =
b"=ybegin line=128 size=3 name=other.bin\r\n-./\r\n=yend size=3 crc32=e90156c0\r\n";
#[test]
fn single_block_no_preamble() {
let (raw, part) = make_part(b"", BLOCK_012);
let blocks = scan_inline_yencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].data, &[0u8, 1, 2]);
assert_eq!(blocks[0].filename, "hi.bin");
assert_eq!(blocks[0].file_size, 3);
assert!(blocks[0].crc32_verified);
assert!(!blocks[0].is_encoding_problem);
assert_eq!(blocks[0].begin_offset, 0);
assert_eq!(blocks[0].begin_length, BLOCK_012.len() as u32);
}
#[test]
fn single_block_with_preamble() {
let preamble = b"Some prose.\r\nMore prose.\r\n";
let (raw, part) = make_part(b"", &[preamble, BLOCK_012].concat());
let blocks = scan_inline_yencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].data, &[0u8, 1, 2]);
assert_eq!(blocks[0].begin_offset, preamble.len() as u32);
assert_eq!(blocks[0].begin_length, BLOCK_012.len() as u32);
let start = blocks[0].begin_offset as usize;
let end = start + blocks[0].begin_length as usize;
assert_eq!(&raw[start..end], BLOCK_012);
}
#[test]
fn two_sequential_blocks() {
let separator = b"Some text between blocks.\r\n";
let body = [BLOCK_012, separator, BLOCK_345].concat();
let (raw, part) = make_part(b"", &body);
let blocks = scan_inline_yencode(&raw, &part);
assert_eq!(blocks.len(), 2, "expected 2 blocks");
assert_eq!(blocks[0].data, &[0u8, 1, 2]);
assert_eq!(blocks[0].filename, "hi.bin");
assert_eq!(blocks[0].begin_offset, 0);
assert_eq!(blocks[1].data, &[3u8, 4, 5]);
assert_eq!(blocks[1].filename, "other.bin");
assert_eq!(
blocks[1].begin_offset,
(BLOCK_012.len() + separator.len()) as u32
);
assert!(blocks[0].begin_offset + blocks[0].begin_length <= blocks[1].begin_offset);
}
#[test]
fn block_with_absolute_prefix_offset() {
let prefix = b"MIME headers here\r\n\r\n";
let (raw, part) = make_part(prefix, BLOCK_012);
let blocks = scan_inline_yencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].begin_offset, prefix.len() as u32);
let start = blocks[0].begin_offset as usize;
let end = start + blocks[0].begin_length as usize;
assert_eq!(&raw[start..end], BLOCK_012);
}
#[test]
fn no_blocks_returns_empty() {
let (raw, part) = make_part(b"", b"Just plain text.\r\nNo yEnc here.\r\n");
assert!(scan_inline_yencode(&raw, &part).is_empty());
}
#[test]
fn empty_body_returns_empty() {
let (raw, part) = make_part(b"", b"");
assert!(scan_inline_yencode(&raw, &part).is_empty());
}
#[test]
fn out_of_bounds_body_range_returns_empty() {
let raw = b"short";
let part = ParsedPart {
part_id: "1".to_owned(),
content_type: "text/plain".to_owned(),
charset: None,
transfer_encoding: TransferEncoding::Identity,
disposition: None,
filename: None,
cid: None,
header_range: (0, 0),
body_range: (3, 100), children: vec![],
is_encoding_problem: false,
};
assert!(scan_inline_yencode(raw, &part).is_empty());
}
#[test]
fn overflow_safe_body_range() {
let raw = b"data";
let part = ParsedPart {
part_id: "1".to_owned(),
content_type: "text/plain".to_owned(),
charset: None,
transfer_encoding: TransferEncoding::Identity,
disposition: None,
filename: None,
cid: None,
header_range: (0, 0),
body_range: (u32::MAX, 1),
children: vec![],
is_encoding_problem: false,
};
assert!(scan_inline_yencode(raw, &part).is_empty());
}
#[test]
fn crc_mismatch_sets_is_encoding_problem() {
let bad = b"=ybegin line=128 size=3 name=f.bin\r\n*+,\r\n=yend size=3 crc32=00000000\r\n";
let (raw, part) = make_part(b"", bad);
let blocks = scan_inline_yencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert!(
blocks[0].is_encoding_problem,
"CRC mismatch should set is_encoding_problem"
);
assert!(
blocks[0].data.is_empty(),
"data should be empty on CRC error"
);
}
#[test]
fn truncated_block_sets_is_encoding_problem() {
let trunc = b"=ybegin line=128 size=3 name=f.bin\r\n*+,\r\n";
let (raw, part) = make_part(b"", trunc);
let blocks = scan_inline_yencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert!(blocks[0].is_encoding_problem);
}
#[test]
fn ybegin_mid_line_not_matched() {
let body = b"this is not =ybegin a real block\r\n=ybegin line=128 size=3 name=f.bin\r\n*+,\r\n=yend size=3 crc32=0854897f\r\n";
let (raw, part) = make_part(b"", body);
let blocks = scan_inline_yencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].data, &[0u8, 1, 2]);
}
#[test]
fn multipart_article_fields_populated() {
use yencoding::{encode_part, EncodePartOptions, DEFAULT_LINE_LENGTH};
let data = [0u8, 1, 2];
let whole_crc: u32 = 0x30eb_cf4a;
let opts = EncodePartOptions {
filename: "split.bin",
total_size: 6,
total_parts: 2,
part: 1,
begin: 1,
end: 3,
whole_file_crc32: whole_crc,
line_length: DEFAULT_LINE_LENGTH,
};
let encoded = encode_part(&data, &opts);
let (raw, part) = make_part(b"", &encoded);
let blocks = scan_inline_yencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].part, Some(1));
assert_eq!(blocks[0].total_parts, Some(2));
assert_eq!(blocks[0].part_begin, Some(1));
assert_eq!(blocks[0].part_end, Some(3));
assert_eq!(blocks[0].file_size, 6);
assert!(blocks[0].crc32_verified);
assert_eq!(
blocks[0].data,
&[0u8, 1, 2],
"decoded bytes must match oracle"
);
let start = blocks[0].begin_offset as usize;
let end = start + blocks[0].begin_length as usize;
assert_eq!(
&raw[start..end],
encoded.as_slice(),
"slice invariant must hold for multi-part block"
);
}
#[test]
fn full_parse_pipeline() {
use crate::parse;
let raw: Vec<u8> = [
b"From: poster@example.com\r\n" as &[u8],
b"Subject: [1/1] hi.bin\r\n",
b"\r\n",
b"Some prose.\r\n",
BLOCK_012,
b"More prose.\r\n",
]
.concat();
let msg = parse(&raw).expect("parse failed");
let part = msg.part_index.find_by_id("1").unwrap();
assert_eq!(part.content_type, "text/plain");
let blocks = scan_inline_yencode(&raw, part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].data, &[0u8, 1, 2]);
assert_eq!(blocks[0].filename, "hi.bin");
assert!(blocks[0].crc32_verified);
}
}