use core::fmt;
use crate::{
class,
error::{ErrorKind, Result, err},
limits::Limits,
scan::control_kind,
};
#[inline]
#[must_use]
pub fn is_token(s: &[u8]) -> bool {
!s.is_empty() && class::all(s, class::Class::TCHAR)
}
#[inline]
#[must_use]
pub const fn is_ows(b: u8) -> bool {
b == b' ' || b == b'\t'
}
#[inline]
#[must_use]
pub fn trim_ows(mut s: &[u8]) -> &[u8] {
while let [first, rest @ ..] = s {
if is_ows(*first) {
s = rest;
} else {
break;
}
}
while let [rest @ .., last] = s {
if is_ows(*last) {
s = rest;
} else {
break;
}
}
s
}
#[inline]
#[must_use]
pub fn skip_ows(s: &[u8]) -> usize {
s.iter().take_while(|&&b| is_ows(b)).count()
}
pub fn parse_token(s: &[u8]) -> Result<(&[u8], usize)> {
let n = s.iter().take_while(|&&b| class::is_tchar(b)).count();
if n == 0 {
return err(ErrorKind::Empty, 0);
}
Ok((s.get(..n).unwrap_or(&[]), n))
}
#[inline]
const fn refused(b: u8) -> ErrorKind {
if b < 0x20 || b == 0x7f {
control_kind(b)
} else {
ErrorKind::InvalidByte
}
}
pub fn parse_quoted(s: &[u8]) -> Result<(Quoted<'_>, usize)> {
let Some((&b'"', body)) = s.split_first() else {
return err(ErrorKind::InvalidByte, 0);
};
let mut has_escapes = false;
let mut i = 0usize;
let mut bytes = body.iter();
while let Some(&b) = bytes.next() {
match b {
b'"' => {
let raw = body.get(..i).unwrap_or(&[]);
return Ok((Quoted { raw, has_escapes }, i + 2));
}
b'\\' => {
has_escapes = true;
match bytes.next() {
None => return err(ErrorKind::UnterminatedQuote, s.len()),
Some(&e) if class::is_quoted_pair_char(e) => i += 2,
Some(_) => return err(ErrorKind::InvalidQuotedPair, i + 2),
}
}
_ if class::is_qdtext(b) => i += 1,
_ => return err(refused(b), i + 1),
}
}
err(ErrorKind::UnterminatedQuote, s.len())
}
pub fn parse_word(s: &[u8]) -> Result<(Word<'_>, usize)> {
if s.first() == Some(&b'"') {
let (q, n) = parse_quoted(s)?;
Ok((Word::Quoted(q), n))
} else {
let (t, n) = parse_token(s)?;
Ok((Word::Token(t), n))
}
}
pub fn parse_comment<'a>(s: &'a [u8], limits: &Limits) -> Result<(Comment<'a>, usize)> {
if s.first() != Some(&b'(') {
return err(ErrorKind::InvalidByte, 0);
}
let end = comment_end(s, 0, limits)?;
let (end, max_depth) = end;
let raw = s.get(1..end - 1).unwrap_or(&[]);
Ok((Comment { raw, max_depth }, end))
}
pub(crate) fn comment_end(s: &[u8], start: usize, limits: &Limits) -> Result<(usize, u8)> {
let limit = u16::from(limits.comment_depth);
let mut depth: u16 = 0;
let mut max: u16 = 0;
let mut i = start;
let mut bytes = s.get(start..).unwrap_or(&[]).iter();
while let Some(&b) = bytes.next() {
match b {
b'(' => {
depth += 1;
if depth > limit {
return err(ErrorKind::CommentTooDeep, i);
}
max = max.max(depth);
i += 1;
}
b')' => {
depth -= 1;
i += 1;
if depth == 0 {
return Ok((i, u8::try_from(max).unwrap_or(u8::MAX)));
}
}
b'\\' => match bytes.next() {
None => return err(ErrorKind::UnbalancedComment, s.len()),
Some(&e) if class::is_quoted_pair_char(e) => i += 2,
Some(_) => return err(ErrorKind::InvalidQuotedPair, i + 1),
},
_ if class::is_ctext(b) => i += 1,
_ => return err(refused(b), i),
}
}
err(ErrorKind::UnbalancedComment, s.len())
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Quoted<'a> {
raw: &'a [u8],
has_escapes: bool,
}
impl<'a> Quoted<'a> {
#[must_use]
pub const fn raw(&self) -> &'a [u8] {
self.raw
}
#[must_use]
pub const fn has_escapes(&self) -> bool {
self.has_escapes
}
#[must_use]
pub const fn as_plain(&self) -> Option<&'a [u8]> {
if self.has_escapes {
None
} else {
Some(self.raw)
}
}
#[must_use]
pub fn unescaped_len(&self) -> usize {
if self.has_escapes {
self.bytes().count()
} else {
self.raw.len()
}
}
#[must_use]
pub fn bytes(&self) -> Unescape<'a> {
Unescape {
rest: self.raw.iter(),
}
}
pub fn unescape_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b [u8]> {
let len = self.unescaped_len();
let Some(dst) = out.get_mut(..len) else {
return err(ErrorKind::BufferTooSmall, 0);
};
for (d, b) in dst.iter_mut().zip(self.bytes()) {
*d = b;
}
Ok(dst)
}
#[must_use]
pub fn eq_bytes(&self, other: &[u8]) -> bool {
match self.as_plain() {
Some(p) => p == other,
None => self.bytes().eq(other.iter().copied()),
}
}
#[must_use]
pub fn eq_ignore_ascii_case(&self, other: &[u8]) -> bool {
match self.as_plain() {
Some(p) => p.eq_ignore_ascii_case(other),
None => {
let mut theirs = other.iter();
self.bytes()
.all(|b| theirs.next().is_some_and(|o| o.eq_ignore_ascii_case(&b)))
&& theirs.next().is_none()
}
}
}
}
impl fmt::Debug for Quoted<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Quoted(\"")?;
for &b in self.raw {
for c in core::ascii::escape_default(b) {
f.write_str(core::str::from_utf8(&[c]).unwrap_or("?"))?;
}
}
f.write_str("\")")
}
}
#[derive(Clone, Debug)]
pub struct Unescape<'a> {
rest: core::slice::Iter<'a, u8>,
}
impl Iterator for Unescape<'_> {
type Item = u8;
#[inline]
fn next(&mut self) -> Option<u8> {
match self.rest.next() {
Some(&b'\\') => self.rest.next().copied(),
other => other.copied(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let n = self.rest.len();
(n.div_ceil(2), Some(n))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Word<'a> {
Token(&'a [u8]),
Quoted(Quoted<'a>),
}
impl<'a> Word<'a> {
#[must_use]
pub const fn as_plain(&self) -> Option<&'a [u8]> {
match self {
Self::Token(t) => Some(t),
Self::Quoted(q) => q.as_plain(),
}
}
#[must_use]
pub fn as_str(&self) -> Option<&'a str> {
self.as_plain().and_then(|p| core::str::from_utf8(p).ok())
}
#[must_use]
pub fn eq_bytes(&self, other: &[u8]) -> bool {
match self {
Self::Token(t) => *t == other,
Self::Quoted(q) => q.eq_bytes(other),
}
}
#[must_use]
pub fn eq_ignore_ascii_case(&self, other: &[u8]) -> bool {
match self {
Self::Token(t) => t.eq_ignore_ascii_case(other),
Self::Quoted(q) => q.eq_ignore_ascii_case(other),
}
}
pub fn unescape_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b [u8]> {
match self {
Self::Token(t) => {
let Some(dst) = out.get_mut(..t.len()) else {
return err(ErrorKind::BufferTooSmall, 0);
};
dst.copy_from_slice(t);
Ok(dst)
}
Self::Quoted(q) => q.unescape_into(out),
}
}
#[must_use]
pub fn len(&self) -> usize {
match self {
Self::Token(t) => t.len(),
Self::Quoted(q) => q.unescaped_len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
Self::Token(t) => t.is_empty(),
Self::Quoted(q) => q.raw.is_empty(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Comment<'a> {
raw: &'a [u8],
max_depth: u8,
}
impl<'a> Comment<'a> {
#[must_use]
pub const fn raw(&self) -> &'a [u8] {
self.raw
}
#[must_use]
pub const fn max_depth(&self) -> u8 {
self.max_depth
}
}
#[cfg(test)]
mod tests {
use std::{format, vec, vec::Vec};
use super::*;
fn q(s: &[u8]) -> Result<(Quoted<'_>, usize)> {
parse_quoted(s)
}
#[test]
fn test_quoted_edge_cases() {
assert_eq!(q(b"\"\"").unwrap().0.raw(), b"");
assert!(q(b"\"\\\"\"").unwrap().0.eq_bytes(b"\""));
assert!(q(b"\"\\\\\"").unwrap().0.eq_bytes(b"\\"));
assert_eq!(q(b"\"\\").unwrap_err().kind(), ErrorKind::UnterminatedQuote);
assert_eq!(q(b"\"a").unwrap_err().kind(), ErrorKind::UnterminatedQuote);
let e = q(b"\"a\x00\"").unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::Nul, 2));
let e = q(b"\"\\\x7f\"").unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidQuotedPair, 2));
assert!(q(b"\"\x80\xff\"").is_ok());
assert!(q(b"\"\\\t\"").unwrap().0.eq_bytes(b"\t"));
assert_eq!(q(b"x").unwrap_err().kind(), ErrorKind::InvalidByte);
assert_eq!(q(b"").unwrap_err().kind(), ErrorKind::InvalidByte);
assert_eq!(q(b"\"a\rb\"").unwrap_err().kind(), ErrorKind::BareCr);
assert_eq!(q(b"\"a\nb\"").unwrap_err().kind(), ErrorKind::LineFeed);
}
#[test]
fn test_quoted_unescape_buffer_one_short_writes_nothing() {
let (v, _) = q(b"\"a\\\"b\"").unwrap();
let mut buf = [0xaau8; 2];
assert_eq!(
v.unescape_into(&mut buf).unwrap_err().kind(),
ErrorKind::BufferTooSmall
);
assert_eq!(buf, [0xaa, 0xaa]);
let mut buf = [0u8; 3];
assert_eq!(v.unescape_into(&mut buf).unwrap(), b"a\"b");
}
#[test]
fn test_quoted_many_backslashes_linear() {
let mut v = vec![b'"'];
v.extend(core::iter::repeat_n(b'\\', 64 * 1024));
v.push(b'"');
let (quoted, used) = q(&v).unwrap();
assert_eq!(used, v.len());
assert_eq!(quoted.unescaped_len(), 32 * 1024);
}
#[test]
fn test_quoted_case_insensitive_compare() {
let (v, _) = q(b"\"A\\Bc\"").unwrap();
assert!(v.eq_ignore_ascii_case(b"abc"));
assert!(!v.eq_ignore_ascii_case(b"ab"));
assert!(!v.eq_ignore_ascii_case(b"abcd"));
assert!(!v.eq_bytes(b"abc"));
assert_eq!(v.bytes().collect::<Vec<u8>>(), b"ABc");
}
#[test]
fn test_word_token_and_quoted_compare_equal() {
let (a, _) = parse_word(b"abc").unwrap();
let (b, _) = parse_word(b"\"a\\bc\"").unwrap();
assert!(a.eq_bytes(b"abc") && b.eq_bytes(b"abc"));
assert_ne!(a, b);
assert_eq!((a.len(), b.len()), (3, 3));
assert_eq!(b.as_str(), None);
let (e, _) = parse_word(b"\"\"").unwrap();
assert!(e.is_empty() && !a.is_empty());
let mut buf = [0u8; 3];
assert_eq!(a.unescape_into(&mut buf).unwrap(), b"abc");
assert_eq!(
a.unescape_into(&mut buf[..2]).unwrap_err().kind(),
ErrorKind::BufferTooSmall
);
assert_eq!(parse_word(b";").unwrap_err().kind(), ErrorKind::Empty);
}
#[test]
fn test_trim_ows_never_strips_vt_ff_nbsp() {
assert_eq!(trim_ows(b"\x0b a"), b"\x0b a");
assert_eq!(trim_ows(b"a\x0c"), b"a\x0c");
assert_eq!(trim_ows(b"\xc2\xa0a"), b"\xc2\xa0a");
assert_eq!(trim_ows(b""), b"");
assert_eq!(skip_ows(b""), 0);
}
#[test]
fn test_comment_edge_cases() {
let l = Limits::DEFAULT;
assert_eq!(parse_comment(b"()", &l).unwrap().0.raw(), b"");
assert_eq!(parse_comment(b"(a(b)c)", &l).unwrap().0.max_depth(), 2);
assert_eq!(parse_comment(b"(\\))", &l).unwrap().1, 4);
assert_eq!(
parse_comment(b"((a)", &l).unwrap_err().kind(),
ErrorKind::UnbalancedComment
);
assert_eq!(parse_comment(b"(a))", &l).unwrap().1, 3);
assert_eq!(
parse_comment(b"x", &l).unwrap_err().kind(),
ErrorKind::InvalidByte
);
assert_eq!(
parse_comment(b"(\\", &l).unwrap_err().kind(),
ErrorKind::UnbalancedComment
);
assert_eq!(
parse_comment(b"(\\\x00)", &l).unwrap_err().kind(),
ErrorKind::InvalidQuotedPair
);
assert_eq!(
parse_comment(b"(a\rb)", &l).unwrap_err().kind(),
ErrorKind::BareCr
);
}
#[test]
fn test_comment_depth_limit_plus_one_refused() {
let l = Limits::DEFAULT.max_comment_depth(3);
assert!(parse_comment(b"(((x)))", &l).is_ok());
let e = parse_comment(b"((((x))))", &l).unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::CommentTooDeep, 3));
let deep: Vec<u8> = core::iter::repeat_n(b'(', 10_000)
.chain(core::iter::repeat_n(b')', 10_000))
.collect();
assert_eq!(
parse_comment(&deep, &Limits::DEFAULT).unwrap_err().kind(),
ErrorKind::CommentTooDeep
);
}
#[test]
fn test_quoted_debug_escapes() {
let (v, _) = q(b"\"a\\\"\x80\"").unwrap();
assert_eq!(format!("{v:?}"), "Quoted(\"a\\\\\\\"\\x80\")");
}
}