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