Skip to main content

rama_http_headers/common/
referrer_policy.rs

1use std::fmt;
2
3use rama_http_types::{HeaderName, HeaderValue};
4use rama_utils::collections::NonEmptySmallVec;
5use rama_utils::collections::smallvec::SmallVec;
6use rama_utils::macros::enums::enum_builder;
7use rama_utils::macros::generate_set_and_with;
8
9use crate::util::{self};
10use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
11
12/// `Referrer-Policy` header, part of
13/// [Referrer Policy](https://www.w3.org/TR/referrer-policy/#referrer-policy-header)
14///
15/// The `Referrer-Policy` HTTP header specifies the referrer
16/// policy that the user agent applies when determining what
17/// referrer information should be included with requests made,
18/// and with browsing contexts created from the context of the
19/// protected resource.
20///
21/// # Fallback chains
22///
23/// Per the [Referrer Policy spec § 3.2](https://www.w3.org/TR/referrer-policy/#parse-referrer-policy-from-header)
24/// the wire form is `1#policy-token` — a comma-separated list where the
25/// user agent walks RIGHT-TO-LEFT and picks the *last* token it
26/// recognises. This lets a server ship a modern policy with a fallback
27/// for older clients:
28///
29/// ```
30/// use rama_http_headers::ReferrerPolicy;
31///
32/// // Emits: `no-referrer, strict-origin-when-cross-origin`.
33/// // A pre-CSP3 browser falls back to `no-referrer`; a modern browser
34/// // picks `strict-origin-when-cross-origin`.
35/// let rp = ReferrerPolicy::NO_REFERRER
36///     .with_fallback(ReferrerPolicy::STRICT_ORIGIN_WHEN_CROSS_ORIGIN);
37/// ```
38///
39/// `with_fallback` appends; emitted order is preserved on the wire
40/// (oldest-known first → newest-known last).
41///
42/// # ABNF
43///
44/// ```text
45/// Referrer-Policy: 1#policy-token
46/// policy-token   = "no-referrer" / "no-referrer-when-downgrade"
47///                  / "same-origin" / "origin"
48///                  / "origin-when-cross-origin" / "unsafe-url"
49///                  / "strict-origin" / "strict-origin-when-cross-origin"
50/// ```
51///
52/// # Example values
53///
54/// * `no-referrer`
55/// * `no-referrer, strict-origin-when-cross-origin`
56///
57/// # Example
58///
59/// ```
60/// use rama_http_headers::ReferrerPolicy;
61///
62/// let rp = ReferrerPolicy::NO_REFERRER;
63/// ```
64#[derive(Clone, Debug, PartialEq, Eq, Hash)]
65pub struct ReferrerPolicy(NonEmptySmallVec<2, Policy>);
66
67enum_builder! {
68    /// One of the eight recognised `Referrer-Policy` tokens. The
69    /// auto-generated `Unknown(String)` variant is unreachable from
70    /// the [`HeaderDecode`] impl — it only stores recognised tokens —
71    /// but the macro produces it unconditionally.
72    @String
73    enum Policy {
74        NoReferrer => "no-referrer",
75        NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
76        SameOrigin => "same-origin",
77        Origin => "origin",
78        OriginWhenCrossOrigin => "origin-when-cross-origin",
79        UnsafeUrl => "unsafe-url",
80        StrictOrigin => "strict-origin",
81        StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin",
82    }
83}
84
85impl ReferrerPolicy {
86    /// `no-referrer`
87    pub const NO_REFERRER: Self = Self::single(Policy::NoReferrer);
88
89    /// `no-referrer-when-downgrade`
90    pub const NO_REFERRER_WHEN_DOWNGRADE: Self = Self::single(Policy::NoReferrerWhenDowngrade);
91
92    /// `same-origin`
93    pub const SAME_ORIGIN: Self = Self::single(Policy::SameOrigin);
94
95    /// `origin`
96    pub const ORIGIN: Self = Self::single(Policy::Origin);
97
98    /// `origin-when-cross-origin`
99    pub const ORIGIN_WHEN_CROSS_ORIGIN: Self = Self::single(Policy::OriginWhenCrossOrigin);
100
101    /// `unsafe-url`
102    pub const UNSAFE_URL: Self = Self::single(Policy::UnsafeUrl);
103
104    /// `strict-origin`
105    pub const STRICT_ORIGIN: Self = Self::single(Policy::StrictOrigin);
106
107    ///`strict-origin-when-cross-origin`
108    pub const STRICT_ORIGIN_WHEN_CROSS_ORIGIN: Self =
109        Self::single(Policy::StrictOriginWhenCrossOrigin);
110
111    const fn single(policy: Policy) -> Self {
112        // Tail is inline-allocated up to 2 elements before spilling to
113        // the heap, so single-token construction and the common
114        // 2-token fallback chain stay heap-free.
115        Self(NonEmptySmallVec {
116            head: policy,
117            tail: SmallVec::new_const(),
118        })
119    }
120
121    generate_set_and_with! {
122        /// Append a fallback policy.
123        ///
124        /// Multiple calls compound; emitted order is preserved on the
125        /// wire (oldest-known first → newest-known last). Browsers
126        /// walk right-to-left and select the last token they
127        /// recognise, so the *appended* policy is the one a modern
128        /// client will pick — call this on your older / pre-CSP3
129        /// baseline and pass the modern token you want.
130        ///
131        /// The argument's full policy list is appended in order (so
132        /// chaining preserves any fallback chain it already carried).
133        pub fn fallback(mut self, policy: Self) -> Self {
134            let (head, tail) = (policy.0.head, policy.0.tail);
135            self.0.tail.push(head);
136            self.0.tail.extend(tail);
137            self
138        }
139    }
140}
141
142impl TypedHeader for ReferrerPolicy {
143    fn name() -> &'static HeaderName {
144        &::rama_http_types::header::REFERRER_POLICY
145    }
146}
147
148impl HeaderDecode for ReferrerPolicy {
149    fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
150        // Preserve every recognised policy token in declared order so
151        // the browser still sees a fallback chain on re-encode. Walk
152        // all header values, all comma-separated tokens, and skip
153        // anything we don't recognise.
154        let mut all: SmallVec<[Policy; 2]> = SmallVec::new();
155        let mut any_value = false;
156        for value in values {
157            any_value = true;
158            let s = value.to_str().map_err(|_err| Error::invalid())?;
159            for raw in s.split(',') {
160                let trimmed = raw.trim();
161                if trimmed.is_empty() {
162                    continue;
163                }
164                // `strict_parse` is case-insensitive and rejects
165                // unknown tokens (including the dropped Gecko-only
166                // legacy aliases `never` / `default` / `always`).
167                // Unknown tokens are skipped so the chain falls
168                // through to the next recognised one.
169                if let Some(p) = Policy::strict_parse(trimmed) {
170                    all.push(p);
171                }
172            }
173        }
174        if !any_value {
175            return Err(Error::invalid());
176        }
177        let inner = NonEmptySmallVec::from_smallvec(all).ok_or_else(Error::invalid)?;
178        Ok(Self(inner))
179    }
180}
181
182impl HeaderEncode for ReferrerPolicy {
183    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
184        struct Adapter<'a>(&'a ReferrerPolicy);
185
186        impl fmt::Display for Adapter<'_> {
187            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188                for (i, p) in self.0.0.iter().enumerate() {
189                    if i > 0 {
190                        f.write_str(", ")?;
191                    }
192                    f.write_str(p.as_str())?;
193                }
194                Ok(())
195            }
196        }
197
198        values.extend(::std::iter::once(util::fmt(Adapter(self))));
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::super::{test_decode, test_encode};
205    use super::ReferrerPolicy;
206    use crate::TypedHeader;
207
208    #[test]
209    fn decode_single_token() {
210        assert_eq!(
211            test_decode::<ReferrerPolicy>(&["origin"]),
212            Some(ReferrerPolicy::ORIGIN),
213        );
214    }
215
216    #[test]
217    fn decode_skips_unknown_tokens_when_single_recognised() {
218        let chained = test_decode::<ReferrerPolicy>(&["origin, nope, nope, nope"]).unwrap();
219        assert_eq!(chained, ReferrerPolicy::ORIGIN);
220
221        let chained = test_decode::<ReferrerPolicy>(&["nope, origin, nope, nope"]).unwrap();
222        assert_eq!(chained, ReferrerPolicy::ORIGIN);
223
224        let chained = test_decode::<ReferrerPolicy>(&["nope, origin", "nope, nope"]).unwrap();
225        assert_eq!(chained, ReferrerPolicy::ORIGIN);
226
227        let chained = test_decode::<ReferrerPolicy>(&["nope", "origin", "nope, nope"]).unwrap();
228        assert_eq!(chained, ReferrerPolicy::ORIGIN);
229    }
230
231    #[test]
232    fn decode_preserves_multi_token_chain() {
233        let parsed =
234            test_decode::<ReferrerPolicy>(&["no-referrer, strict-origin-when-cross-origin"])
235                .expect("decode");
236        let expected = ReferrerPolicy::NO_REFERRER
237            .with_fallback(ReferrerPolicy::STRICT_ORIGIN_WHEN_CROSS_ORIGIN);
238        assert_eq!(parsed, expected);
239    }
240
241    #[test]
242    fn decode_unknown() {
243        assert_eq!(test_decode::<ReferrerPolicy>(&["nope"]), None);
244    }
245
246    #[test]
247    fn decode_rejects_dropped_legacy_aliases() {
248        // `never`, `default`, `always` were Gecko-only and have been
249        // dropped from the W3C spec — the parser must treat them as
250        // unknown so the chain falls through to the next valid token.
251        for legacy in ["never", "default", "always"] {
252            assert!(
253                test_decode::<ReferrerPolicy>(&[legacy]).is_none(),
254                "legacy alias `{legacy}` should no longer parse",
255            );
256        }
257        // …but a chain containing one alongside a recognised token
258        // still works (the alias is just dropped):
259        let chained = test_decode::<ReferrerPolicy>(&["never, no-referrer"]).expect("decode");
260        assert_eq!(chained, ReferrerPolicy::NO_REFERRER);
261    }
262
263    #[test]
264    fn decode_empty_returns_error() {
265        assert_eq!(test_decode::<ReferrerPolicy>(&[] as &[&str]), None);
266    }
267
268    #[test]
269    fn matches_via_equality() {
270        // Pattern matching against the const constants is no longer
271        // possible (the inner SmallVec is non-structural), but
272        // `assert_eq!` works the same as before for the common usage
273        // pattern.
274        let rp = ReferrerPolicy::ORIGIN;
275        assert_eq!(rp, ReferrerPolicy::ORIGIN);
276        assert_ne!(rp, ReferrerPolicy::NO_REFERRER);
277    }
278
279    #[test]
280    fn encode_single_token() {
281        let map = test_encode(ReferrerPolicy::NO_REFERRER);
282        let raw = map
283            .get(super::ReferrerPolicy::name())
284            .expect("header set")
285            .to_str()
286            .unwrap()
287            .to_owned();
288        assert_eq!(raw, "no-referrer");
289    }
290
291    #[test]
292    fn encode_fallback_chain_canonical_order() {
293        let rp = ReferrerPolicy::NO_REFERRER
294            .with_fallback(ReferrerPolicy::STRICT_ORIGIN_WHEN_CROSS_ORIGIN);
295        let map = test_encode(rp);
296        let raw = map
297            .get(super::ReferrerPolicy::name())
298            .expect("header set")
299            .to_str()
300            .unwrap()
301            .to_owned();
302        assert_eq!(raw, "no-referrer, strict-origin-when-cross-origin");
303    }
304
305    #[test]
306    fn fallback_chains_compound() {
307        let rp = ReferrerPolicy::NO_REFERRER
308            .with_fallback(ReferrerPolicy::ORIGIN)
309            .with_fallback(ReferrerPolicy::STRICT_ORIGIN_WHEN_CROSS_ORIGIN);
310        let map = test_encode(rp);
311        let raw = map
312            .get(super::ReferrerPolicy::name())
313            .expect("header set")
314            .to_str()
315            .unwrap()
316            .to_owned();
317        assert_eq!(raw, "no-referrer, origin, strict-origin-when-cross-origin");
318    }
319
320    #[test]
321    fn fallback_preserves_existing_chain() {
322        // The argument can itself be a multi-token chain — append it
323        // wholesale rather than just the head.
324        let inner = ReferrerPolicy::ORIGIN.with_fallback(ReferrerPolicy::STRICT_ORIGIN);
325        let outer = ReferrerPolicy::NO_REFERRER.with_fallback(inner);
326        let map = test_encode(outer);
327        let raw = map
328            .get(super::ReferrerPolicy::name())
329            .expect("header set")
330            .to_str()
331            .unwrap()
332            .to_owned();
333        assert_eq!(raw, "no-referrer, origin, strict-origin");
334    }
335
336    #[test]
337    fn full_round_trip_through_header_map() {
338        let original = ReferrerPolicy::NO_REFERRER
339            .with_fallback(ReferrerPolicy::STRICT_ORIGIN_WHEN_CROSS_ORIGIN);
340        let map = test_encode(original.clone());
341        let raw = map
342            .get(super::ReferrerPolicy::name())
343            .expect("header set")
344            .to_str()
345            .unwrap()
346            .to_owned();
347        let parsed = test_decode::<ReferrerPolicy>(&[raw.as_str()]).expect("re-decode");
348        assert_eq!(parsed, original);
349    }
350}