use crate::{
error::{ErrorKind, ParseError, Result, WriteError},
limits::Limits,
num::parse_u32_saturating,
scan::control_kind,
text::{Word, is_token, parse_word, skip_ows, token_len, trim_ows},
write::{Sink, reserve, write_decimal_u64},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Directive<'a> {
pub name: &'a [u8],
pub value: Option<Word<'a>>,
}
#[inline]
fn skip_ws(mut s: &[u8]) -> &[u8] {
while let [b' ' | b'\t', rest @ ..] = s {
s = rest;
}
s
}
fn directive(s: &[u8]) -> Result<(Directive<'_>, &[u8])> {
let n = token_len(s);
if n == 0 {
return Err(bad(s, 0));
}
let (name, after) = s.split_at(n);
let [b'=', value @ ..] = after else {
return Ok((Directive { name, value: None }, after));
};
let (w, used) = parse_word(value).map_err(|x| match x.kind() {
ErrorKind::Empty => bad(s, n + 1),
_ => x.shifted(n + 1),
})?;
Ok((
Directive {
name,
value: Some(w),
},
value.get(used..).unwrap_or(&[]),
))
}
fn walk<'a>(v: &'a [u8], limits: &Limits, mut f: impl FnMut(Directive<'a>)) -> Result<usize> {
let at = |rest: &[u8]| v.len() - rest.len();
let (mut rest, mut count, mut empty) = (v, 0usize, 0usize);
loop {
rest = skip_ws(rest);
match rest {
[] => return Ok(count),
[b',', tail @ ..] => {
empty += 1;
if empty > usize::from(limits.empty_elements) {
return Err(ParseError::new(ErrorKind::TooManyEmptyElements, at(rest)));
}
rest = tail;
continue;
}
_ => {}
}
let (d, tail) = directive(rest).map_err(|e| e.shifted(at(rest)))?;
count += 1;
if count > usize::from(limits.list_members) {
return Err(ParseError::new(ErrorKind::TooManyMembers, at(rest)));
}
f(d);
rest = skip_ws(tail);
match rest {
[] => return Ok(count),
[b',', tail @ ..] => rest = tail,
_ => return Err(bad(rest, 0).shifted(at(rest))),
}
}
}
fn walk_value<'a>(
value: &'a [u8],
limits: &Limits,
f: impl FnMut(Directive<'a>),
) -> Result<(&'a [u8], usize)> {
let lead = skip_ows(value);
let v = trim_ows(value);
let count = walk(v, limits, f).map_err(|e| e.shifted(lead))?;
Ok((v, count))
}
fn bad(e: &[u8], at: usize) -> ParseError {
match e.get(at) {
Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
_ => ParseError::new(ErrorKind::NotToken, at),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Directives<'a> {
text: &'a [u8],
count: usize,
}
impl<'a> Directives<'a> {
pub fn iter(&self) -> impl Iterator<Item = Directive<'a>> + use<'a> {
DirectiveIter { s: self.text }
}
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
struct DirectiveIter<'a> {
s: &'a [u8],
}
impl<'a> Iterator for DirectiveIter<'a> {
type Item = Directive<'a>;
fn next(&mut self) -> Option<Directive<'a>> {
let mut rest = self.s;
while let [b' ' | b'\t' | b',', tail @ ..] = rest {
rest = tail;
}
let (d, tail) = directive(rest).ok()?;
self.s = tail;
Some(d)
}
}
pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<Directives<'a>> {
let (text, count) = walk_value(value, limits, |_| {})?;
Ok(Directives { text, count })
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ResponseDirectives {
pub max_age: Option<u32>,
pub s_maxage: Option<u32>,
pub no_cache: bool,
pub no_store: bool,
pub private: bool,
pub public: bool,
pub must_revalidate: bool,
pub proxy_revalidate: bool,
pub must_understand: bool,
pub no_transform: bool,
pub immutable: bool,
pub stale_while_revalidate: Option<u32>,
pub stale_if_error: Option<u32>,
pub duplicates: bool,
pub invalid_values: bool,
pub unknown: u16,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct RequestDirectives {
pub max_age: Option<u32>,
pub max_stale: Option<Option<u32>>,
pub min_fresh: Option<u32>,
pub no_cache: bool,
pub no_store: bool,
pub no_transform: bool,
pub only_if_cached: bool,
pub stale_if_error: Option<u32>,
pub duplicates: bool,
pub invalid_values: bool,
pub unknown: u16,
}
fn seconds(value: Option<Word<'_>>, invalid: &mut bool) -> Option<u32> {
let n = match value {
Some(Word::Token(t)) => parse_u32_saturating(t).ok(),
Some(Word::Quoted(q)) => match q.as_plain() {
Some(p) => parse_u32_saturating(p).ok(),
None => {
let mut n: Option<u32> = None;
for b in q.bytes() {
if !b.is_ascii_digit() {
n = None;
break;
}
n = Some(
n.unwrap_or(0)
.saturating_mul(10)
.saturating_add(u32::from(b - b'0'))
.min(crate::num::DELTA_SECONDS_MAX),
);
}
n
}
},
None => None,
};
if n.is_none() {
*invalid = true;
}
n
}
fn flag(
slot: &mut bool,
seen: &mut bool,
dup: &mut bool,
value: Option<Word<'_>>,
invalid: &mut bool,
list_ok: bool,
) {
if *seen {
*dup = true;
return;
}
*seen = true;
*slot = true;
if value.is_some() && !list_ok {
*invalid = true;
}
}
fn number(
slot: &mut Option<u32>,
seen: &mut bool,
dup: &mut bool,
value: Option<Word<'_>>,
invalid: &mut bool,
) {
if *seen {
*dup = true;
return;
}
*seen = true;
*slot = seconds(value, invalid);
}
fn is_name(n: &[u8], lower: &[u8]) -> bool {
n.len() == lower.len() && n.iter().zip(lower).all(|(&b, &p)| b | 0x20 == p)
}
fn response_slot(n: &[u8]) -> Option<usize> {
let is = |s: &[u8]| is_name(n, s);
match n.len() {
6 if is(b"public") => Some(5),
7 if is(b"max-age") => Some(0),
7 if is(b"private") => Some(4),
8 if is(b"s-maxage") => Some(1),
8 if is(b"no-cache") => Some(2),
8 if is(b"no-store") => Some(3),
9 if is(b"immutable") => Some(10),
12 if is(b"no-transform") => Some(9),
14 if is(b"stale-if-error") => Some(12),
15 if is(b"must-revalidate") => Some(6),
15 if is(b"must-understand") => Some(8),
16 if is(b"proxy-revalidate") => Some(7),
22 if is(b"stale-while-revalidate") => Some(11),
_ => None,
}
}
#[derive(Default)]
struct ResponseBuilder {
r: ResponseDirectives,
seen: [bool; 13],
}
impl ResponseBuilder {
fn push(&mut self, d: Directive<'_>) {
let r = &mut self.r;
let Some(slot) = response_slot(d.name) else {
r.unknown = r.unknown.saturating_add(1);
return;
};
let Some(seen) = self.seen.get_mut(slot) else {
return;
};
let (dup, inv) = (&mut r.duplicates, &mut r.invalid_values);
match slot {
0 => number(&mut r.max_age, seen, dup, d.value, inv),
1 => number(&mut r.s_maxage, seen, dup, d.value, inv),
2 => flag(&mut r.no_cache, seen, dup, d.value, inv, true),
3 => flag(&mut r.no_store, seen, dup, d.value, inv, false),
4 => flag(&mut r.private, seen, dup, d.value, inv, true),
5 => flag(&mut r.public, seen, dup, d.value, inv, false),
6 => flag(&mut r.must_revalidate, seen, dup, d.value, inv, false),
7 => flag(&mut r.proxy_revalidate, seen, dup, d.value, inv, false),
8 => flag(&mut r.must_understand, seen, dup, d.value, inv, false),
9 => flag(&mut r.no_transform, seen, dup, d.value, inv, false),
10 => flag(&mut r.immutable, seen, dup, d.value, inv, false),
11 => number(&mut r.stale_while_revalidate, seen, dup, d.value, inv),
_ => number(&mut r.stale_if_error, seen, dup, d.value, inv),
}
}
}
fn request_slot(n: &[u8]) -> Option<usize> {
let is = |s: &[u8]| is_name(n, s);
match n.len() {
7 if is(b"max-age") => Some(0),
8 if is(b"no-cache") => Some(3),
8 if is(b"no-store") => Some(4),
9 if is(b"max-stale") => Some(1),
9 if is(b"min-fresh") => Some(2),
12 if is(b"no-transform") => Some(5),
14 if is(b"only-if-cached") => Some(6),
14 if is(b"stale-if-error") => Some(7),
_ => None,
}
}
#[derive(Default)]
struct RequestBuilder {
r: RequestDirectives,
seen: [bool; 8],
}
impl RequestBuilder {
fn push(&mut self, d: Directive<'_>) {
let r = &mut self.r;
let Some(slot) = request_slot(d.name) else {
r.unknown = r.unknown.saturating_add(1);
return;
};
let Some(seen) = self.seen.get_mut(slot) else {
return;
};
let (dup, inv) = (&mut r.duplicates, &mut r.invalid_values);
match slot {
0 => number(&mut r.max_age, seen, dup, d.value, inv),
1 => {
if *seen {
*dup = true;
} else {
*seen = true;
r.max_stale = Some(match d.value {
None => None,
v => seconds(v, inv),
});
}
}
2 => number(&mut r.min_fresh, seen, dup, d.value, inv),
3 => flag(&mut r.no_cache, seen, dup, d.value, inv, false),
4 => flag(&mut r.no_store, seen, dup, d.value, inv, false),
5 => flag(&mut r.no_transform, seen, dup, d.value, inv, false),
6 => flag(&mut r.only_if_cached, seen, dup, d.value, inv, false),
_ => number(&mut r.stale_if_error, seen, dup, d.value, inv),
}
}
}
pub fn response_cache_control(value: &[u8]) -> Result<ResponseDirectives> {
let mut b = ResponseBuilder::default();
let _ = walk_value(value, &Limits::DEFAULT, |d| b.push(d))?;
Ok(b.r)
}
pub fn request_cache_control(value: &[u8]) -> Result<RequestDirectives> {
let mut b = RequestBuilder::default();
let _ = walk_value(value, &Limits::DEFAULT, |d| b.push(d))?;
Ok(b.r)
}
pub fn response_lines<'a, I>(lines: I) -> Result<ResponseDirectives>
where
I: IntoIterator<Item = &'a [u8]>,
I::IntoIter: Clone,
{
let mut b = ResponseBuilder::default();
for (n, line) in lines.into_iter().enumerate() {
let _ = walk_value(line, &Limits::DEFAULT, |d| b.push(d)).map_err(|e| e.on_line(n))?;
}
Ok(b.r)
}
pub fn request_lines<'a, I>(lines: I) -> Result<RequestDirectives>
where
I: IntoIterator<Item = &'a [u8]>,
I::IntoIter: Clone,
{
let mut b = RequestBuilder::default();
for (n, line) in lines.into_iter().enumerate() {
let _ = walk_value(line, &Limits::DEFAULT, |d| b.push(d)).map_err(|e| e.on_line(n))?;
}
Ok(b.r)
}
pub struct Writer<'s, S: Sink> {
out: &'s mut S,
first: bool,
}
impl<'s, S: Sink> Writer<'s, S> {
pub fn new(out: &'s mut S) -> Self {
Self { out, first: true }
}
fn separator(&self) -> &'static [u8] {
if self.first { b"" } else { b", " }
}
pub fn flag(&mut self, name: &str) -> core::result::Result<&mut Self, WriteError> {
if !is_token(name.as_bytes()) {
return Err(WriteError::NotToken);
}
let sep = self.separator();
reserve(self.out, sep.len() + name.len())?;
self.out.put(sep)?;
self.out.put(name.as_bytes())?;
self.first = false;
Ok(self)
}
pub fn seconds(
&mut self,
name: &str,
secs: u32,
) -> core::result::Result<&mut Self, WriteError> {
if !is_token(name.as_bytes()) {
return Err(WriteError::NotToken);
}
let mut digits = crate::write::CountSink::new();
write_decimal_u64(u64::from(secs), &mut digits)?;
let sep = self.separator();
reserve(self.out, sep.len() + name.len() + 1 + digits.count())?;
self.out.put(sep)?;
self.out.put(name.as_bytes())?;
self.out.put(b"=")?;
write_decimal_u64(u64::from(secs), self.out)?;
self.first = false;
Ok(self)
}
pub fn fields(
&mut self,
name: &str,
fields: &[&str],
) -> core::result::Result<&mut Self, WriteError> {
if !is_token(name.as_bytes()) || fields.iter().any(|f| !is_token(f.as_bytes())) {
return Err(WriteError::NotToken);
}
let sep = self.separator();
let list_len: usize =
fields.iter().map(|f| f.len()).sum::<usize>() + fields.len().saturating_sub(1) * 2;
reserve(self.out, sep.len() + name.len() + 3 + list_len)?;
self.out.put(sep)?;
self.out.put(name.as_bytes())?;
self.out.put(b"=\"")?;
for (i, f) in fields.iter().enumerate() {
if i > 0 {
self.out.put(b", ")?;
}
self.out.put(f.as_bytes())?;
}
self.out.put(b"\"")?;
self.first = false;
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_directives_cases() {
let r = |v: &[u8]| response_cache_control(v).unwrap();
assert_eq!(r(b"max-age=0").max_age, Some(0));
assert_eq!(r(b"max-age=\"60\"").max_age, Some(60));
let bad = r(b"max-age=60s");
assert_eq!((bad.max_age, bad.invalid_values), (None, true));
assert_eq!(
r(b"max-age=99999999999999999999").max_age,
Some(2_147_483_648)
);
let dup = r(b"max-age=1, max-age=2");
assert_eq!((dup.max_age, dup.duplicates), (Some(1), true));
let nc = r(b"no-cache=\"Set-Cookie\"");
assert!(nc.no_cache && !nc.invalid_values);
let both = r(b"private, public");
assert!(both.private && both.public);
assert_eq!(r(b"x-unknown, y=1").unknown, 2);
assert!(r(b"no-store, , NO-TRANSFORM, immutable").no_transform);
assert_eq!(
r(b"stale-while-revalidate=30, stale-if-error=60").stale_if_error,
Some(60)
);
assert!(r(b"no-store=1").invalid_values);
assert!(r(b"max-age").invalid_values);
assert_eq!(r(b"").unknown, 0);
}
#[test]
fn test_request_directives_cases() {
let q = |v: &[u8]| request_cache_control(v).unwrap();
assert_eq!(q(b"max-stale").max_stale, Some(None));
assert_eq!(q(b"max-stale=10").max_stale, Some(Some(10)));
assert!(q(b"only-if-cached").only_if_cached);
assert_eq!(q(b"min-fresh=5, min-fresh=6").min_fresh, Some(5));
assert!(q(b"min-fresh=5, min-fresh=6").duplicates);
}
#[test]
fn test_cache_structure_errors() {
let k = |v: &[u8]| response_cache_control(v).unwrap_err().kind();
assert_eq!(k(b"max-age = 5"), ErrorKind::NotToken);
assert_eq!(k(b"max-age=\"5"), ErrorKind::UnterminatedQuote);
assert_eq!(k(b"=5"), ErrorKind::NotToken);
assert_eq!(k(b"a=b c"), ErrorKind::NotToken);
assert_eq!(k(b"a=\x01"), ErrorKind::Control);
}
#[test]
fn test_lines() {
let lines: [&[u8]; 2] = [b"public", b"max-age=5"];
let r = response_lines(lines).unwrap();
assert!(r.public && r.max_age == Some(5));
let bad: [&[u8]; 2] = [b"public", b"max-age=\"5"];
assert_eq!(response_lines(bad).unwrap_err().line(), 1);
let req: [&[u8]; 2] = [b"no-cache", b"max-stale"];
assert_eq!(request_lines(req).unwrap().max_stale, Some(None));
}
#[test]
fn test_writer_round_trip_and_refusals() {
let mut buf = [0u8; 128];
let mut out = crate::SliceSink::new(&mut buf);
Writer::new(&mut out)
.flag("public")
.and_then(|w| w.seconds("max-age", u32::MAX))
.and_then(|w| w.fields("private", &["X-A"]))
.map(|_| ())
.unwrap();
let r = response_cache_control(out.written()).unwrap();
assert!(r.public && r.private);
assert_eq!(r.max_age, Some(2_147_483_648));
let mut w = Writer::new(&mut out);
assert!(matches!(w.flag("a b"), Err(WriteError::NotToken)));
assert!(matches!(
w.fields("no-cache", &["a b"]),
Err(WriteError::NotToken)
));
let mut small = [0u8; 3];
let mut out = crate::SliceSink::new(&mut small);
assert!(matches!(
Writer::new(&mut out).flag("public"),
Err(WriteError::Full)
));
assert!(out.is_empty());
}
}