diff --git a/Cargo.lock b/Cargo.lock index a6892ed..b8c54b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,7 @@ dependencies = [ "format-bytes", "log", "nom", + "num-traits", ] [[package]] diff --git a/Justfile b/Justfile index 14ab9fd..eada087 100644 --- a/Justfile +++ b/Justfile @@ -7,6 +7,9 @@ fmt: doc: cargo doc --workspace --no-deps +test: + cargo test --all + watch: cargo watch -c diff --git a/imap/src/client/codec.rs b/imap/src/client/codec.rs index 249bb05..e7f2946 100644 --- a/imap/src/client/codec.rs +++ b/imap/src/client/codec.rs @@ -65,7 +65,7 @@ impl<'a> Decoder for ImapCodec { Err(Err::Error(err)) | Err(Err::Failure(err)) => { let buf4 = buf3.clone().into(); error!("failed to parse: {:?}", buf4); - error!("code: {}", convert_error(buf4, err)); + error!("code: {}", convert_error(buf4, &err)); return Err(io::Error::new( io::ErrorKind::Other, format!("error during parsing of {:?}", buf), diff --git a/imap/src/proto/mod.rs b/imap/src/proto/mod.rs index 042a842..3426e6d 100644 --- a/imap/src/proto/mod.rs +++ b/imap/src/proto/mod.rs @@ -13,7 +13,3 @@ pub mod rfc6154; #[cfg(feature = "rfc2177")] pub mod rfc2177; - -// tests -#[cfg(test)] -pub mod test_rfc3501; diff --git a/imap/src/proto/response.rs b/imap/src/proto/response.rs index 68b6e08..09a101e 100644 --- a/imap/src/proto/response.rs +++ b/imap/src/proto/response.rs @@ -20,8 +20,8 @@ impl DisplayBytes for Tag { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { write_bytes!(w, b"{}", self.0) } } -#[derive(Clone, Debug)] -pub struct Timestamp(DateTime); +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Timestamp(pub(crate) DateTime); #[cfg(feature = "fuzzing")] impl<'a> Arbitrary<'a> for Timestamp { @@ -145,7 +145,7 @@ pub struct Envelope { pub message_id: Option, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "fuzzing", derive(Arbitrary))] pub struct Address { pub name: Option, diff --git a/imap/src/proto/rfc3501.rs b/imap/src/proto/rfc3501/mod.rs similarity index 79% rename from imap/src/proto/rfc3501.rs rename to imap/src/proto/rfc3501/mod.rs index a3246aa..ea66b97 100644 --- a/imap/src/proto/rfc3501.rs +++ b/imap/src/proto/rfc3501/mod.rs @@ -3,21 +3,28 @@ //! //! Grammar from +#[cfg(test)] +pub mod tests; + +use chrono::{FixedOffset, NaiveTime, TimeZone}; use nom::{ branch::alt, combinator::{map, map_res, opt, verify}, - multi::{many0, many1}, + multi::{count, many0, many1, many_m_n}, sequence::{delimited, pair, preceded, separated_pair, terminated, tuple}, }; use panorama_proto_common::{ - byte, never, parse_u32, satisfy, tagi, take, take_while1, Bytes, VResult, + byte, never, parse_num, satisfy, tagi, take, take_while1, Bytes, VResult, }; use super::response::{ Address, Atom, Capability, Condition, Envelope, Flag, Mailbox, MailboxData, MailboxList, MailboxListFlag, MessageAttribute, Response, ResponseCode, ResponseText, Status, Tag, + Timestamp, +}; +use super::rfc2234::{ + is_char, is_cr, is_ctl, is_digit, is_dquote, is_lf, is_sp, CRLF, DIGIT, DQUOTE, SP, }; -use super::rfc2234::{is_char, is_cr, is_ctl, is_digit, is_dquote, is_lf, is_sp, CRLF, DQUOTE, SP}; /// Grammar rule `T / nil` produces `Option` macro_rules! opt_nil { @@ -89,6 +96,54 @@ rule!(pub continue_req : Response => delimited(pair(byte(b'+'), SP), map(resp_text, Response::Continue), CRLF)); +rule!(pub date_day : u32 => map(many_m_n(1, 2, DIGIT), |s| match s.as_slice() { + &[x] => (x - b'0') as u32, + &[x, y] => (x - b'0') as u32 * 10 + (y - b'0') as u32, + _ => unreachable!("only up to two digits"), +})); + +rule!(pub date_day_fixed : u32 => alt(( + map(preceded(SP, DIGIT), |d| (d - b'0') as u32), + map(pair(DIGIT, DIGIT), |(x, y)| (x - b'0') as u32 * 10 + (y - b'0') as u32), +))); + +rule!(pub date_month : u32 => alt(( + map(tagi(b"Jan"), |_| 1), + map(tagi(b"Feb"), |_| 2), + map(tagi(b"Mar"), |_| 3), + map(tagi(b"Apr"), |_| 4), + map(tagi(b"May"), |_| 5), + map(tagi(b"Jun"), |_| 6), + map(tagi(b"Jul"), |_| 7), + map(tagi(b"Aug"), |_| 8), + map(tagi(b"Sep"), |_| 9), + map(tagi(b"Oct"), |_| 10), + map(tagi(b"Nov"), |_| 11), + map(tagi(b"Dec"), |_| 12), +))); + +rule!(pub date_time : Timestamp => delimited(DQUOTE, + map_res(tuple(( + date_day_fixed, + byte(b'-'), + date_month, + byte(b'-'), + date_year, + SP, + time, + SP, + zone, + )), |(d, _, m, _, y, _, time, _, zone)| { + eprintln!("{}-{}-{} {:?} {:?}", y, m, d, time, zone); + zone.ymd(y, m, d) + .and_time(time) + .map(Timestamp) + .ok_or_else(|| anyhow!("invalid time")) + }), +DQUOTE)); + +rule!(pub date_year : i32 => map_res(count(DIGIT, 4), parse_num::<_, i32>)); + rule!(pub envelope : Envelope => map(paren!(tuple(( terminated(env_date, SP), terminated(env_subject, SP), @@ -215,8 +270,10 @@ rule!(pub msg_att_dynamic : MessageAttribute => alt(( ))); rule!(pub msg_att_static : MessageAttribute => alt(( + map(preceded(pair(tagi(b"UID"), SP), uniqueid), MessageAttribute::Uid), map(preceded(pair(tagi(b"ENVELOPE"), SP), envelope), MessageAttribute::Envelope), - map(preceded(pair(tagi(b"ENVELOPE"), SP), envelope), MessageAttribute::Envelope), + map(preceded(pair(tagi(b"INTERNALDATE"), SP), date_time), MessageAttribute::InternalDate), + map(preceded(pair(tagi(b"RFC822.SIZE"), SP), number), MessageAttribute::Rfc822Size), ))); rule!(pub nil : Bytes => tagi(b"NIL")); @@ -224,7 +281,7 @@ rule!(pub nil : Bytes => tagi(b"NIL")); rule!(pub nstring : Option => opt_nil!(string)); pub(crate) fn number(i: Bytes) -> VResult { - map_res(take_while1(is_digit), parse_u32)(i) + map_res(take_while1(is_digit), parse_num::<_, u32>)(i) } rule!(pub nz_number : u32 => verify(number, |n| *n != 0)); @@ -294,3 +351,20 @@ rule!(pub text : Bytes => map(take_while1(is_text_char), Bytes::from)); pub(crate) fn is_text_char(c: u8) -> bool { is_char(c) && !is_cr(c) && !is_lf(c) } rule!(pub TEXT_CHAR : u8 => satisfy(is_text_char)); + +rule!(pub time : NaiveTime => map( + tuple(( + map_res(count(DIGIT, 2), parse_num::<_, u32>), + byte(b':'), + map_res(count(DIGIT, 2), parse_num::<_, u32>), + byte(b':'), + map_res(count(DIGIT, 2), parse_num::<_, u32>), + )), + |(h, _, m, _, s)| NaiveTime::from_hms(h, m, s))); + +rule!(pub uniqueid : u32 => nz_number); + +rule!(pub zone : FixedOffset => map(pair( + alt((map(byte(b'+'), |_| true), map(byte(b'-'), |_| false), )), + map_res(count(DIGIT, 4), parse_num::<_, i32>), +), |(pos, value)| if pos { FixedOffset::east(value) } else { FixedOffset::west(value) })); diff --git a/imap/src/proto/test_rfc3501.rs b/imap/src/proto/rfc3501/tests.rs similarity index 79% rename from imap/src/proto/test_rfc3501.rs rename to imap/src/proto/rfc3501/tests.rs index 6f097c8..79a818a 100644 --- a/imap/src/proto/test_rfc3501.rs +++ b/imap/src/proto/rfc3501/tests.rs @@ -1,11 +1,12 @@ #![allow(unused_imports)] +use chrono::*; use nom::{multi::*, sequence::*}; use panorama_proto_common::*; -use super::response::*; -use super::rfc2234::*; -use super::rfc3501::*; +use crate::proto::response::*; +use crate::proto::rfc2234::*; +use crate::proto::rfc3501::*; #[test] fn test_literal() { @@ -22,23 +23,27 @@ fn test_literal() { fn afl() { let _ = response(Bytes::from(b"* 4544444444 444 ")); } #[test] -fn test_date() {} +fn test_date() { + assert_eq!(date_year(Bytes::from(b"2021")).unwrap().1, 2021); + + assert_eq!( + date_time(Bytes::from(b"\"22-Mar-2021 01:44:12 +0000\"")) + .unwrap() + .1, + Timestamp(FixedOffset::east(0).ymd(2021, 3, 22).and_hms(1, 44, 12)), + ); +} #[test] fn test_fetch() { + assert!(flag_list(Bytes::from(b"()")).unwrap().1.is_empty()); + use nom::Err; use panorama_proto_common::convert_error; let buf = Bytes::from(b"* 8045 FETCH (UID 8225 ENVELOPE (\"Sun, 21 Mar 2021 18:44:10 -0700\" \"SUBJECT\" ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"SENDER\" NIL \"sender\" \"example.com\")) ((\"noreply\" NIL \"noreply\" \"example.com\")) ((\"NAME\" NIL \"user\" \"gmail.com\")) NIL NIL NIL \"\") FLAGS () INTERNALDATE \"22-Mar-2021 01:44:12 +0000\" RFC822.SIZE 13503)\r\n".to_vec()); let res = response(buf.clone()); - let res = match res { - Ok((_, res)) => res, - Err(Err::Error(err)) | Err(Err::Failure(err)) => { - println!("code: {}", convert_error(buf, err)); - panic!() - } - _ => panic!(), - }; - assert!(matches!(res, Response::Expunge(2))); + println!("response: {:?}", res); + assert!(matches!(res.unwrap().1, Response::Fetch(8045, _))); } #[test] diff --git a/proto-common/Cargo.toml b/proto-common/Cargo.toml index 29c3bfa..0f6a4c0 100644 --- a/proto-common/Cargo.toml +++ b/proto-common/Cargo.toml @@ -21,4 +21,5 @@ log = "0.4.14" nom = "6.2.1" # for fuzzing -arbitrary = { version = "1", optional = true, features = ["derive"] } \ No newline at end of file +arbitrary = { version = "1", optional = true, features = ["derive"] } +num-traits = "0.2.14" diff --git a/proto-common/src/bytes.rs b/proto-common/src/bytes.rs index 9097703..30ed4a9 100644 --- a/proto-common/src/bytes.rs +++ b/proto-common/src/bytes.rs @@ -4,7 +4,7 @@ use std::ops::{Deref, RangeBounds}; use format_bytes::DisplayBytes; use nom::{ error::{ErrorKind, ParseError}, - CompareResult, Err, IResult, InputLength, Needed, + CompareResult, Err, HexDisplay, IResult, InputLength, Needed, }; #[cfg(feature = "fuzzing")] @@ -42,6 +42,50 @@ impl DisplayBytes for Bytes { fn display_bytes(&self, w: &mut dyn Write) -> io::Result<()> { w.write(&*self.0).map(|_| ()) } } +static CHARS: &[u8] = b"0123456789abcdef"; +impl HexDisplay for Bytes { + fn to_hex(&self, chunk_size: usize) -> String { self.to_hex_from(chunk_size, 0) } + + fn to_hex_from(&self, chunk_size: usize, from: usize) -> String { + let mut v = Vec::with_capacity(self.len() * 3); + let mut i = from; + for chunk in self.chunks(chunk_size) { + let s = format!("{:08x}", i); + for &ch in s.as_bytes().iter() { + v.push(ch); + } + v.push(b'\t'); + + i += chunk_size; + + for &byte in chunk { + v.push(CHARS[(byte >> 4) as usize]); + v.push(CHARS[(byte & 0xf) as usize]); + v.push(b' '); + } + if chunk_size > chunk.len() { + for _ in 0..(chunk_size - chunk.len()) { + v.push(b' '); + v.push(b' '); + v.push(b' '); + } + } + v.push(b'\t'); + + for &byte in chunk { + if (byte >= 32 && byte <= 126) || byte >= 128 { + v.push(byte); + } else { + v.push(b'.'); + } + } + v.push(b'\n'); + } + + String::from_utf8_lossy(&v[..]).into_owned() + } +} + impl From for Bytes { fn from(b: bytes::Bytes) -> Self { Bytes(b) } } diff --git a/proto-common/src/convert_error.rs b/proto-common/src/convert_error.rs index b0f687b..d4976b1 100644 --- a/proto-common/src/convert_error.rs +++ b/proto-common/src/convert_error.rs @@ -5,11 +5,33 @@ use std::ops::Deref; use bstr::ByteSlice; use nom::{ error::{VerboseError, VerboseErrorKind}, - Offset, + Err, HexDisplay, Offset, }; +use crate::VResult; + +/// Same as nom's dbg_dmp, except operates on Bytes +pub fn dbg_dmp<'a, T, F, O>(mut f: F, context: &'static str) -> impl FnMut(T) -> VResult +where + F: FnMut(T) -> VResult, + T: AsRef<[u8]> + HexDisplay + Clone + Debug + Deref, +{ + move |i: T| match f(i.clone()) { + Err(Err::Failure(e)) => { + println!( + "{}: Error({}) at:\n{}", + context, + convert_error(i.clone(), &e), + i.to_hex(16) + ); + Err(Err::Failure(e)) + } + a => a, + } +} + /// Same as nom's convert_error, except operates on u8 -pub fn convert_error + Debug>(input: I, e: VerboseError) -> String { +pub fn convert_error + Debug>(input: I, e: &VerboseError) -> String { let mut result = String::new(); debug!("e: {:?}", e); diff --git a/proto-common/src/lib.rs b/proto-common/src/lib.rs index c7571de..1337ba6 100644 --- a/proto-common/src/lib.rs +++ b/proto-common/src/lib.rs @@ -10,6 +10,6 @@ mod parsers; mod rule; pub use crate::bytes::{Bytes, ShitCompare, ShitNeededForParsing}; -pub use crate::convert_error::convert_error; +pub use crate::convert_error::{convert_error, dbg_dmp}; pub use crate::formatter::quote_string; -pub use crate::parsers::{byte, never, parse_u32, satisfy, skip, tagi, take, take_while1, VResult}; +pub use crate::parsers::{byte, never, parse_num, satisfy, skip, tagi, take, take_while1, VResult}; diff --git a/proto-common/src/parsers.rs b/proto-common/src/parsers.rs index 02ae644..efb6eb8 100644 --- a/proto-common/src/parsers.rs +++ b/proto-common/src/parsers.rs @@ -3,6 +3,7 @@ use nom::{ error::{ErrorKind, ParseError, VerboseError}, CompareResult, Err, IResult, InputLength, Needed, Parser, ToUsize, }; +use num_traits::{CheckedAdd, CheckedMul, FromPrimitive, Zero}; use super::bytes::{ShitCompare, ShitNeededForParsing}; @@ -71,23 +72,50 @@ macro_rules! paren { } /// Parse from a [u8] into a u32 without first decoding it to UTF-8. -pub fn parse_u32(s: impl AsRef<[u8]>) -> Result { - let mut total = 0u32; +pub fn parse_num(s: S) -> Result +where + S: AsRef<[u8]>, + T: CheckedMul + Zero + CheckedAdd + FromPrimitive, +{ + let mut total = T::zero(); + let ten = T::from_u8(10).unwrap(); let s = s.as_ref(); for digit in s.iter() { let digit = *digit; - total = match total.checked_mul(10) { + total = match total.checked_mul(&ten) { Some(v) => v, - None => bail!("number {:?} overflows u32", s), + None => bail!("number {:?} overflow", s), }; if !(digit >= b'0' && digit <= b'9') { bail!("invalid digit {}", digit) } - total += (digit - b'\x30') as u32; + let new_digit = T::from_u8(digit - b'\x30').unwrap(); + total = match total.checked_add(&new_digit) { + Some(v) => v, + None => bail!("number {:?} overflow", s), + }; } Ok(total) } +// /// Parse from a [u8] into a u32 without first decoding it to UTF-8. +// pub fn parse_u32(s: impl AsRef<[u8]>) -> Result { +// let mut total = 0u32; +// let s = s.as_ref(); +// for digit in s.iter() { +// let digit = *digit; +// total = match total.checked_mul(10) { +// Some(v) => v, +// None => bail!("number {:?} overflows u32", s), +// }; +// if !(digit >= b'0' && digit <= b'9') { +// bail!("invalid digit {}", digit) +// } +// total += (digit - b'\x30') as u32; +// } +// Ok(total) +// } + /// Always fails, used as a no-op. pub fn never(i: I) -> IResult where @@ -167,7 +195,10 @@ where { move |i: I| match i.first().map(|t| (f(t), t)) { Some((true, ft)) => Ok((i.slice(1..), ft)), - Some((false, _)) => Err(Err::Error(E::from_error_kind(i, ErrorKind::Satisfy))), + Some((false, _)) => Err(Err::Error(E::from_error_kind( + i.slice(1..), + ErrorKind::Satisfy, + ))), None => Err(Err::Incomplete(Needed::Unknown)), } } diff --git a/proto-common/src/rule.rs b/proto-common/src/rule.rs index 65504ac..499079c 100644 --- a/proto-common/src/rule.rs +++ b/proto-common/src/rule.rs @@ -9,7 +9,7 @@ macro_rules! rule { $vis fn $name ( i: panorama_proto_common::Bytes ) -> panorama_proto_common::VResult { - $expr(i) + panorama_proto_common::dbg_dmp($expr, stringify!($name))(i) } }; }