diff --git a/rust/src/mime/mime.rs b/rust/src/mime/mime.rs index fd4c8f61e7..aacfe8bd4e 100644 --- a/rust/src/mime/mime.rs +++ b/rust/src/mime/mime.rs @@ -25,32 +25,41 @@ use nom8::{Err, IResult, Parser}; use std; use std::collections::HashMap; -#[derive(Clone)] -pub struct HeaderTokens<'a> { - pub tokens: HashMap<&'a [u8], &'a [u8]>, +fn mime_skip_value_delimited(input: &[u8]) -> IResult<&[u8], &[u8]> { + let (input, _) = char('"').parse(input)?; + let mut escaping = false; + for i in 0..input.len() { + if escaping { + escaping = false; + } else if input[i] == b'\\' { + escaping = true; + continue; + } else if input[i] == b'"' { + return Ok((&input[i + 1..], &input[..i])); + } + } + return Err(Err::Error(make_error(input, ErrorKind::Eof))); } -fn mime_parse_value_delimited(input: &[u8]) -> IResult<&[u8], &[u8]> { +fn mime_parse_value_delimited(input: &[u8]) -> IResult<&[u8], Vec> { let (input, _) = char('"').parse(input)?; let mut escaping = false; + let mut r = Vec::new(); for i in 0..input.len() { - if input[i] == b'\\' { - escaping = true; - } else { - if input[i] == b'"' && !escaping { - return Ok((&input[i + 1..], &input[..i])); - } - // unescape can be processed later + if escaping { escaping = false; + } else if input[i] == b'\\' { + escaping = true; + continue; + } else if input[i] == b'"' { + return Ok((&input[i + 1..], r)); } + r.push(input[i]); } - // should fail - let (input, value) = take_until("\"").parse(input)?; - let (input, _) = char('"').parse(input)?; - return Ok((input, value)); + return Err(Err::Error(make_error(input, ErrorKind::Eof))); } -fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> { +fn mime_skip_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> { let (input, value) = alt((take_till(|ch: u8| ch == b';'), rest)).parse(input)?; for i in 0..value.len() { if !is_mime_space(value[value.len() - i - 1]) { @@ -60,93 +69,117 @@ fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> { return Ok((input, value)); } +fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], Vec> { + let (input, value) = mime_skip_value_until_semicolon(input)?; + return Ok((input, value.to_vec())); +} + #[inline] fn is_mime_space(ch: u8) -> bool { ch == 0x20 || ch == 0x09 || ch == 0x0a || ch == 0x0d } -pub fn mime_parse_header_token(input: &[u8]) -> IResult<&[u8], (&'_ [u8], &'_ [u8])> { +pub fn mime_parse_header_token_name(input: &[u8]) -> IResult<&[u8], &[u8]> { // from RFC2047 : like ch.is_ascii_whitespace but without 0x0c FORM-FEED let (input, _) = take_while(is_mime_space).parse(input)?; let (input, name) = take_until("=").parse(input)?; let (input, _) = char('=').parse(input)?; + return Ok((input, name)); +} + +pub fn mime_skip_header_token_value(input: &[u8]) -> IResult<&[u8], ()> { + let (input, _) = + alt((mime_skip_value_delimited, mime_skip_value_until_semicolon)).parse(input)?; + let (input, _) = take_while(is_mime_space).parse(input)?; + let (input, _) = opt(complete(char(';'))).parse(input)?; + return Ok((input, ())); +} + +pub fn mime_parse_header_token_value(input: &[u8]) -> IResult<&[u8], Vec> { let (input, value) = alt((mime_parse_value_delimited, mime_parse_value_until_semicolon)).parse(input)?; let (input, _) = take_while(is_mime_space).parse(input)?; let (input, _) = opt(complete(char(';'))).parse(input)?; - return Ok((input, (name, value))); + return Ok((input, value)); } -fn mime_parse_header_tokens(input: &[u8]) -> IResult<&[u8], HeaderTokens<'_>> { - let (mut input, _) = take_until_and_consume(b";").parse(input)?; - let mut tokens = HashMap::new(); +pub fn mime_find_header_token(header: &[u8], token: &[u8]) -> Option> { + let (mut input, _) = take_until_and_consume::>(b";") + .parse(header) + .ok()?; + + // in case of multiple sections for the parameter cf RFC2231 + let mut current_section_slice = Vec::with_capacity(token.len() + 2); + current_section_slice.extend_from_slice(token); + current_section_slice.extend_from_slice(b"*0"); + let mut sections_values = Vec::new(); + let mut current_section_seen = 0; + let mut sections = HashMap::new(); + while !input.is_empty() { - match mime_parse_header_token(input) { - Ok((rem, t)) => { - tokens.insert(t.0, t.1); + match mime_parse_header_token_name(input) { + Ok((rem, name)) => { + let prev = input; + if name == token { + if let Ok((_, value)) = mime_parse_header_token_value(rem) { + return Some(value); + } else { + return None; + } + } else if name == current_section_slice { + if let Ok((rem2, value)) = mime_parse_header_token_value(rem) { + sections_values.extend_from_slice(&value); + input = rem2; + } else { + return None; + } + current_section_seen += 1; + let nbdigits = current_section_slice.len() - token.len() - 1; + current_section_slice.truncate(current_section_slice.len() - nbdigits); + current_section_slice + .extend_from_slice(current_section_seen.to_string().as_bytes()); + } else if name.len() > token.len() && &name[..token.len()] == token { + // prefix + if let Ok((rem2, value)) = mime_parse_header_token_value(rem) { + sections.insert(name, value); + input = rem2; + } else { + return None; + } + } else if let Ok((rem2, _)) = mime_skip_header_token_value(rem) { + input = rem2; + } else { + break; + } + // should never happen - debug_validate_bug_on!(input.len() == rem.len()); - if input.len() == rem.len() { + debug_validate_bug_on!(input.len() == prev.len()); + if input.len() == prev.len() { //infinite loop - return Err(Err::Error(make_error(input, ErrorKind::Eof))); + return None; } - input = rem; } Err(_) => { - // keep first tokens is error in remaining buffer break; } } } - return Ok((input, HeaderTokens { tokens })); -} - -pub fn mime_find_header_token<'a>( - header: &'a [u8], token: &[u8], sections_values: &'a mut Vec, -) -> Option<&'a [u8]> { - match mime_parse_header_tokens(header) { - Ok((_rem, t)) => { - // in case of multiple sections for the parameter cf RFC2231 - let mut current_section_slice = Vec::new(); - - // look for the specific token - match t.tokens.get(token) { - // easy nominal case - Some(value) => return Some(value), - None => { - // check for initial section of a parameter - current_section_slice.extend_from_slice(token); - current_section_slice.extend_from_slice(b"*0"); - { - let value = t.tokens.get(¤t_section_slice[..])?; - sections_values.extend_from_slice(value); - let l = current_section_slice.len(); - current_section_slice[l - 1] = b'1'; - } - } - } - - let mut current_section_seen = 1; - // we have at least the initial section - // try looping until we do not find anymore a next section - loop { - match t.tokens.get(¤t_section_slice[..]) { - Some(value) => { - sections_values.extend_from_slice(value); - current_section_seen += 1; - let nbdigits = current_section_slice.len() - token.len() - 1; - current_section_slice.truncate(current_section_slice.len() - nbdigits); - current_section_slice - .extend_from_slice(current_section_seen.to_string().as_bytes()); - } - None => return Some(sections_values), + if current_section_seen > 0 { + loop { + match sections.get(¤t_section_slice[..]) { + Some(value) => { + sections_values.extend_from_slice(value); + current_section_seen += 1; + let nbdigits = current_section_slice.len() - token.len() - 1; + current_section_slice.truncate(current_section_slice.len() - nbdigits); + current_section_slice + .extend_from_slice(current_section_seen.to_string().as_bytes()); } + None => return Some(sections_values), } } - Err(_) => { - return None; - } } + return None; } pub(crate) const RS_MIME_MAX_TOKEN_LEN: usize = 255; @@ -257,19 +290,10 @@ fn mime_parse_headers<'a>( { if let Ok((value, name)) = mime_parse_header_line(line) { if slice_equals_lowercase(name, "content-disposition".as_bytes()) { - let mut sections_values = Vec::new(); - if let Some(filename) = - mime_find_header_token(value, "filename".as_bytes(), &mut sections_values) - { + if let Some(filename) = mime_find_header_token(value, "filename".as_bytes()) { if !filename.is_empty() { - ctx.filename = Vec::with_capacity(filename.len()); + ctx.filename = filename; fileopen = true; - for c in filename { - // unescape - if *c != b'\\' { - ctx.filename.push(*c); - } - } } } } @@ -418,8 +442,7 @@ fn mime_process(ctx: &mut MimeStateHTTP, i: &[u8]) -> (MimeParserResult, u32, u3 } pub fn mime_state_init(i: &[u8]) -> Option { - let mut sections_values = Vec::new(); - if let Some(value) = mime_find_header_token(i, "boundary".as_bytes(), &mut sections_values) { + if let Some(value) = mime_find_header_token(i, "boundary".as_bytes()) { if value.len() <= RS_MIME_MAX_TOKEN_LEN { let mut r = MimeStateHTTP { boundary: Vec::with_capacity(2 + value.len()), @@ -428,12 +451,7 @@ pub fn mime_state_init(i: &[u8]) -> Option { // start wih 2 additional hyphens r.boundary.push(b'-'); r.boundary.push(b'-'); - for c in value { - // unescape - if *c != b'\\' { - r.boundary.push(*c); - } - } + r.boundary.extend_from_slice(&value); return Some(r); } } @@ -491,78 +509,74 @@ mod test { #[test] fn test_mime_find_header_token() { - let mut outvec = Vec::new(); let undelimok = mime_find_header_token( "attachment; filename=test;".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(undelimok, Some("test".as_bytes())); + ) + .unwrap(); + assert_eq!(undelimok, b"test"); let delimok = mime_find_header_token( "attachment; filename=\"test2\";".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(delimok, Some("test2".as_bytes())); + ) + .unwrap(); + assert_eq!(delimok, b"test2"); let escaped = mime_find_header_token( "attachment; filename=\"test\\\"2\";".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(escaped, Some("test\\\"2".as_bytes())); + ) + .unwrap(); + assert_eq!(escaped, b"test\"2"); let evasion_othertoken = mime_find_header_token( "attachment; dummy=\"filename=wrong\"; filename=real;".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(evasion_othertoken, Some("real".as_bytes())); + ) + .unwrap(); + assert_eq!(evasion_othertoken, b"real"); let evasion_suffixtoken = mime_find_header_token( "attachment; notafilename=wrong; filename=good;".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(evasion_suffixtoken, Some("good".as_bytes())); + ) + .unwrap(); + assert_eq!(evasion_suffixtoken, b"good"); let badending = mime_find_header_token( "attachment; filename=oksofar; badending".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(badending, Some("oksofar".as_bytes())); + ) + .unwrap(); + assert_eq!(badending, b"oksofar"); let missend = mime_find_header_token( "attachment; filename=test".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(missend, Some("test".as_bytes())); + ) + .unwrap(); + assert_eq!(missend, b"test"); let spaces = mime_find_header_token( "attachment; filename=test me wrong".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(spaces, Some("test me wrong".as_bytes())); + ) + .unwrap(); + assert_eq!(spaces, b"test me wrong"); - assert_eq!(outvec.len(), 0); let multi = mime_find_header_token( "attachment; filename*0=abc; filename*1=\"def\";".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(multi, Some("abcdef".as_bytes())); - outvec.clear(); + ) + .unwrap(); + assert_eq!(multi, b"abcdef"); let multi = mime_find_header_token( "attachment; filename*1=456; filename*0=\"123\"".as_bytes(), "filename".as_bytes(), - &mut outvec, - ); - assert_eq!(multi, Some("123456".as_bytes())); - outvec.clear(); + ) + .unwrap(); + assert_eq!(multi, b"123456"); } } diff --git a/rust/src/mime/smtp.rs b/rust/src/mime/smtp.rs index dc1dac7d97..cd70795b3b 100644 --- a/rust/src/mime/smtp.rs +++ b/rust/src/mime/smtp.rs @@ -184,26 +184,19 @@ pub const MIME_ANOM_LONG_BOUNDARY: u32 = 0x80; pub const MIME_ANOM_LONG_FILENAME: u32 = 0x100; fn mime_smtp_process_headers(ctx: &mut MimeStateSMTP) -> (u32, bool) { - let mut sections_values = Vec::new(); let mut warnings = 0; let mut encap = false; for h in &ctx.headers[ctx.main_headers_nb..] { if mime::slice_equals_lowercase(&h.name, b"content-disposition") { if ctx.filename.is_empty() { - if let Some(value) = - mime::mime_find_header_token(&h.value, b"filename", &mut sections_values) - { - let value = if value.len() > mime::RS_MIME_MAX_TOKEN_LEN { + if let Some(mut value) = mime::mime_find_header_token(&h.value, b"filename") { + if value.len() > mime::RS_MIME_MAX_TOKEN_LEN { warnings |= MIME_ANOM_LONG_FILENAME; - &value[..mime::RS_MIME_MAX_TOKEN_LEN] - } else { - value - }; - ctx.filename.extend_from_slice(value); - let mut newname = Vec::new(); - newname.extend_from_slice(value); - ctx.attachments.push(newname); - sections_values.clear(); + value.truncate(mime::RS_MIME_MAX_TOKEN_LEN); + value.shrink_to_fit(); + } + ctx.filename.extend_from_slice(&value); + ctx.attachments.push(value); } } } else if mime::slice_equals_lowercase(&h.name, b"content-transfer-encoding") { @@ -215,35 +208,26 @@ fn mime_smtp_process_headers(ctx: &mut MimeStateSMTP) -> (u32, bool) { } } else if mime::slice_equals_lowercase(&h.name, b"content-type") { if ctx.filename.is_empty() { - if let Some(value) = - mime::mime_find_header_token(&h.value, b"name", &mut sections_values) - { - let value = if value.len() > mime::RS_MIME_MAX_TOKEN_LEN { + if let Some(mut value) = mime::mime_find_header_token(&h.value, b"name") { + if value.len() > mime::RS_MIME_MAX_TOKEN_LEN { warnings |= MIME_ANOM_LONG_FILENAME; - &value[..mime::RS_MIME_MAX_TOKEN_LEN] - } else { - value - }; - ctx.filename.extend_from_slice(value); - let mut newname = Vec::new(); - newname.extend_from_slice(value); - ctx.attachments.push(newname); - sections_values.clear(); + value.truncate(mime::RS_MIME_MAX_TOKEN_LEN); + value.shrink_to_fit(); + } + ctx.filename.extend_from_slice(&value); + ctx.attachments.push(value); } } - if let Some(value) = - mime::mime_find_header_token(&h.value, b"boundary", &mut sections_values) - { + if let Some(value) = mime::mime_find_header_token(&h.value, b"boundary") { // start wih 2 additional hyphens let mut boundary = Vec::new(); boundary.push(b'-'); boundary.push(b'-'); - boundary.extend_from_slice(value); + boundary.extend_from_slice(&value); ctx.boundaries.push(boundary); if value.len() > MAX_BOUNDARY_LEN { warnings |= MIME_ANOM_LONG_BOUNDARY; } - sections_values.clear(); } let ct = if let Some(x) = h.value.iter().position(|&x| x == b';') { &h.value[..x]