use crate::{
class,
error::{ErrorKind, Result, err},
limits::Limits,
scan::control_kind,
text::{self, comment_end, parse_quoted, trim_ows},
};
#[derive(Clone, Copy, Default)]
struct Budget {
members: usize,
empties: usize,
}
#[inline]
fn is_simple(value: &[u8]) -> bool {
!value.iter().any(|&b| matches!(b, b'"' | b'(' | b')'))
}
#[inline]
fn segment<'a>(
value: &'a [u8],
start: usize,
end: usize,
limits: &Limits,
budget: &mut Budget,
f: &mut impl FnMut(&'a [u8]),
) -> Result<()> {
let raw = value.get(start..end).unwrap_or(&[]);
let element = trim_ows(raw);
if element.is_empty() {
if !(end == value.len() && start == 0) {
budget.empties += 1;
if budget.empties > usize::from(limits.empty_elements) {
return err(ErrorKind::TooManyEmptyElements, end);
}
}
} else {
budget.members += 1;
if budget.members > usize::from(limits.list_members) {
return err(ErrorKind::TooManyMembers, start + text::skip_ows(raw));
}
f(element);
}
Ok(())
}
fn walk<'a>(
value: &'a [u8],
limits: &Limits,
budget: &mut Budget,
mut f: impl FnMut(&'a [u8]),
) -> Result<bool> {
let mut simple = true;
let mut start = 0usize;
let mut i = 0usize;
loop {
let rest = value.get(i..).unwrap_or(&[]);
i += rest
.iter()
.position(|&b| !class::is_list_plain(b))
.unwrap_or(rest.len());
match value.get(i) {
None => {
segment(value, start, i, limits, budget, &mut f)?;
return Ok(simple);
}
Some(b',') => {
segment(value, start, i, limits, budget, &mut f)?;
i += 1;
start = i;
}
Some(b'"') => {
simple = false;
let rest = value.get(i..).unwrap_or(&[]);
let (_, used) = parse_quoted(rest).map_err(|e| e.shifted(i))?;
i += used;
}
Some(b'(') => {
simple = false;
let (end, _) = comment_end(value, i, limits)?;
i = end;
}
Some(b')') => return err(ErrorKind::UnbalancedComment, i),
Some(&b) => return err(control_kind(b), i),
}
}
}
pub fn list(value: &[u8]) -> Result<List<'_>> {
parse(value, &Limits::DEFAULT)
}
pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<List<'a>> {
let mut budget = Budget::default();
let simple = walk(value, limits, &mut budget, |_| {})?;
Ok(List {
value,
count: u32::try_from(budget.members).unwrap_or(u32::MAX),
simple,
})
}
pub fn for_each<'a>(value: &'a [u8], limits: &Limits, f: impl FnMut(&'a [u8])) -> Result<usize> {
let mut budget = Budget::default();
let _simple = walk(value, limits, &mut budget, f)?;
Ok(budget.members)
}
pub fn combined<'a, I>(lines: I, limits: &Limits) -> Result<CombinedList<'a, I::IntoIter>>
where
I: IntoIterator<Item = &'a [u8]>,
I::IntoIter: Clone,
{
let lines = lines.into_iter();
let mut budget = Budget::default();
for (n, line) in lines.clone().enumerate() {
let _simple = walk(line, limits, &mut budget, |_| {}).map_err(|e| e.on_line(n))?;
}
Ok(CombinedList {
lines,
count: budget.members,
_value: core::marker::PhantomData,
})
}
pub fn contains_token<'a, I>(lines: I, token: &[u8]) -> bool
where
I: IntoIterator<Item = &'a [u8]>,
{
lines.into_iter().any(|line| {
parse(line, &Limits::DEFAULT).is_ok_and(|l| l.iter().any(|e| e.eq_ignore_ascii_case(token)))
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct List<'a> {
value: &'a [u8],
count: u32,
simple: bool,
}
impl<'a> List<'a> {
#[must_use]
pub fn iter(&self) -> Elements<'a> {
Elements {
rest: self.value,
remaining: self.count as usize,
simple: self.simple,
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.count as usize
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
impl<'a> IntoIterator for List<'a> {
type Item = &'a [u8];
type IntoIter = Elements<'a>;
fn into_iter(self) -> Elements<'a> {
self.iter()
}
}
fn top_level_comma(s: &[u8]) -> Option<usize> {
let mut i = 0usize;
let mut depth = 0u32;
let mut quoted = false;
while let Some(&b) = s.get(i) {
match b {
b'\\' if quoted || depth > 0 => i += 1,
b'"' if depth == 0 => quoted = !quoted,
b'(' if !quoted => depth += 1,
b')' if !quoted => depth = depth.saturating_sub(1),
b',' if !quoted && depth == 0 => return Some(i),
_ => {}
}
i += 1;
}
None
}
#[derive(Clone, Debug)]
pub struct Elements<'a> {
rest: &'a [u8],
remaining: usize,
simple: bool,
}
impl<'a> Iterator for Elements<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<&'a [u8]> {
while self.remaining > 0 && !self.rest.is_empty() {
let end = if self.simple {
self.rest.iter().position(|&b| b == b',')
} else {
top_level_comma(self.rest)
}
.unwrap_or(self.rest.len());
let element = trim_ows(self.rest.get(..end).unwrap_or(&[]));
self.rest = self.rest.get(end + 1..).unwrap_or(&[]);
if !element.is_empty() {
self.remaining -= 1;
return Some(element);
}
}
None
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining, Some(self.remaining))
}
}
impl ExactSizeIterator for Elements<'_> {}
#[derive(Clone, Debug)]
pub struct CombinedList<'a, I> {
lines: I,
count: usize,
_value: core::marker::PhantomData<&'a [u8]>,
}
impl<'a, I: Iterator<Item = &'a [u8]> + Clone> CombinedList<'a, I> {
#[must_use]
pub fn iter(&self) -> CombinedElements<'a, I> {
CombinedElements {
lines: self.lines.clone(),
current: Elements {
rest: &[],
remaining: 0,
simple: true,
},
remaining: self.count,
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
#[derive(Clone, Debug)]
pub struct CombinedElements<'a, I> {
lines: I,
current: Elements<'a>,
remaining: usize,
}
impl<'a, I: Iterator<Item = &'a [u8]>> Iterator for CombinedElements<'a, I> {
type Item = &'a [u8];
fn next(&mut self) -> Option<&'a [u8]> {
while self.remaining > 0 {
if let Some(e) = self.current.next() {
self.remaining -= 1;
return Some(e);
}
let line = self.lines.next()?;
self.current = Elements {
rest: line,
remaining: usize::MAX,
simple: is_simple(line),
};
}
None
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining, Some(self.remaining))
}
}
impl<'a, I: Iterator<Item = &'a [u8]>> ExactSizeIterator for CombinedElements<'a, I> {}
#[cfg(test)]
mod tests {
use std::{vec, vec::Vec};
use super::*;
fn all(v: &[u8]) -> Vec<&[u8]> {
list(v).unwrap().iter().collect()
}
#[test]
fn test_list_edge_cases() {
assert!(all(b"").is_empty());
assert!(all(b" ").is_empty());
assert!(all(b",").is_empty());
assert!(all(b", ,").is_empty());
assert_eq!(all(b"a,"), [b"a"]);
assert_eq!(all(b",a"), [b"a"]);
assert_eq!(all(b"a,,b"), [&b"a"[..], b"b"]);
assert_eq!(all(b"a, \"b,c\""), [&b"a"[..], b"\"b,c\""]);
assert_eq!(all(b"a, (b,c)"), [&b"a"[..], b"(b,c)"]);
assert_eq!(all(b"a;q=1 , b"), [&b"a;q=1"[..], b"b"]);
assert_eq!(all(b"a=\"x\\\",y\", b"), [&b"a=\"x\\\",y\""[..], b"b"]);
assert_eq!(all(b"(a\\)b,c), d"), [&b"(a\\)b,c)"[..], b"d"]);
}
#[test]
fn test_list_comma_in_quotes_is_one_element() {
assert_eq!(all(b"filename=\"a,b.txt\""), [&b"filename=\"a,b.txt\""[..]]);
}
#[test]
fn test_list_empty_element_budget() {
let mut sixteen = vec![b'a'];
sixteen.extend([b','; 17]);
sixteen.push(b'b');
assert_eq!(all(&sixteen), [&b"a"[..], b"b"]);
let mut seventeen = vec![b'a'];
seventeen.extend([b','; 18]);
seventeen.push(b'b');
let e = list(&seventeen).unwrap_err();
assert_eq!(
(e.kind(), e.offset()),
(ErrorKind::TooManyEmptyElements, 18)
);
}
#[test]
fn test_list_member_budget() {
let mut v = Vec::new();
for i in 0..1025 {
if i > 0 {
v.extend_from_slice(b", ");
}
v.push(b'a');
}
assert_eq!(list(&v[..v.len() - 3]).unwrap().len(), 1024);
assert_eq!(list(&v).unwrap_err().kind(), ErrorKind::TooManyMembers);
let e = parse(b"a, b, c", &Limits::DEFAULT.max_list_members(2)).unwrap_err();
assert_eq!(e.offset(), 6);
}
#[test]
fn test_list_refuses_bad_bytes_and_structure() {
assert_eq!(list(b"a\rb").unwrap_err().kind(), ErrorKind::BareCr);
assert_eq!(
list(b"a, \"b").unwrap_err().kind(),
ErrorKind::UnterminatedQuote
);
assert_eq!(
list(b"a)").unwrap_err().kind(),
ErrorKind::UnbalancedComment
);
assert_eq!(
list(b"(a").unwrap_err().kind(),
ErrorKind::UnbalancedComment
);
let e = list(b"ab, \"c\x01\"").unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::Control, 6));
}
#[test]
fn test_combined_lines() {
let lines: [&[u8]; 3] = [b"a, b", b"", b"c"];
let c = combined(lines, &Limits::DEFAULT).unwrap();
assert_eq!(c.len(), 3);
assert_eq!(c.iter().collect::<Vec<_>>(), [&b"a"[..], b"b", b"c"]);
assert_eq!(c.iter().len(), 3);
let bad: [&[u8]; 2] = [b"a", b"\"open"];
let e = combined(bad, &Limits::DEFAULT).unwrap_err();
assert_eq!((e.kind(), e.line()), (ErrorKind::UnterminatedQuote, 1));
let empty: [&[u8]; 0] = [];
assert!(combined(empty, &Limits::DEFAULT).unwrap().is_empty());
}
#[test]
fn test_combined_budgets_count_across_lines() {
let l = Limits::DEFAULT.max_list_members(2);
let lines: [&[u8]; 2] = [b"a", b"b, c"];
let e = combined(lines, &l).unwrap_err();
assert_eq!(
(e.kind(), e.line(), e.offset()),
(ErrorKind::TooManyMembers, 1, 3)
);
}
#[test]
fn test_combined_equals_joined_at_every_comma() {
let value = b"a, \"b,c\", (d,e), f;g=\"h\", i";
let whole = all(value);
let mut commas = Vec::new();
let mut from = 0;
while let Some(i) = top_level_comma(&value[from..]) {
commas.push(from + i);
from += i + 1;
}
assert_eq!(commas.len(), 4);
for &split in &commas {
let lines: [&[u8]; 2] = [&value[..split], &value[split + 1..]];
let c = combined(lines, &Limits::DEFAULT).unwrap();
assert_eq!(c.iter().collect::<Vec<_>>(), whole, "split at {split}");
}
}
#[test]
fn test_contains_token_fails_closed() {
assert!(contains_token(
[&b"close"[..], b"keep-alive, Upgrade"],
b"upgrade"
));
assert!(!contains_token([&b"upgrade\r"[..]], b"upgrade"));
let none: [&[u8]; 0] = [];
assert!(!contains_token(none, b"x"));
}
#[test]
fn test_for_each_sees_every_element() {
let mut seen = vec![];
let n = for_each(b"a, b ,c", &Limits::DEFAULT, |e| seen.push(e)).unwrap();
assert_eq!((n, seen), (3, vec![&b"a"[..], b"b", b"c"]));
}
}