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