rama_http_headers/common/
referrer_policy.rs1use 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
65pub struct ReferrerPolicy(NonEmptySmallVec<2, Policy>);
66
67enum_builder! {
68 @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 pub const NO_REFERRER: Self = Self::single(Policy::NoReferrer);
88
89 pub const NO_REFERRER_WHEN_DOWNGRADE: Self = Self::single(Policy::NoReferrerWhenDowngrade);
91
92 pub const SAME_ORIGIN: Self = Self::single(Policy::SameOrigin);
94
95 pub const ORIGIN: Self = Self::single(Policy::Origin);
97
98 pub const ORIGIN_WHEN_CROSS_ORIGIN: Self = Self::single(Policy::OriginWhenCrossOrigin);
100
101 pub const UNSAFE_URL: Self = Self::single(Policy::UnsafeUrl);
103
104 pub const STRICT_ORIGIN: Self = Self::single(Policy::StrictOrigin);
106
107 pub const STRICT_ORIGIN_WHEN_CROSS_ORIGIN: Self =
109 Self::single(Policy::StrictOriginWhenCrossOrigin);
110
111 const fn single(policy: Policy) -> Self {
112 Self(NonEmptySmallVec {
116 head: policy,
117 tail: SmallVec::new_const(),
118 })
119 }
120
121 generate_set_and_with! {
122 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 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 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 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 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 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 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}