parse date + fetch

This commit is contained in:
Michael Zhang 2021-08-23 21:54:09 -05:00
parent a43b3ead2a
commit f35ec53938
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
13 changed files with 216 additions and 39 deletions

1
Cargo.lock generated
View file

@ -768,6 +768,7 @@ dependencies = [
"format-bytes",
"log",
"nom",
"num-traits",
]
[[package]]

View file

@ -7,6 +7,9 @@ fmt:
doc:
cargo doc --workspace --no-deps
test:
cargo test --all
watch:
cargo watch -c

View file

@ -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),

View file

@ -13,7 +13,3 @@ pub mod rfc6154;
#[cfg(feature = "rfc2177")]
pub mod rfc2177;
// tests
#[cfg(test)]
pub mod test_rfc3501;

View file

@ -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<FixedOffset>);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Timestamp(pub(crate) DateTime<FixedOffset>);
#[cfg(feature = "fuzzing")]
impl<'a> Arbitrary<'a> for Timestamp {
@ -145,7 +145,7 @@ pub struct Envelope {
pub message_id: Option<Bytes>,
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
pub struct Address {
pub name: Option<Bytes>,

View file

@ -3,21 +3,28 @@
//!
//! Grammar from <https://datatracker.ietf.org/doc/html/rfc3501#section-9>
#[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<T>`
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<Bytes> => opt_nil!(string));
pub(crate) fn number(i: Bytes) -> VResult<Bytes, u32> {
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) }));

View file

@ -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 \"<HASH-99c91810@example.com>\") 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]

View file

@ -21,4 +21,5 @@ log = "0.4.14"
nom = "6.2.1"
# for fuzzing
arbitrary = { version = "1", optional = true, features = ["derive"] }
arbitrary = { version = "1", optional = true, features = ["derive"] }
num-traits = "0.2.14"

View file

@ -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<bytes::Bytes> for Bytes {
fn from(b: bytes::Bytes) -> Self { Bytes(b) }
}

View file

@ -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<T, O>
where
F: FnMut(T) -> VResult<T, O>,
T: AsRef<[u8]> + HexDisplay + Clone + Debug + Deref<Target = [u8]>,
{
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<I: Deref<Target = [u8]> + Debug>(input: I, e: VerboseError<I>) -> String {
pub fn convert_error<I: Deref<Target = [u8]> + Debug>(input: I, e: &VerboseError<I>) -> String {
let mut result = String::new();
debug!("e: {:?}", e);

View file

@ -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};

View file

@ -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<u32> {
let mut total = 0u32;
pub fn parse_num<S, T>(s: S) -> Result<T>
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<u32> {
// 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, O, E>(i: I) -> IResult<I, O, E>
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)),
}
}

View file

@ -9,7 +9,7 @@ macro_rules! rule {
$vis fn $name (
i: panorama_proto_common::Bytes
) -> panorama_proto_common::VResult<panorama_proto_common::Bytes, $ret> {
$expr(i)
panorama_proto_common::dbg_dmp($expr, stringify!($name))(i)
}
};
}