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    error::{ErrorKind, ParseError, Result, WriteError},
21    limits::Limits,
22    num::parse_u32_saturating,
23    scan::control_kind,
24    text::{Word, is_token, parse_word, skip_ows, token_len, trim_ows},
25    write::{Sink, reserve, write_decimal_u64},
26};
27
28/// One directive: a name and an optional argument.
29///
30/// # Examples
31///
32/// ```
33/// use parse_net::{cache, Limits};
34///
35/// let d = cache::parse(b"max-age=60", &Limits::DEFAULT)?.iter().next().expect("one directive");
36/// assert_eq!(d.name, b"max-age");
37/// assert!(d.value.is_some_and(|v| v.eq_bytes(b"60")));
38/// # Ok::<(), parse_net::ParseError>(())
39/// ```
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41pub struct Directive<'a> {
42    /// The name as sent (compare it ASCII case-insensitively).
43    pub name: &'a [u8],
44    /// The argument, token or quoted.
45    pub value: Option<Word<'a>>,
46}
47
48/// Skips SP and HTAB.
49#[inline]
50fn skip_ws(mut s: &[u8]) -> &[u8] {
51    while let [b' ' | b'\t', rest @ ..] = s {
52        s = rest;
53    }
54    s
55}
56
57/// One directive at the front of `s`: `token [ "=" ( token / quoted-string ) ]`.
58/// Returns it and what follows; error offsets are relative to `s`.
59fn directive(s: &[u8]) -> Result<(Directive<'_>, &[u8])> {
60    let n = token_len(s);
61    if n == 0 {
62        return Err(bad(s, 0));
63    }
64    let (name, after) = s.split_at(n);
65    let [b'=', value @ ..] = after else {
66        return Ok((Directive { name, value: None }, after));
67    };
68    let (w, used) = parse_word(value).map_err(|x| match x.kind() {
69        ErrorKind::Empty => bad(s, n + 1),
70        _ => x.shifted(n + 1),
71    })?;
72    Ok((
73        Directive {
74            name,
75            value: Some(w),
76        },
77        value.get(used..).unwrap_or(&[]),
78    ))
79}
80
81/// Validates a `Cache-Control` value in one pass over `v` (OWS already
82/// trimmed), calling `f` for each directive; returns the count. Empty list
83/// elements are skipped up to `max_empty_elements`. The cursor is a
84/// shrinking slice; offsets are computed only for an error.
85fn walk<'a>(v: &'a [u8], limits: &Limits, mut f: impl FnMut(Directive<'a>)) -> Result<usize> {
86    let at = |rest: &[u8]| v.len() - rest.len();
87    let (mut rest, mut count, mut empty) = (v, 0usize, 0usize);
88    loop {
89        rest = skip_ws(rest);
90        match rest {
91            [] => return Ok(count),
92            [b',', tail @ ..] => {
93                empty += 1;
94                if empty > usize::from(limits.empty_elements) {
95                    return Err(ParseError::new(ErrorKind::TooManyEmptyElements, at(rest)));
96                }
97                rest = tail;
98                continue;
99            }
100            _ => {}
101        }
102        let (d, tail) = directive(rest).map_err(|e| e.shifted(at(rest)))?;
103        count += 1;
104        if count > usize::from(limits.list_members) {
105            return Err(ParseError::new(ErrorKind::TooManyMembers, at(rest)));
106        }
107        f(d);
108        rest = skip_ws(tail);
109        match rest {
110            [] => return Ok(count),
111            [b',', tail @ ..] => rest = tail,
112            _ => return Err(bad(rest, 0).shifted(at(rest))),
113        }
114    }
115}
116
117/// Runs [`walk`] over a whole field value, offsets relative to it.
118fn walk_value<'a>(
119    value: &'a [u8],
120    limits: &Limits,
121    f: impl FnMut(Directive<'a>),
122) -> Result<(&'a [u8], usize)> {
123    let lead = skip_ows(value);
124    let v = trim_ows(value);
125    let count = walk(v, limits, f).map_err(|e| e.shifted(lead))?;
126    Ok((v, count))
127}
128
129fn bad(e: &[u8], at: usize) -> ParseError {
130    match e.get(at) {
131        Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
132        _ => ParseError::new(ErrorKind::NotToken, at),
133    }
134}
135
136/// Validated directives.
137///
138/// # Examples
139///
140/// ```
141/// use parse_net::{cache, Limits};
142///
143/// let d = cache::parse(b"no-store, max-age=0", &Limits::DEFAULT)?;
144/// assert_eq!(d.len(), 2);
145/// assert!(d.iter().any(|x| x.name.eq_ignore_ascii_case(b"NO-STORE")));
146/// # Ok::<(), parse_net::ParseError>(())
147/// ```
148#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
149pub struct Directives<'a> {
150    /// The validated value, OWS trimmed.
151    text: &'a [u8],
152    count: usize,
153}
154
155impl<'a> Directives<'a> {
156    /// The directives, in order.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use parse_net::{cache, Limits};
162    ///
163    /// let d = cache::parse(b"public, max-age=60", &Limits::DEFAULT)?;
164    /// let has_value: Vec<bool> = d.iter().map(|x| x.value.is_some()).collect();
165    /// assert_eq!(has_value, [false, true]);
166    /// # Ok::<(), parse_net::ParseError>(())
167    /// ```
168    pub fn iter(&self) -> impl Iterator<Item = Directive<'a>> + use<'a> {
169        DirectiveIter { s: self.text }
170    }
171
172    /// The number of directives.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use parse_net::{cache, Limits};
178    ///
179    /// assert_eq!(cache::parse(b"no-cache, no-store, x-ext=1", &Limits::DEFAULT)?.len(), 3);
180    /// # Ok::<(), parse_net::ParseError>(())
181    /// ```
182    #[must_use]
183    pub const fn len(&self) -> usize {
184        self.count
185    }
186
187    /// Whether there are no directives.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use parse_net::{cache, Limits};
193    ///
194    /// assert!(cache::parse(b"", &Limits::DEFAULT)?.is_empty());
195    /// assert!(!cache::parse(b"no-store", &Limits::DEFAULT)?.is_empty());
196    /// # Ok::<(), parse_net::ParseError>(())
197    /// ```
198    #[must_use]
199    pub const fn is_empty(&self) -> bool {
200        self.count == 0
201    }
202}
203
204/// The directives of a validated [`Directives`].
205struct DirectiveIter<'a> {
206    s: &'a [u8],
207}
208
209impl<'a> Iterator for DirectiveIter<'a> {
210    type Item = Directive<'a>;
211
212    fn next(&mut self) -> Option<Directive<'a>> {
213        let mut rest = self.s;
214        while let [b' ' | b'\t' | b',', tail @ ..] = rest {
215            rest = tail;
216        }
217        let (d, tail) = directive(rest).ok()?;
218        self.s = tail;
219        Some(d)
220    }
221}
222
223/// Parses `#( token [ "=" ( token / quoted-string ) ] )`, keeping every
224/// directive raw.
225///
226/// # Errors
227///
228/// The list errors; [`ErrorKind::NotToken`] for a malformed directive; the
229/// quoted-string errors.
230///
231/// # Examples
232///
233/// ```
234/// use parse_net::{cache, Limits};
235///
236/// let d = cache::parse(b"no-cache=\"Set-Cookie\", x-y", &Limits::DEFAULT)?;
237/// let names: Vec<&[u8]> = d.iter().map(|x| x.name).collect();
238/// assert_eq!(names, [&b"no-cache"[..], b"x-y"]);
239/// # Ok::<(), parse_net::ParseError>(())
240/// ```
241pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<Directives<'a>> {
242    let (text, count) = walk_value(value, limits, |_| {})?;
243    Ok(Directives { text, count })
244}
245
246/// The response directives (RFC 9111 section 5.2.2 and the extensions).
247///
248/// Unknown directives are counted in `unknown` and available raw through
249/// [`parse`]. A directive given twice keeps its first value and sets
250/// `duplicates` (RFC 9111 section 4.2.1); an argument that does not fit
251/// (`max-age=60s`) leaves the field `None` and sets `invalid_values`: treat
252/// such a response as stale. Conflicting directives (`private, public`) are
253/// both reported.
254///
255/// # Examples
256///
257/// ```
258/// let cc = parse_net::response_cache_control(b"max-age=60, s-maxage=60s, immutable")?;
259/// assert_eq!(cc.max_age, Some(60));
260/// assert!(cc.immutable);
261/// assert!(cc.s_maxage.is_none() && cc.invalid_values);
262/// # Ok::<(), parse_net::ParseError>(())
263/// ```
264#[non_exhaustive]
265#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
266pub struct ResponseDirectives {
267    /// `max-age`, saturating at 2147483648.
268    pub max_age: Option<u32>,
269    /// `s-maxage`.
270    pub s_maxage: Option<u32>,
271    /// `no-cache` (with or without a field list).
272    pub no_cache: bool,
273    /// `no-store`.
274    pub no_store: bool,
275    /// `private` (with or without a field list).
276    pub private: bool,
277    /// `public`.
278    pub public: bool,
279    /// `must-revalidate`.
280    pub must_revalidate: bool,
281    /// `proxy-revalidate`.
282    pub proxy_revalidate: bool,
283    /// `must-understand`.
284    pub must_understand: bool,
285    /// `no-transform`.
286    pub no_transform: bool,
287    /// `immutable` (RFC 8246).
288    pub immutable: bool,
289    /// `stale-while-revalidate` (RFC 5861).
290    pub stale_while_revalidate: Option<u32>,
291    /// `stale-if-error` (RFC 5861).
292    pub stale_if_error: Option<u32>,
293    /// A known directive appeared more than once.
294    pub duplicates: bool,
295    /// A known directive had an argument that does not fit its grammar.
296    pub invalid_values: bool,
297    /// The number of extension directives (saturating).
298    pub unknown: u16,
299}
300
301/// The request directives (RFC 9111 section 5.2.1).
302///
303/// # Examples
304///
305/// ```
306/// let req = parse_net::request_cache_control(b"no-store, min-fresh=30, max-age=1, max-age=2")?;
307/// assert!(req.no_store);
308/// assert_eq!((req.min_fresh, req.max_age, req.duplicates), (Some(30), Some(1), true));
309/// # Ok::<(), parse_net::ParseError>(())
310/// ```
311#[non_exhaustive]
312#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
313pub struct RequestDirectives {
314    /// `max-age`.
315    pub max_age: Option<u32>,
316    /// `max-stale`: `Some(None)` without a value (any staleness).
317    pub max_stale: Option<Option<u32>>,
318    /// `min-fresh`.
319    pub min_fresh: Option<u32>,
320    /// `no-cache`.
321    pub no_cache: bool,
322    /// `no-store`.
323    pub no_store: bool,
324    /// `no-transform`.
325    pub no_transform: bool,
326    /// `only-if-cached`.
327    pub only_if_cached: bool,
328    /// `stale-if-error` (RFC 5861).
329    pub stale_if_error: Option<u32>,
330    /// A known directive appeared more than once.
331    pub duplicates: bool,
332    /// A known directive had an argument that does not fit its grammar.
333    pub invalid_values: bool,
334    /// The number of extension directives (saturating).
335    pub unknown: u16,
336}
337
338/// Reads a `delta-seconds` argument, token or quoted (RFC 9111 section 5.2:
339/// recipients accept both forms).
340fn seconds(value: Option<Word<'_>>, invalid: &mut bool) -> Option<u32> {
341    // Senders must not quote delta-seconds (RFC 9111 section 5.2), but
342    // recipients read both forms; a quoted pair is read through the
343    // unescaping iterator, so no buffer is needed.
344    let n = match value {
345        Some(Word::Token(t)) => parse_u32_saturating(t).ok(),
346        Some(Word::Quoted(q)) => match q.as_plain() {
347            Some(p) => parse_u32_saturating(p).ok(),
348            None => {
349                let mut n: Option<u32> = None;
350                for b in q.bytes() {
351                    if !b.is_ascii_digit() {
352                        n = None;
353                        break;
354                    }
355                    n = Some(
356                        n.unwrap_or(0)
357                            .saturating_mul(10)
358                            .saturating_add(u32::from(b - b'0'))
359                            .min(crate::num::DELTA_SECONDS_MAX),
360                    );
361                }
362                n
363            }
364        },
365        None => None,
366    };
367    if n.is_none() {
368        *invalid = true;
369    }
370    n
371}
372
373/// A flag directive: no argument expected; one (other than a field list for
374/// `no-cache` and `private`) marks the value invalid.
375fn flag(
376    slot: &mut bool,
377    seen: &mut bool,
378    dup: &mut bool,
379    value: Option<Word<'_>>,
380    invalid: &mut bool,
381    list_ok: bool,
382) {
383    if *seen {
384        *dup = true;
385        return;
386    }
387    *seen = true;
388    *slot = true;
389    if value.is_some() && !list_ok {
390        *invalid = true;
391    }
392}
393
394/// The first value of a numeric directive; a repeat sets `dup`.
395fn number(
396    slot: &mut Option<u32>,
397    seen: &mut bool,
398    dup: &mut bool,
399    value: Option<Word<'_>>,
400    invalid: &mut bool,
401) {
402    if *seen {
403        *dup = true;
404        return;
405    }
406    *seen = true;
407    *slot = seconds(value, invalid);
408}
409
410/// Whether the token `n` equals `lower`, a lowercase name of letters and
411/// `-`, ignoring ASCII case. Among token bytes, `b | 0x20` maps exactly `A-Z`
412/// onto `a-z` and leaves `-` alone (the only other byte it maps onto `-` is
413/// CR, which is not a token byte), so one OR per byte suffices.
414fn is_name(n: &[u8], lower: &[u8]) -> bool {
415    n.len() == lower.len() && n.iter().zip(lower).all(|(&b, &p)| b | 0x20 == p)
416}
417
418/// The index of a response directive name, dispatched on length first.
419fn response_slot(n: &[u8]) -> Option<usize> {
420    let is = |s: &[u8]| is_name(n, s);
421    match n.len() {
422        6 if is(b"public") => Some(5),
423        7 if is(b"max-age") => Some(0),
424        7 if is(b"private") => Some(4),
425        8 if is(b"s-maxage") => Some(1),
426        8 if is(b"no-cache") => Some(2),
427        8 if is(b"no-store") => Some(3),
428        9 if is(b"immutable") => Some(10),
429        12 if is(b"no-transform") => Some(9),
430        14 if is(b"stale-if-error") => Some(12),
431        15 if is(b"must-revalidate") => Some(6),
432        15 if is(b"must-understand") => Some(8),
433        16 if is(b"proxy-revalidate") => Some(7),
434        22 if is(b"stale-while-revalidate") => Some(11),
435        _ => None,
436    }
437}
438
439/// Builds [`ResponseDirectives`] one directive at a time.
440#[derive(Default)]
441struct ResponseBuilder {
442    r: ResponseDirectives,
443    seen: [bool; 13],
444}
445
446impl ResponseBuilder {
447    fn push(&mut self, d: Directive<'_>) {
448        let r = &mut self.r;
449        let Some(slot) = response_slot(d.name) else {
450            r.unknown = r.unknown.saturating_add(1);
451            return;
452        };
453        let Some(seen) = self.seen.get_mut(slot) else {
454            return;
455        };
456        let (dup, inv) = (&mut r.duplicates, &mut r.invalid_values);
457        match slot {
458            0 => number(&mut r.max_age, seen, dup, d.value, inv),
459            1 => number(&mut r.s_maxage, seen, dup, d.value, inv),
460            2 => flag(&mut r.no_cache, seen, dup, d.value, inv, true),
461            3 => flag(&mut r.no_store, seen, dup, d.value, inv, false),
462            4 => flag(&mut r.private, seen, dup, d.value, inv, true),
463            5 => flag(&mut r.public, seen, dup, d.value, inv, false),
464            6 => flag(&mut r.must_revalidate, seen, dup, d.value, inv, false),
465            7 => flag(&mut r.proxy_revalidate, seen, dup, d.value, inv, false),
466            8 => flag(&mut r.must_understand, seen, dup, d.value, inv, false),
467            9 => flag(&mut r.no_transform, seen, dup, d.value, inv, false),
468            10 => flag(&mut r.immutable, seen, dup, d.value, inv, false),
469            11 => number(&mut r.stale_while_revalidate, seen, dup, d.value, inv),
470            _ => number(&mut r.stale_if_error, seen, dup, d.value, inv),
471        }
472    }
473}
474
475/// The index of a request directive name, dispatched on length first.
476fn request_slot(n: &[u8]) -> Option<usize> {
477    let is = |s: &[u8]| is_name(n, s);
478    match n.len() {
479        7 if is(b"max-age") => Some(0),
480        8 if is(b"no-cache") => Some(3),
481        8 if is(b"no-store") => Some(4),
482        9 if is(b"max-stale") => Some(1),
483        9 if is(b"min-fresh") => Some(2),
484        12 if is(b"no-transform") => Some(5),
485        14 if is(b"only-if-cached") => Some(6),
486        14 if is(b"stale-if-error") => Some(7),
487        _ => None,
488    }
489}
490
491/// Builds [`RequestDirectives`] one directive at a time.
492#[derive(Default)]
493struct RequestBuilder {
494    r: RequestDirectives,
495    seen: [bool; 8],
496}
497
498impl RequestBuilder {
499    fn push(&mut self, d: Directive<'_>) {
500        let r = &mut self.r;
501        let Some(slot) = request_slot(d.name) else {
502            r.unknown = r.unknown.saturating_add(1);
503            return;
504        };
505        let Some(seen) = self.seen.get_mut(slot) else {
506            return;
507        };
508        let (dup, inv) = (&mut r.duplicates, &mut r.invalid_values);
509        match slot {
510            0 => number(&mut r.max_age, seen, dup, d.value, inv),
511            1 => {
512                if *seen {
513                    *dup = true;
514                } else {
515                    *seen = true;
516                    r.max_stale = Some(match d.value {
517                        None => None,
518                        v => seconds(v, inv),
519                    });
520                }
521            }
522            2 => number(&mut r.min_fresh, seen, dup, d.value, inv),
523            3 => flag(&mut r.no_cache, seen, dup, d.value, inv, false),
524            4 => flag(&mut r.no_store, seen, dup, d.value, inv, false),
525            5 => flag(&mut r.no_transform, seen, dup, d.value, inv, false),
526            6 => flag(&mut r.only_if_cached, seen, dup, d.value, inv, false),
527            _ => number(&mut r.stale_if_error, seen, dup, d.value, inv),
528        }
529    }
530}
531
532/// Reads a response `Cache-Control` value.
533///
534/// # Errors
535///
536/// As [`parse`]: only structural errors; unknown directives and bad
537/// arguments are reported in the struct.
538///
539/// # Examples
540///
541/// ```
542/// let cc = parse_net::response_cache_control(b"max-age=99999999999999999999, private, public")?;
543/// assert_eq!(cc.max_age, Some(2_147_483_648));
544/// assert!(cc.private && cc.public);
545/// # Ok::<(), parse_net::ParseError>(())
546/// ```
547pub fn response_cache_control(value: &[u8]) -> Result<ResponseDirectives> {
548    let mut b = ResponseBuilder::default();
549    let _ = walk_value(value, &Limits::DEFAULT, |d| b.push(d))?;
550    Ok(b.r)
551}
552
553/// Reads a request `Cache-Control` value.
554///
555/// # Errors
556///
557/// As [`parse`].
558///
559/// # Examples
560///
561/// ```
562/// let req = parse_net::request_cache_control(b"max-stale, no-cache")?;
563/// assert_eq!(req.max_stale, Some(None));
564/// assert!(req.no_cache);
565/// # Ok::<(), parse_net::ParseError>(())
566/// ```
567pub fn request_cache_control(value: &[u8]) -> Result<RequestDirectives> {
568    let mut b = RequestBuilder::default();
569    let _ = walk_value(value, &Limits::DEFAULT, |d| b.push(d))?;
570    Ok(b.r)
571}
572
573/// Reads several response `Cache-Control` field lines as one.
574///
575/// # Errors
576///
577/// As [`parse`], with [`ParseError::line`] set.
578///
579/// # Examples
580///
581/// ```
582/// use parse_net::cache;
583///
584/// let lines: [&[u8]; 2] = [b"public", b"max-age=300"];
585/// let cc = cache::response_lines(lines)?;
586/// assert!(cc.public);
587/// assert_eq!(cc.max_age, Some(300));
588/// # Ok::<(), parse_net::ParseError>(())
589/// ```
590pub fn response_lines<'a, I>(lines: I) -> Result<ResponseDirectives>
591where
592    I: IntoIterator<Item = &'a [u8]>,
593    I::IntoIter: Clone,
594{
595    let mut b = ResponseBuilder::default();
596    for (n, line) in lines.into_iter().enumerate() {
597        let _ = walk_value(line, &Limits::DEFAULT, |d| b.push(d)).map_err(|e| e.on_line(n))?;
598    }
599    Ok(b.r)
600}
601
602/// Reads several request `Cache-Control` field lines as one.
603///
604/// # Errors
605///
606/// As [`parse`], with [`ParseError::line`] set.
607///
608/// # Examples
609///
610/// ```
611/// use parse_net::cache;
612///
613/// let lines: [&[u8]; 2] = [b"no-cache", b"max-stale=60"];
614/// let req = cache::request_lines(lines)?;
615/// assert!(req.no_cache);
616/// assert_eq!(req.max_stale, Some(Some(60)));
617/// # Ok::<(), parse_net::ParseError>(())
618/// ```
619pub fn request_lines<'a, I>(lines: I) -> Result<RequestDirectives>
620where
621    I: IntoIterator<Item = &'a [u8]>,
622    I::IntoIter: Clone,
623{
624    let mut b = RequestBuilder::default();
625    for (n, line) in lines.into_iter().enumerate() {
626        let _ = walk_value(line, &Limits::DEFAULT, |d| b.push(d)).map_err(|e| e.on_line(n))?;
627    }
628    Ok(b.r)
629}
630
631/// Writes `Cache-Control` directives, separated by `, `.
632///
633/// Delta-seconds are always written as tokens (senders must not quote
634/// them); field lists are always quoted.
635///
636/// # Examples
637///
638/// ```
639/// use parse_net::{cache::Writer, SliceSink};
640///
641/// let mut buf = [0u8; 64];
642/// let mut out = SliceSink::new(&mut buf);
643/// Writer::new(&mut out).flag("no-store")?.seconds("max-age", 0)?;
644/// assert_eq!(out.written(), b"no-store, max-age=0");
645/// # Ok::<(), parse_net::WriteError>(())
646/// ```
647pub struct Writer<'s, S: Sink> {
648    out: &'s mut S,
649    first: bool,
650}
651
652impl<'s, S: Sink> Writer<'s, S> {
653    /// A writer over `out`.
654    ///
655    /// # Examples
656    ///
657    /// ```
658    /// use parse_net::{cache::Writer, SliceSink};
659    ///
660    /// let mut buf = [0u8; 16];
661    /// let mut out = SliceSink::new(&mut buf);
662    /// let mut w = Writer::new(&mut out);
663    /// w.flag("public")?;
664    /// assert_eq!(out.written(), b"public");
665    /// # Ok::<(), parse_net::WriteError>(())
666    /// ```
667    pub fn new(out: &'s mut S) -> Self {
668        Self { out, first: true }
669    }
670
671    fn separator(&self) -> &'static [u8] {
672        if self.first { b"" } else { b", " }
673    }
674
675    /// Writes a directive with no argument (`no-store`).
676    ///
677    /// # Errors
678    ///
679    /// [`WriteError::NotToken`]; [`WriteError::Full`] (nothing written for
680    /// this directive when the sink reports its room).
681    ///
682    /// # Examples
683    ///
684    /// ```
685    /// use parse_net::{cache::Writer, SliceSink, WriteError};
686    ///
687    /// let mut buf = [0u8; 64];
688    /// let mut out = SliceSink::new(&mut buf);
689    /// Writer::new(&mut out).flag("no-store")?.flag("no-transform")?;
690    /// assert_eq!(out.written(), b"no-store, no-transform");
691    /// assert!(matches!(Writer::new(&mut out).flag("no store"), Err(WriteError::NotToken)));
692    /// # Ok::<(), parse_net::WriteError>(())
693    /// ```
694    pub fn flag(&mut self, name: &str) -> core::result::Result<&mut Self, WriteError> {
695        if !is_token(name.as_bytes()) {
696            return Err(WriteError::NotToken);
697        }
698        let sep = self.separator();
699        reserve(self.out, sep.len() + name.len())?;
700        self.out.put(sep)?;
701        self.out.put(name.as_bytes())?;
702        self.first = false;
703        Ok(self)
704    }
705
706    /// Writes `name=secs` (`max-age=60`).
707    ///
708    /// # Errors
709    ///
710    /// As [`flag`](Self::flag).
711    ///
712    /// # Examples
713    ///
714    /// ```
715    /// use parse_net::{cache::Writer, SliceSink};
716    ///
717    /// let mut buf = [0u8; 64];
718    /// let mut out = SliceSink::new(&mut buf);
719    /// Writer::new(&mut out).seconds("max-age", 3600)?.seconds("stale-while-revalidate", 30)?;
720    /// assert_eq!(out.written(), b"max-age=3600, stale-while-revalidate=30");
721    /// # Ok::<(), parse_net::WriteError>(())
722    /// ```
723    pub fn seconds(
724        &mut self,
725        name: &str,
726        secs: u32,
727    ) -> core::result::Result<&mut Self, WriteError> {
728        if !is_token(name.as_bytes()) {
729            return Err(WriteError::NotToken);
730        }
731        let mut digits = crate::write::CountSink::new();
732        write_decimal_u64(u64::from(secs), &mut digits)?;
733        let sep = self.separator();
734        reserve(self.out, sep.len() + name.len() + 1 + digits.count())?;
735        self.out.put(sep)?;
736        self.out.put(name.as_bytes())?;
737        self.out.put(b"=")?;
738        write_decimal_u64(u64::from(secs), self.out)?;
739        self.first = false;
740        Ok(self)
741    }
742
743    /// Writes `name="field, field"` (`no-cache="Set-Cookie"`).
744    ///
745    /// # Errors
746    ///
747    /// [`WriteError::NotToken`] for the name or a field name, and as
748    /// [`flag`](Self::flag).
749    ///
750    /// # Examples
751    ///
752    /// ```
753    /// use parse_net::{cache::Writer, SliceSink};
754    ///
755    /// let mut buf = [0u8; 64];
756    /// let mut out = SliceSink::new(&mut buf);
757    /// Writer::new(&mut out).fields("no-cache", &["Set-Cookie", "Authorization"]).expect("room");
758    /// assert_eq!(out.written(), b"no-cache=\"Set-Cookie, Authorization\"");
759    /// ```
760    pub fn fields(
761        &mut self,
762        name: &str,
763        fields: &[&str],
764    ) -> core::result::Result<&mut Self, WriteError> {
765        if !is_token(name.as_bytes()) || fields.iter().any(|f| !is_token(f.as_bytes())) {
766            return Err(WriteError::NotToken);
767        }
768        let sep = self.separator();
769        let list_len: usize =
770            fields.iter().map(|f| f.len()).sum::<usize>() + fields.len().saturating_sub(1) * 2;
771        reserve(self.out, sep.len() + name.len() + 3 + list_len)?;
772        self.out.put(sep)?;
773        self.out.put(name.as_bytes())?;
774        self.out.put(b"=\"")?;
775        for (i, f) in fields.iter().enumerate() {
776            if i > 0 {
777                self.out.put(b", ")?;
778            }
779            self.out.put(f.as_bytes())?;
780        }
781        self.out.put(b"\"")?;
782        self.first = false;
783        Ok(self)
784    }
785}
786
787#[cfg(test)]
788mod tests {
789    use super::*;
790
791    #[test]
792    fn test_response_directives_cases() {
793        let r = |v: &[u8]| response_cache_control(v).unwrap();
794        assert_eq!(r(b"max-age=0").max_age, Some(0));
795        assert_eq!(r(b"max-age=\"60\"").max_age, Some(60));
796        let bad = r(b"max-age=60s");
797        assert_eq!((bad.max_age, bad.invalid_values), (None, true));
798        assert_eq!(
799            r(b"max-age=99999999999999999999").max_age,
800            Some(2_147_483_648)
801        );
802        let dup = r(b"max-age=1, max-age=2");
803        assert_eq!((dup.max_age, dup.duplicates), (Some(1), true));
804        let nc = r(b"no-cache=\"Set-Cookie\"");
805        assert!(nc.no_cache && !nc.invalid_values);
806        let both = r(b"private, public");
807        assert!(both.private && both.public);
808        assert_eq!(r(b"x-unknown, y=1").unknown, 2);
809        assert!(r(b"no-store, , NO-TRANSFORM, immutable").no_transform);
810        assert_eq!(
811            r(b"stale-while-revalidate=30, stale-if-error=60").stale_if_error,
812            Some(60)
813        );
814        assert!(r(b"no-store=1").invalid_values);
815        assert!(r(b"max-age").invalid_values);
816        assert_eq!(r(b"").unknown, 0);
817    }
818
819    #[test]
820    fn test_request_directives_cases() {
821        let q = |v: &[u8]| request_cache_control(v).unwrap();
822        assert_eq!(q(b"max-stale").max_stale, Some(None));
823        assert_eq!(q(b"max-stale=10").max_stale, Some(Some(10)));
824        assert!(q(b"only-if-cached").only_if_cached);
825        assert_eq!(q(b"min-fresh=5, min-fresh=6").min_fresh, Some(5));
826        assert!(q(b"min-fresh=5, min-fresh=6").duplicates);
827    }
828
829    #[test]
830    fn test_cache_structure_errors() {
831        let k = |v: &[u8]| response_cache_control(v).unwrap_err().kind();
832        assert_eq!(k(b"max-age = 5"), ErrorKind::NotToken);
833        assert_eq!(k(b"max-age=\"5"), ErrorKind::UnterminatedQuote);
834        assert_eq!(k(b"=5"), ErrorKind::NotToken);
835        assert_eq!(k(b"a=b c"), ErrorKind::NotToken);
836        assert_eq!(k(b"a=\x01"), ErrorKind::Control);
837    }
838
839    #[test]
840    fn test_lines() {
841        let lines: [&[u8]; 2] = [b"public", b"max-age=5"];
842        let r = response_lines(lines).unwrap();
843        assert!(r.public && r.max_age == Some(5));
844        let bad: [&[u8]; 2] = [b"public", b"max-age=\"5"];
845        assert_eq!(response_lines(bad).unwrap_err().line(), 1);
846        let req: [&[u8]; 2] = [b"no-cache", b"max-stale"];
847        assert_eq!(request_lines(req).unwrap().max_stale, Some(None));
848    }
849
850    #[test]
851    fn test_writer_round_trip_and_refusals() {
852        let mut buf = [0u8; 128];
853        let mut out = crate::SliceSink::new(&mut buf);
854        Writer::new(&mut out)
855            .flag("public")
856            .and_then(|w| w.seconds("max-age", u32::MAX))
857            .and_then(|w| w.fields("private", &["X-A"]))
858            .map(|_| ())
859            .unwrap();
860        let r = response_cache_control(out.written()).unwrap();
861        assert!(r.public && r.private);
862        assert_eq!(r.max_age, Some(2_147_483_648));
863        let mut w = Writer::new(&mut out);
864        assert!(matches!(w.flag("a b"), Err(WriteError::NotToken)));
865        assert!(matches!(
866            w.fields("no-cache", &["a b"]),
867            Err(WriteError::NotToken)
868        ));
869        let mut small = [0u8; 3];
870        let mut out = crate::SliceSink::new(&mut small);
871        assert!(matches!(
872            Writer::new(&mut out).flag("public"),
873            Err(WriteError::Full)
874        ));
875        assert!(out.is_empty());
876    }
877}