use crate::part::ParsedPart;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlineUUBlock {
pub begin_offset: u32,
pub begin_length: u32,
pub mode: u32,
pub filename: String,
pub data: Vec<u8>,
pub is_encoding_problem: bool,
}
pub fn scan_inline_uuencode(raw: &[u8], part: &ParsedPart) -> Vec<InlineUUBlock> {
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];
uuencoding::scan(body)
.map(|result| match result {
Ok(block) => {
let abs_begin = offset_u32
.saturating_add(u32::try_from(block.begin_offset).unwrap_or(u32::MAX));
let block_len = u32::try_from(block.end_offset.saturating_sub(block.begin_offset))
.unwrap_or(u32::MAX);
InlineUUBlock {
begin_offset: abs_begin,
begin_length: block_len,
mode: block.metadata.mode,
filename: block.metadata.filename,
data: block.data,
is_encoding_problem: block.is_truncated,
}
}
Err(_) => {
InlineUUBlock {
begin_offset: 0,
begin_length: 0,
mode: 0,
filename: String::new(),
data: Vec::new(),
is_encoding_problem: true,
}
}
})
.collect()
}
#[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)
}
#[test]
fn test_single_block_hello() {
let body =
hex_bytes("626567696e203634342068656c6c6f2e7478740a253226354c3b265c200a200a656e640a");
let (raw, part) = make_part(b"", &body);
let blocks = scan_inline_uuencode(&raw, &part);
assert_eq!(blocks.len(), 1, "expected 1 block");
let b = &blocks[0];
assert_eq!(b.mode, 0o644);
assert_eq!(b.filename, "hello.txt");
assert_eq!(b.data, hex_bytes("48656c6c6f"));
assert!(!b.is_encoding_problem);
assert_eq!(b.begin_offset, 0);
assert_eq!(b.begin_length, body.len() as u32);
let sliced = &raw[b.begin_offset as usize..(b.begin_offset + b.begin_length) as usize];
assert_eq!(sliced, body.as_slice());
}
#[test]
fn test_two_blocks() {
let body = hex_bytes(
"626567696e203634342068656c6c6f2e7478740a253226354c3b265c200a200a656e64\
0a536f6d65207465787420696e206265747765656e0a626567696e2036303020666f78\
2e62696e0a3335264145282725553a362d4b282629523b573d4e2826394f3e2020200a\
200a656e640a",
);
let (raw, part) = make_part(b"", &body);
let blocks = scan_inline_uuencode(&raw, &part);
assert_eq!(blocks.len(), 2, "expected 2 blocks");
let b0 = &blocks[0];
assert_eq!(b0.mode, 0o644);
assert_eq!(b0.filename, "hello.txt");
assert_eq!(b0.data, hex_bytes("48656c6c6f")); assert!(!b0.is_encoding_problem);
assert_eq!(b0.begin_offset, 0);
assert_eq!(b0.begin_length, 36);
let b1 = &blocks[1];
assert_eq!(b1.mode, 0o600);
assert_eq!(b1.filename, "fox.bin");
assert_eq!(
b1.data,
hex_bytes("54686520717569636b2062726f776e20666f78") );
assert!(!b1.is_encoding_problem);
assert_eq!(b1.begin_offset, 57);
assert_eq!(b1.begin_length, 54);
let s0 = &raw[b0.begin_offset as usize..(b0.begin_offset + b0.begin_length) as usize];
let s1 = &raw[b1.begin_offset as usize..(b1.begin_offset + b1.begin_length) as usize];
assert!(s0.starts_with(b"begin 644 hello.txt\n"));
assert!(s0.ends_with(b"end\n"));
assert!(s1.starts_with(b"begin 600 fox.bin\n"));
assert!(s1.ends_with(b"end\n"));
}
#[test]
fn test_two_blocks_with_prefix_offset() {
let body = hex_bytes(
"626567696e203634342068656c6c6f2e7478740a253226354c3b265c200a200a656e64\
0a536f6d65207465787420696e206265747765656e0a626567696e2036303020666f78\
2e62696e0a3335264145282725553a362d4b282629523b573d4e2826394f3e2020200a\
200a656e640a",
);
let prefix = b"Content-Type: text/plain\r\n\r\n"; let (raw, part) = make_part(prefix, &body);
let blocks = scan_inline_uuencode(&raw, &part);
assert_eq!(blocks.len(), 2);
assert_eq!(blocks[0].begin_offset, 28);
assert_eq!(blocks[1].begin_offset, 28 + 57);
for b in &blocks {
let sliced = &raw[b.begin_offset as usize..(b.begin_offset + b.begin_length) as usize];
assert!(sliced.starts_with(b"begin "));
assert!(sliced.ends_with(b"end\n"));
}
}
#[test]
fn test_missing_end_line() {
let body = hex_bytes("626567696e2036343420746573742e7478740a253226354c3b265c200a");
let (raw, part) = make_part(b"", &body);
let blocks = scan_inline_uuencode(&raw, &part);
assert_eq!(blocks.len(), 1, "block still found even without end");
assert!(
blocks[0].is_encoding_problem,
"missing end must set is_encoding_problem"
);
}
#[test]
fn test_full_line_45_bytes() {
let body = hex_bytes(
"626567696e2036343420616c6c62797465732e62696e0a4d202024222050302521\
403c282230482b2320542e2351203124412c34253138372621443a2651503d27\
415c402832284329223446295240492a424c4c0a200a656e640a",
);
let (raw, part) = make_part(b"", &body);
let blocks = scan_inline_uuencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].mode, 0o644);
assert_eq!(blocks[0].filename, "allbytes.bin");
assert_eq!(
blocks[0].data,
hex_bytes("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c")
);
assert!(!blocks[0].is_encoding_problem);
}
#[test]
fn test_backtick_empty_block() {
let body = hex_bytes("626567696e2037353520656d7074792e62696e0a600a656e640a");
let (raw, part) = make_part(b"", &body);
let blocks = scan_inline_uuencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].mode, 0o755);
assert_eq!(blocks[0].filename, "empty.bin");
assert!(blocks[0].data.is_empty(), "expected empty data");
assert!(!blocks[0].is_encoding_problem);
}
#[test]
fn test_multiline_block() {
let body = hex_bytes(
"626567696e20363434206d756c74696c696e652e7478740a4d3226354c3b265c4c\
28253d4f3c465144283221343a26455328264553282624403d2635533d22214f39\
42214d3d3651543a32554c3a365945282535350a3d2826354e38565d443a365947\
2b422121392631493b463c403b365d52393221423e3731453c5258200a200a656e\
640a",
);
let (raw, part) = make_part(b"", &body);
let blocks = scan_inline_uuencode(&raw, &part);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].mode, 0o644);
assert_eq!(blocks[0].filename, "multiline.txt");
assert_eq!(
blocks[0].data,
hex_bytes("48656c6c6f2c20576f726c6421205468697320697320612074657374206f66206d756c74692d6c696e6520555520656e636f64696e672e20416464696e67206d6f72652062797465732e")
);
assert!(!blocks[0].is_encoding_problem);
}
#[test]
fn test_no_uu_blocks() {
let body = b"This is just plain text.\nNo UU blocks here.\n";
let (raw, part) = make_part(b"", body);
let blocks = scan_inline_uuencode(&raw, &part);
assert!(blocks.is_empty());
}
#[test]
fn test_out_of_bounds_body_range() {
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,
};
let blocks = scan_inline_uuencode(raw, &part);
assert!(
blocks.is_empty(),
"out-of-bounds body_range must return empty Vec"
);
}
#[test]
fn test_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,
};
let blocks = scan_inline_uuencode(raw, &part);
assert!(
blocks.is_empty(),
"overflowing body_range must return empty Vec"
);
}
#[test]
fn test_begin_base64_is_encoding_problem() {
let b64_block = b"begin-base64 644 file.txt\naGVsbG8=\n====\n";
let uu_block = b"begin 644 hello.txt\n%2&5L;&\\ \n \nend\n";
let mut body = Vec::new();
body.extend_from_slice(b64_block);
body.extend_from_slice(uu_block);
let (raw, part) = make_part(b"", &body);
let blocks = scan_inline_uuencode(&raw, &part);
assert_eq!(blocks.len(), 2, "expected 2 items");
assert!(
blocks[0].is_encoding_problem,
"begin-base64 block must have is_encoding_problem=true"
);
assert!(
!blocks[1].is_encoding_problem,
"valid UU block must not have is_encoding_problem"
);
assert_eq!(blocks[1].data, b"Hello");
}
fn hex_bytes(s: &str) -> Vec<u8> {
let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
}