Skip to main content

parse_net/
cache.rs

1//! `Cache-Control` (RFC 9111 section 5.2, with `immutable` from RFC 8246 and
2//! `stale-while-revalidate` and `stale-if-error` from RFC 5861).
3//!
4//! # Examples
5//!
6//! ```
7//! use parse_net::{self as pn, cache::Writer};
8//!
9//! let cc = pn::response_cache_control(b"public, max-age=\"60\", MAX-AGE=5, x-ext=1")?;
10//! assert_eq!((cc.public, cc.max_age, cc.duplicates, cc.unknown), (true, Some(60), true, 1));
11//!
12//! let mut buf = [0u8; 64];
13//! let mut out = pn::SliceSink::new(&mut buf);
14//! Writer::new(&mut out).flag("public").and_then(|w| w.seconds("max-age", 60)).expect("room");
15//! assert_eq!(out.written(), b"public, max-age=60");
16//! # Ok::<(), pn::ParseError>(())
17//! ```
18
19use crate::{
20    class,
21    error::{ErrorKind, ParseError, Result, WriteError},
22    limits::Limits,
23    list,
24    num::parse_u32_saturating,
25    scan::control_kind,
26    text::{Word, is_token, parse_word},
27    write::{Sink, reserve, write_decimal_u64},
28};
29
30/// One directive: a name and an optional argument.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub struct Directive<'a> {
33    /// The name as sent (compare it ASCII case-insensitively).
34    pub name: &'a [u8],
35    /// The argument, token or quoted.
36    pub value: Option<Word<'a>>,
37}
38
39/// Parses one `token [ "=" ( token / quoted-string ) ]`.
40fn directive(e: &[u8]) -> Result<Directive<'_>> {
41    let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
42    if n == 0 {
43        return Err(bad(e, 0));
44    }
45    let name = e.get(..n).unwrap_or(&[]);
46    match e.get(n) {
47        None => Ok(Directive { name, value: None }),
48        Some(b'=') => {
49            let rest = e.get(n + 1..).unwrap_or(&[]);
50            let (w, used) = parse_word(rest).map_err(|x| match x.kind() {
51                ErrorKind::Empty => bad(e, n + 1),
52                _ => x.shifted(n + 1),
53            })?;
54            if n + 1 + used != e.len() {
55                return Err(bad(e, n + 1 + used));
56            }
57            Ok(Directive {
58                name,
59                value: Some(w),
60            })
61        }
62        Some(_) => Err(bad(e, n)),
63    }
64}
65
66fn bad(e: &[u8], at: usize) -> ParseError {
67    match e.get(at) {
68        Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
69        _ => ParseError::new(ErrorKind::NotToken, at),
70    }
71}
72
73/// Validated directives.
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
75pub struct Directives<'a> {
76    list: list::List<'a>,
77}
78
79impl<'a> Directives<'a> {
80    /// The directives, in order.
81    pub fn iter(&self) -> impl Iterator<Item = Directive<'a>> + use<'a> {
82        self.list.iter().filter_map(|e| directive(e).ok())
83    }
84
85    /// The number of directives.
86    #[must_use]
87    pub const fn len(&self) -> usize {
88        self.list.len()
89    }
90
91    /// Whether there are no directives.
92    #[must_use]
93    pub const fn is_empty(&self) -> bool {
94        self.list.is_empty()
95    }
96}
97
98/// Parses `#( token [ "=" ( token / quoted-string ) ] )`, keeping every
99/// directive raw.
100///
101/// # Errors
102///
103/// The list errors; [`ErrorKind::NotToken`] for a malformed directive; the
104/// quoted-string errors.
105///
106/// # Examples
107///
108/// ```
109/// use parse_net::{cache, Limits};
110///
111/// let d = cache::parse(b"no-cache=\"Set-Cookie\", x-y", &Limits::DEFAULT)?;
112/// let names: Vec<&[u8]> = d.iter().map(|x| x.name).collect();
113/// assert_eq!(names, [&b"no-cache"[..], b"x-y"]);
114/// # Ok::<(), parse_net::ParseError>(())
115/// ```
116pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<Directives<'a>> {
117    let l = list::parse(value, limits)?;
118    for e in l.iter() {
119        let at = (e.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
120        directive(e).map(|_| ()).map_err(|x| x.shifted(at))?;
121    }
122    Ok(Directives { list: l })
123}
124
125/// The response directives (RFC 9111 section 5.2.2 and the extensions).
126///
127/// Unknown directives are counted in `unknown` and available raw through
128/// [`parse`]. A directive given twice keeps its first value and sets
129/// `duplicates` (RFC 9111 section 4.2.1); an argument that does not fit
130/// (`max-age=60s`) leaves the field `None` and sets `invalid_values`: treat
131/// such a response as stale. Conflicting directives (`private, public`) are
132/// both reported.
133#[non_exhaustive]
134#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
135pub struct ResponseDirectives {
136    /// `max-age`, saturating at 2147483648.
137    pub max_age: Option<u32>,
138    /// `s-maxage`.
139    pub s_maxage: Option<u32>,
140    /// `no-cache` (with or without a field list).
141    pub no_cache: bool,
142    /// `no-store`.
143    pub no_store: bool,
144    /// `private` (with or without a field list).
145    pub private: bool,
146    /// `public`.
147    pub public: bool,
148    /// `must-revalidate`.
149    pub must_revalidate: bool,
150    /// `proxy-revalidate`.
151    pub proxy_revalidate: bool,
152    /// `must-understand`.
153    pub must_understand: bool,
154    /// `no-transform`.
155    pub no_transform: bool,
156    /// `immutable` (RFC 8246).
157    pub immutable: bool,
158    /// `stale-while-revalidate` (RFC 5861).
159    pub stale_while_revalidate: Option<u32>,
160    /// `stale-if-error` (RFC 5861).
161    pub stale_if_error: Option<u32>,
162    /// A known directive appeared more than once.
163    pub duplicates: bool,
164    /// A known directive had an argument that does not fit its grammar.
165    pub invalid_values: bool,
166    /// The number of extension directives (saturating).
167    pub unknown: u16,
168}
169
170/// The request directives (RFC 9111 section 5.2.1).
171#[non_exhaustive]
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
173pub struct RequestDirectives {
174    /// `max-age`.
175    pub max_age: Option<u32>,
176    /// `max-stale`: `Some(None)` without a value (any staleness).
177    pub max_stale: Option<Option<u32>>,
178    /// `min-fresh`.
179    pub min_fresh: Option<u32>,
180    /// `no-cache`.
181    pub no_cache: bool,
182    /// `no-store`.
183    pub no_store: bool,
184    /// `no-transform`.
185    pub no_transform: bool,
186    /// `only-if-cached`.
187    pub only_if_cached: bool,
188    /// `stale-if-error` (RFC 5861).
189    pub stale_if_error: Option<u32>,
190    /// A known directive appeared more than once.
191    pub duplicates: bool,
192    /// A known directive had an argument that does not fit its grammar.
193    pub invalid_values: bool,
194    /// The number of extension directives (saturating).
195    pub unknown: u16,
196}
197
198/// Reads a `delta-seconds` argument, token or quoted (RFC 9111 section 5.2:
199/// recipients accept both forms).
200fn seconds(value: Option<Word<'_>>, invalid: &mut bool) -> Option<u32> {
201    let mut buf = [0u8; 32];
202    let digits = match value {
203        Some(Word::Token(t)) => t,
204        Some(q @ Word::Quoted(_)) => match q.unescape_into(&mut buf) {
205            Ok(d) => d,
206            Err(_) => {
207                *invalid = true;
208                return None;
209            }
210        },
211        None => {
212            *invalid = true;
213            return None;
214        }
215    };
216    match parse_u32_saturating(digits) {
217        Ok(n) => Some(n),
218        Err(_) => {
219            *invalid = true;
220            None
221        }
222    }
223}
224
225/// A flag directive: no argument expected; one (other than a field list for
226/// `no-cache` and `private`) marks the value invalid.
227fn flag(
228    slot: &mut bool,
229    seen: &mut bool,
230    dup: &mut bool,
231    value: Option<Word<'_>>,
232    invalid: &mut bool,
233    list_ok: bool,
234) {
235    if *seen {
236        *dup = true;
237        return;
238    }
239    *seen = true;
240    *slot = true;
241    if value.is_some() && !list_ok {
242        *invalid = true;
243    }
244}
245
246/// The first value of a numeric directive; a repeat sets `dup`.
247fn number(
248    slot: &mut Option<u32>,
249    seen: &mut bool,
250    dup: &mut bool,
251    value: Option<Word<'_>>,
252    invalid: &mut bool,
253) {
254    if *seen {
255        *dup = true;
256        return;
257    }
258    *seen = true;
259    *slot = seconds(value, invalid);
260}
261
262fn response_from<'a>(directives: impl Iterator<Item = Directive<'a>>) -> ResponseDirectives {
263    let mut r = ResponseDirectives::default();
264    let mut seen = [false; 13];
265    for d in directives {
266        let n = d.name;
267        let is = |s: &[u8]| n.eq_ignore_ascii_case(s);
268        let (dup, inv) = (&mut r.duplicates, &mut r.invalid_values);
269        match () {
270            () if is(b"max-age") => number(&mut r.max_age, &mut seen[0], dup, d.value, inv),
271            () if is(b"s-maxage") => number(&mut r.s_maxage, &mut seen[1], dup, d.value, inv),
272            () if is(b"no-cache") => flag(&mut r.no_cache, &mut seen[2], dup, d.value, inv, true),
273            () if is(b"no-store") => flag(&mut r.no_store, &mut seen[3], dup, d.value, inv, false),
274            () if is(b"private") => flag(&mut r.private, &mut seen[4], dup, d.value, inv, true),
275            () if is(b"public") => flag(&mut r.public, &mut seen[5], dup, d.value, inv, false),
276            () if is(b"must-revalidate") => flag(
277                &mut r.must_revalidate,
278                &mut seen[6],
279                dup,
280                d.value,
281                inv,
282                false,
283            ),
284            () if is(b"proxy-revalidate") => flag(
285                &mut r.proxy_revalidate,
286                &mut seen[7],
287                dup,
288                d.value,
289                inv,
290                false,
291            ),
292            () if is(b"must-understand") => flag(
293                &mut r.must_understand,
294                &mut seen[8],
295                dup,
296                d.value,
297                inv,
298                false,
299            ),
300            () if is(b"no-transform") => {
301                flag(&mut r.no_transform, &mut seen[9], dup, d.value, inv, false)
302            }
303            () if is(b"immutable") => {
304                flag(&mut r.immutable, &mut seen[10], dup, d.value, inv, false)
305            }
306            () if is(b"stale-while-revalidate") => {
307                number(
308                    &mut r.stale_while_revalidate,
309                    &mut seen[11],
310                    dup,
311                    d.value,
312                    inv,
313                );
314            }
315            () if is(b"stale-if-error") => {
316                number(&mut r.stale_if_error, &mut seen[12], dup, d.value, inv)
317            }
318            () => r.unknown = r.unknown.saturating_add(1),
319        }
320    }
321    r
322}
323
324fn request_from<'a>(directives: impl Iterator<Item = Directive<'a>>) -> RequestDirectives {
325    let mut r = RequestDirectives::default();
326    let mut seen = [false; 8];
327    for d in directives {
328        let n = d.name;
329        let is = |s: &[u8]| n.eq_ignore_ascii_case(s);
330        let (dup, inv) = (&mut r.duplicates, &mut r.invalid_values);
331        match () {
332            () if is(b"max-age") => number(&mut r.max_age, &mut seen[0], dup, d.value, inv),
333            () if is(b"max-stale") => {
334                if seen[1] {
335                    *dup = true;
336                } else {
337                    seen[1] = true;
338                    r.max_stale = Some(match d.value {
339                        None => None,
340                        v => seconds(v, inv),
341                    });
342                }
343            }
344            () if is(b"min-fresh") => number(&mut r.min_fresh, &mut seen[2], dup, d.value, inv),
345            () if is(b"no-cache") => flag(&mut r.no_cache, &mut seen[3], dup, d.value, inv, false),
346            () if is(b"no-store") => flag(&mut r.no_store, &mut seen[4], dup, d.value, inv, false),
347            () if is(b"no-transform") => {
348                flag(&mut r.no_transform, &mut seen[5], dup, d.value, inv, false)
349            }
350            () if is(b"only-if-cached") => flag(
351                &mut r.only_if_cached,
352                &mut seen[6],
353                dup,
354                d.value,
355                inv,
356                false,
357            ),
358            () if is(b"stale-if-error") => {
359                number(&mut r.stale_if_error, &mut seen[7], dup, d.value, inv)
360            }
361            () => r.unknown = r.unknown.saturating_add(1),
362        }
363    }
364    r
365}
366
367/// Reads a response `Cache-Control` value.
368///
369/// # Errors
370///
371/// As [`parse`]: only structural errors; unknown directives and bad
372/// arguments are reported in the struct.
373///
374/// # Examples
375///
376/// ```
377/// let cc = parse_net::response_cache_control(b"max-age=99999999999999999999, private, public")?;
378/// assert_eq!(cc.max_age, Some(2_147_483_648));
379/// assert!(cc.private && cc.public);
380/// # Ok::<(), parse_net::ParseError>(())
381/// ```
382pub fn response_cache_control(value: &[u8]) -> Result<ResponseDirectives> {
383    Ok(response_from(parse(value, &Limits::DEFAULT)?.iter()))
384}
385
386/// Reads a request `Cache-Control` value.
387///
388/// # Errors
389///
390/// As [`parse`].
391///
392/// # Examples
393///
394/// ```
395/// let req = parse_net::request_cache_control(b"max-stale, no-cache")?;
396/// assert_eq!(req.max_stale, Some(None));
397/// assert!(req.no_cache);
398/// # Ok::<(), parse_net::ParseError>(())
399/// ```
400pub fn request_cache_control(value: &[u8]) -> Result<RequestDirectives> {
401    Ok(request_from(parse(value, &Limits::DEFAULT)?.iter()))
402}
403
404/// Reads several response `Cache-Control` field lines as one.
405///
406/// # Errors
407///
408/// As [`parse`], with [`ParseError::line`] set.
409pub fn response_lines<'a, I>(lines: I) -> Result<ResponseDirectives>
410where
411    I: IntoIterator<Item = &'a [u8]>,
412    I::IntoIter: Clone,
413{
414    let lines = lines.into_iter();
415    for (n, line) in lines.clone().enumerate() {
416        parse(line, &Limits::DEFAULT)
417            .map(|_| ())
418            .map_err(|e| e.on_line(n))?;
419    }
420    Ok(response_from(lines.flat_map(|l| {
421        list::parse(l, &Limits::DEFAULT)
422            .into_iter()
423            .flat_map(|x| x.iter())
424            .filter_map(|e| directive(e).ok())
425    })))
426}
427
428/// Reads several request `Cache-Control` field lines as one.
429///
430/// # Errors
431///
432/// As [`parse`], with [`ParseError::line`] set.
433pub fn request_lines<'a, I>(lines: I) -> Result<RequestDirectives>
434where
435    I: IntoIterator<Item = &'a [u8]>,
436    I::IntoIter: Clone,
437{
438    let lines = lines.into_iter();
439    for (n, line) in lines.clone().enumerate() {
440        parse(line, &Limits::DEFAULT)
441            .map(|_| ())
442            .map_err(|e| e.on_line(n))?;
443    }
444    Ok(request_from(lines.flat_map(|l| {
445        list::parse(l, &Limits::DEFAULT)
446            .into_iter()
447            .flat_map(|x| x.iter())
448            .filter_map(|e| directive(e).ok())
449    })))
450}
451
452/// Writes `Cache-Control` directives, separated by `, `.
453///
454/// Delta-seconds are always written as tokens (senders must not quote
455/// them); field lists are always quoted.
456pub struct Writer<'s, S: Sink> {
457    out: &'s mut S,
458    first: bool,
459}
460
461impl<'s, S: Sink> Writer<'s, S> {
462    /// A writer over `out`.
463    pub fn new(out: &'s mut S) -> Self {
464        Self { out, first: true }
465    }
466
467    fn separator(&self) -> &'static [u8] {
468        if self.first { b"" } else { b", " }
469    }
470
471    /// Writes a directive with no argument (`no-store`).
472    ///
473    /// # Errors
474    ///
475    /// [`WriteError::NotToken`]; [`WriteError::Full`] (nothing written for
476    /// this directive when the sink reports its room).
477    pub fn flag(&mut self, name: &str) -> core::result::Result<&mut Self, WriteError> {
478        if !is_token(name.as_bytes()) {
479            return Err(WriteError::NotToken);
480        }
481        let sep = self.separator();
482        reserve(self.out, sep.len() + name.len())?;
483        self.out.put(sep)?;
484        self.out.put(name.as_bytes())?;
485        self.first = false;
486        Ok(self)
487    }
488
489    /// Writes `name=secs` (`max-age=60`).
490    ///
491    /// # Errors
492    ///
493    /// As [`flag`](Self::flag).
494    pub fn seconds(
495        &mut self,
496        name: &str,
497        secs: u32,
498    ) -> core::result::Result<&mut Self, WriteError> {
499        if !is_token(name.as_bytes()) {
500            return Err(WriteError::NotToken);
501        }
502        let mut digits = crate::write::CountSink::new();
503        write_decimal_u64(u64::from(secs), &mut digits)?;
504        let sep = self.separator();
505        reserve(self.out, sep.len() + name.len() + 1 + digits.count())?;
506        self.out.put(sep)?;
507        self.out.put(name.as_bytes())?;
508        self.out.put(b"=")?;
509        write_decimal_u64(u64::from(secs), self.out)?;
510        self.first = false;
511        Ok(self)
512    }
513
514    /// Writes `name="field, field"` (`no-cache="Set-Cookie"`).
515    ///
516    /// # Errors
517    ///
518    /// [`WriteError::NotToken`] for the name or a field name, and as
519    /// [`flag`](Self::flag).
520    ///
521    /// # Examples
522    ///
523    /// ```
524    /// use parse_net::{cache::Writer, SliceSink};
525    ///
526    /// let mut buf = [0u8; 64];
527    /// let mut out = SliceSink::new(&mut buf);
528    /// Writer::new(&mut out).fields("no-cache", &["Set-Cookie", "Authorization"]).expect("room");
529    /// assert_eq!(out.written(), b"no-cache=\"Set-Cookie, Authorization\"");
530    /// ```
531    pub fn fields(
532        &mut self,
533        name: &str,
534        fields: &[&str],
535    ) -> core::result::Result<&mut Self, WriteError> {
536        if !is_token(name.as_bytes()) || fields.iter().any(|f| !is_token(f.as_bytes())) {
537            return Err(WriteError::NotToken);
538        }
539        let sep = self.separator();
540        let list_len: usize =
541            fields.iter().map(|f| f.len()).sum::<usize>() + fields.len().saturating_sub(1) * 2;
542        reserve(self.out, sep.len() + name.len() + 3 + list_len)?;
543        self.out.put(sep)?;
544        self.out.put(name.as_bytes())?;
545        self.out.put(b"=\"")?;
546        for (i, f) in fields.iter().enumerate() {
547            if i > 0 {
548                self.out.put(b", ")?;
549            }
550            self.out.put(f.as_bytes())?;
551        }
552        self.out.put(b"\"")?;
553        self.first = false;
554        Ok(self)
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561
562    #[test]
563    fn test_response_directives_cases() {
564        let r = |v: &[u8]| response_cache_control(v).unwrap();
565        assert_eq!(r(b"max-age=0").max_age, Some(0));
566        assert_eq!(r(b"max-age=\"60\"").max_age, Some(60));
567        let bad = r(b"max-age=60s");
568        assert_eq!((bad.max_age, bad.invalid_values), (None, true));
569        assert_eq!(
570            r(b"max-age=99999999999999999999").max_age,
571            Some(2_147_483_648)
572        );
573        let dup = r(b"max-age=1, max-age=2");
574        assert_eq!((dup.max_age, dup.duplicates), (Some(1), true));
575        let nc = r(b"no-cache=\"Set-Cookie\"");
576        assert!(nc.no_cache && !nc.invalid_values);
577        let both = r(b"private, public");
578        assert!(both.private && both.public);
579        assert_eq!(r(b"x-unknown, y=1").unknown, 2);
580        assert!(r(b"no-store, , NO-TRANSFORM, immutable").no_transform);
581        assert_eq!(
582            r(b"stale-while-revalidate=30, stale-if-error=60").stale_if_error,
583            Some(60)
584        );
585        assert!(r(b"no-store=1").invalid_values);
586        assert!(r(b"max-age").invalid_values);
587        assert_eq!(r(b"").unknown, 0);
588    }
589
590    #[test]
591    fn test_request_directives_cases() {
592        let q = |v: &[u8]| request_cache_control(v).unwrap();
593        assert_eq!(q(b"max-stale").max_stale, Some(None));
594        assert_eq!(q(b"max-stale=10").max_stale, Some(Some(10)));
595        assert!(q(b"only-if-cached").only_if_cached);
596        assert_eq!(q(b"min-fresh=5, min-fresh=6").min_fresh, Some(5));
597        assert!(q(b"min-fresh=5, min-fresh=6").duplicates);
598    }
599
600    #[test]
601    fn test_cache_structure_errors() {
602        let k = |v: &[u8]| response_cache_control(v).unwrap_err().kind();
603        assert_eq!(k(b"max-age = 5"), ErrorKind::NotToken);
604        assert_eq!(k(b"max-age=\"5"), ErrorKind::UnterminatedQuote);
605        assert_eq!(k(b"=5"), ErrorKind::NotToken);
606        assert_eq!(k(b"a=b c"), ErrorKind::NotToken);
607        assert_eq!(k(b"a=\x01"), ErrorKind::Control);
608    }
609
610    #[test]
611    fn test_lines() {
612        let lines: [&[u8]; 2] = [b"public", b"max-age=5"];
613        let r = response_lines(lines).unwrap();
614        assert!(r.public && r.max_age == Some(5));
615        let bad: [&[u8]; 2] = [b"public", b"max-age=\"5"];
616        assert_eq!(response_lines(bad).unwrap_err().line(), 1);
617        let req: [&[u8]; 2] = [b"no-cache", b"max-stale"];
618        assert_eq!(request_lines(req).unwrap().max_stale, Some(None));
619    }
620
621    #[test]
622    fn test_writer_round_trip_and_refusals() {
623        let mut buf = [0u8; 128];
624        let mut out = crate::SliceSink::new(&mut buf);
625        Writer::new(&mut out)
626            .flag("public")
627            .and_then(|w| w.seconds("max-age", u32::MAX))
628            .and_then(|w| w.fields("private", &["X-A"]))
629            .map(|_| ())
630            .unwrap();
631        let r = response_cache_control(out.written()).unwrap();
632        assert!(r.public && r.private);
633        assert_eq!(r.max_age, Some(2_147_483_648));
634        let mut w = Writer::new(&mut out);
635        assert!(matches!(w.flag("a b"), Err(WriteError::NotToken)));
636        assert!(matches!(
637            w.fields("no-cache", &["a b"]),
638            Err(WriteError::NotToken)
639        ));
640        let mut small = [0u8; 3];
641        let mut out = crate::SliceSink::new(&mut small);
642        assert!(matches!(
643            Writer::new(&mut out).flag("public"),
644            Err(WriteError::Full)
645        ));
646        assert!(out.is_empty());
647    }
648}