use std::{borrow::Cow, num::NonZeroU32, str::from_utf8};
#[cfg(not(feature = "quirk_crlf_relaxed"))]
use abnf_core::streaming::crlf;
#[cfg(feature = "quirk_crlf_relaxed")]
use abnf_core::streaming::crlf_relaxed as crlf;
use abnf_core::{is_alpha, is_digit, streaming::dquote};
use base64::{engine::general_purpose::STANDARD as _base64, Engine};
pub use imap_types::core::*;
#[cfg(feature = "ext_literal")]
use nom::character::streaming::char;
use nom::{
branch::alt,
bytes::streaming::{escaped, tag, tag_no_case, take, take_while, take_while1, take_while_m_n},
character::streaming::{digit1, one_of},
combinator::{map, map_res, opt, recognize},
sequence::{delimited, terminated, tuple},
};
use crate::{
codec::{IMAPErrorKind, IMAPParseError, IMAPResult},
utils::{
indicators::{is_astring_char, is_atom_char, is_quoted_specials, is_text_char},
unescape_quoted,
},
};
pub(crate) fn number(input: &[u8]) -> IMAPResult<&[u8], u32> {
map_res(
map(digit1, |val| from_utf8(val).unwrap()),
str::parse::<u32>,
)(input)
}
#[cfg(feature = "ext_quota")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_quota")))]
pub(crate) fn number64(input: &[u8]) -> IMAPResult<&[u8], u64> {
map_res(
map(digit1, |val| from_utf8(val).unwrap()),
str::parse::<u64>,
)(input)
}
pub(crate) fn nz_number(input: &[u8]) -> IMAPResult<&[u8], NonZeroU32> {
map_res(number, NonZeroU32::try_from)(input)
}
pub(crate) fn string(input: &[u8]) -> IMAPResult<&[u8], IString> {
alt((map(quoted, IString::Quoted), map(literal, IString::Literal)))(input)
}
pub(crate) fn quoted(input: &[u8]) -> IMAPResult<&[u8], Quoted> {
let mut parser = tuple((
dquote,
map(
escaped(
take_while1(is_any_text_char_except_quoted_specials),
'\\',
one_of("\\\""),
),
|val| from_utf8(val).unwrap(),
),
dquote,
));
let (remaining, (_, quoted, _)) = parser(input)?;
Ok((remaining, Quoted::unvalidated(unescape_quoted(quoted))))
}
pub(crate) fn quoted_char(input: &[u8]) -> IMAPResult<&[u8], QuotedChar> {
map(
alt((
map(
take_while_m_n(1, 1, is_any_text_char_except_quoted_specials),
|bytes: &[u8]| {
assert_eq!(bytes.len(), 1);
bytes[0] as char
},
),
map(
tuple((tag("\\"), take_while_m_n(1, 1, is_quoted_specials))),
|(_, bytes): (_, &[u8])| {
assert_eq!(bytes.len(), 1);
bytes[0] as char
},
),
)),
QuotedChar::unvalidated,
)(input)
}
pub(crate) fn is_any_text_char_except_quoted_specials(byte: u8) -> bool {
is_text_char(byte) && !is_quoted_specials(byte)
}
pub(crate) fn literal(input: &[u8]) -> IMAPResult<&[u8], Literal> {
#[cfg(not(feature = "ext_literal"))]
let (remaining, length) = terminated(delimited(tag(b"{"), number, tag(b"}")), crlf)(input)?;
#[cfg(feature = "ext_literal")]
let (remaining, (length, mode)) = terminated(
delimited(
tag(b"{"),
tuple((
number,
map(opt(char('+')), |i| {
i.map(|_| LiteralMode::NonSync).unwrap_or(LiteralMode::Sync)
}),
)),
tag(b"}"),
),
crlf,
)(input)?;
if remaining.is_empty() {
return Err(nom::Err::Failure(IMAPParseError {
input,
kind: IMAPErrorKind::Literal {
length,
#[cfg(feature = "ext_literal")]
mode,
},
}));
}
let (remaining, data) = take(length)(remaining)?;
match Literal::try_from(data) {
#[cfg(not(feature = "ext_literal"))]
Ok(literal) => Ok((remaining, literal)),
#[cfg(feature = "ext_literal")]
Ok(mut literal) => {
literal.set_mode(mode);
Ok((remaining, literal))
}
Err(_) => Err(nom::Err::Failure(IMAPParseError {
input,
kind: IMAPErrorKind::LiteralContainsNull,
})),
}
}
pub(crate) fn astring(input: &[u8]) -> IMAPResult<&[u8], AString> {
alt((
map(take_while1(is_astring_char), |bytes: &[u8]| {
AString::Atom(AtomExt::unvalidated(Cow::Borrowed(
std::str::from_utf8(bytes).unwrap(),
)))
}),
map(string, AString::String),
))(input)
}
pub(crate) fn atom(input: &[u8]) -> IMAPResult<&[u8], Atom> {
let parser = take_while1(is_atom_char);
let (remaining, parsed_atom) = parser(input)?;
Ok((
remaining,
Atom::unvalidated(from_utf8(parsed_atom).unwrap()),
))
}
pub(crate) fn nstring(input: &[u8]) -> IMAPResult<&[u8], NString> {
alt((
map(string, |item| NString(Some(item))),
map(nil, |_| NString(None)),
))(input)
}
#[inline]
pub(crate) fn nil(input: &[u8]) -> IMAPResult<&[u8], &[u8]> {
tag_no_case(b"NIL")(input)
}
pub(crate) fn text(input: &[u8]) -> IMAPResult<&[u8], Text> {
map(take_while1(is_text_char), |bytes|
Text::unvalidated(from_utf8(bytes).unwrap()))(input)
}
pub(crate) fn base64(input: &[u8]) -> IMAPResult<&[u8], Vec<u8>> {
map_res(
recognize(tuple((
take_while(is_base64_char),
opt(alt((tag("=="), tag("=")))),
))),
|input| _base64.decode(input),
)(input)
}
pub(crate) fn is_base64_char(i: u8) -> bool {
is_alpha(i) || is_digit(i) || i == b'+' || i == b'/'
}
pub(crate) fn charset(input: &[u8]) -> IMAPResult<&[u8], Charset> {
alt((map(atom, Charset::Atom), map(quoted, Charset::Quoted)))(input)
}
pub(crate) fn tag_imap(input: &[u8]) -> IMAPResult<&[u8], Tag> {
map(take_while1(|b| is_astring_char(b) && b != b'+'), |val| {
Tag::unvalidated(from_utf8(val).unwrap())
})(input)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::Encode;
#[test]
fn test_atom() {
assert!(atom(b" ").is_err());
assert!(atom(b"").is_err());
let (rem, val) = atom(b"a(").unwrap();
assert_eq!(val, "a".try_into().unwrap());
assert_eq!(rem, b"(");
let (rem, val) = atom(b"xxx yyy").unwrap();
assert_eq!(val, "xxx".try_into().unwrap());
assert_eq!(rem, b" yyy");
}
#[test]
fn test_quoted() {
let (rem, val) = quoted(br#""Hello"???"#).unwrap();
assert_eq!(rem, b"???");
assert_eq!(val, Quoted::try_from("Hello").unwrap());
assert!(quoted(br#""Hello \" "???"#).is_ok());
assert!(quoted(br#""Hello \\ "???"#).is_ok());
assert!(quoted(br#""Hello \a "???"#).is_err());
assert!(quoted(br#""Hello \z "???"#).is_err());
assert!(quoted(br#""Hello \? "???"#).is_err());
let (rem, val) = quoted(br#""Hello \"World\""???"#).unwrap();
assert_eq!(rem, br#"???"#);
assert_eq!(val, Quoted::try_from("Hello \"World\"").unwrap());
assert!(matches!(quoted(br#""#), Err(nom::Err::Incomplete(_))));
assert!(matches!(quoted(br#""\"#), Err(nom::Err::Incomplete(_))));
assert!(matches!(
quoted(br#""Hello "#),
Err(nom::Err::Incomplete(_))
));
assert!(matches!(quoted(br#"\"#), Err(nom::Err::Error(_))));
}
#[test]
fn test_quoted_char() {
let (rem, val) = quoted_char(b"\\\"xxx").unwrap();
assert_eq!(rem, b"xxx");
assert_eq!(val, QuotedChar::try_from('"').unwrap());
}
#[test]
fn test_number() {
assert!(number(b"").is_err());
assert!(number(b"?").is_err());
assert!(number(b"0?").is_ok());
assert!(number(b"55?").is_ok());
assert!(number(b"999?").is_ok());
}
#[test]
fn test_nz_number() {
assert!(number(b"").is_err());
assert!(number(b"?").is_err());
assert!(nz_number(b"0?").is_err());
assert!(nz_number(b"55?").is_ok());
assert!(nz_number(b"999?").is_ok());
}
#[test]
fn test_literal() {
assert!(literal(b"{3}\r\n123").is_ok());
assert!(literal(b"{3}\r\n1\x003").is_err());
let (rem, val) = literal(b"{3}\r\n123xxx").unwrap();
assert_eq!(rem, b"xxx");
assert_eq!(val, Literal::try_from(b"123".as_slice()).unwrap());
}
#[test]
fn test_nil() {
assert!(nil(b"nil").is_ok());
assert!(nil(b"nil ").is_ok());
assert!(nil(b" nil").is_err());
assert!(nil(b"null").is_err());
let (rem, _) = nil(b"nilxxx").unwrap();
assert_eq!(rem, b"xxx");
}
#[test]
fn test_encode_charset() {
let tests = [
("bengali", "bengali"),
("\"simple\" english", r#""\"simple\" english""#),
("", "\"\""),
("\"", "\"\\\"\""),
("\\", "\"\\\\\""),
];
for (from, expected) in tests.iter() {
let cs = Charset::try_from(*from).unwrap();
println!("{:?}", cs);
let out = cs.encode().dump();
assert_eq!(from_utf8(&out).unwrap(), *expected);
}
assert!(Charset::try_from("\r").is_err());
assert!(Charset::try_from("\n").is_err());
assert!(Charset::try_from("¹").is_err());
assert!(Charset::try_from("²").is_err());
assert!(Charset::try_from("\x00").is_err());
}
}