rama_http_headers/util/
entity.rs1use std::{fmt, str::FromStr};
2
3use rama_core::error::BoxError;
4use rama_core::telemetry::tracing;
5use rama_http_types::HeaderValue;
6use rama_utils::collections::NonEmptyVec;
7
8use super::IterExt;
9use crate::{
10 Error,
11 util::{
12 FlatCsvSeparator, try_decode_flat_csv_header_values_as_non_empty_vec,
13 try_encode_non_empty_vec_of_bytes_as_flat_csv_header_value,
14 },
15};
16
17#[derive(Clone, Eq, PartialEq)]
48pub(crate) struct EntityTag<T = HeaderValue>(T);
49
50#[derive(Clone, Debug, PartialEq)]
51pub(crate) enum EntityTagRange {
52 Any,
53 Tags(NonEmptyVec<EntityTag>),
54}
55
56impl<T: AsRef<[u8]>> EntityTag<T> {
59 pub(crate) fn tag(&self) -> &[u8] {
61 let bytes = self.0.as_ref();
62 let end = bytes.len() - 1;
63 if bytes[0] == b'W' {
64 &bytes[3..end]
66 } else {
67 &bytes[1..end]
69 }
70 }
71
72 pub(crate) fn is_weak(&self) -> bool {
74 self.0.as_ref()[0] == b'W'
75 }
76
77 pub(crate) fn strong_eq<R>(&self, other: &EntityTag<R>) -> bool
80 where
81 R: AsRef<[u8]>,
82 {
83 !self.is_weak() && !other.is_weak() && self.tag() == other.tag()
84 }
85
86 pub(crate) fn weak_eq<R>(&self, other: &EntityTag<R>) -> bool
90 where
91 R: AsRef<[u8]>,
92 {
93 self.tag() == other.tag()
94 }
95
96 #[cfg(test)]
98 pub(crate) fn strong_ne(&self, other: &EntityTag) -> bool {
99 !self.strong_eq(other)
100 }
101
102 #[cfg(test)]
104 pub(crate) fn weak_ne(&self, other: &EntityTag) -> bool {
105 !self.weak_eq(other)
106 }
107
108 pub(crate) fn parse(src: T) -> Option<Self> {
109 let slice = src.as_ref();
110 let length = slice.len();
111
112 if length < 2 || slice[length - 1] != b'"' {
114 return None;
115 }
116
117 let start = match slice[0] {
118 b'"' => 1,
120 b'W' if length >= 4 && slice[1] == b'/' && slice[2] == b'"' => 3,
122 _ => return None,
123 };
124
125 if check_slice_validity(&slice[start..length - 1]) {
126 Some(Self(src))
127 } else {
128 None
129 }
130 }
131}
132
133impl EntityTag {
134 #[cfg(test)]
159 pub(crate) fn from_static(bytes: &'static str) -> Self {
160 let val = HeaderValue::from_static(bytes);
161 match Self::from_val(&val) {
162 Some(tag) => tag,
163 None => {
164 panic!("invalid static string for EntityTag: {bytes:?}");
165 }
166 }
167 }
168
169 pub(crate) fn from_owned(val: HeaderValue) -> Option<Self> {
170 EntityTag::parse(val.as_bytes())?;
171 Some(Self(val))
172 }
173
174 pub(crate) fn from_val(val: &HeaderValue) -> Option<Self> {
175 EntityTag::parse(val.as_bytes()).map(|_entity| Self(val.clone()))
176 }
177}
178
179impl<T: AsRef<[u8]>> AsRef<[u8]> for EntityTag<T> {
180 #[inline(always)]
181 fn as_ref(&self) -> &[u8] {
182 self.0.as_ref()
183 }
184}
185
186impl<T: fmt::Debug> fmt::Debug for EntityTag<T> {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 self.0.fmt(f)
189 }
190}
191
192impl super::TryFromValues for EntityTag {
193 fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
194 where
195 I: Iterator<Item = &'i HeaderValue>,
196 {
197 values
198 .just_one()
199 .and_then(Self::from_val)
200 .ok_or_else(Error::invalid)
201 }
202}
203
204impl<T: FromStr> FromStr for EntityTag<T> {
205 type Err = T::Err;
206
207 fn from_str(s: &str) -> Result<Self, Self::Err> {
208 Ok(Self(s.parse()?))
209 }
210}
211
212impl From<EntityTag> for HeaderValue {
213 fn from(tag: EntityTag) -> Self {
214 tag.0
215 }
216}
217
218impl<'a> From<&'a EntityTag> for HeaderValue {
219 fn from(tag: &'a EntityTag) -> Self {
220 tag.0.clone()
221 }
222}
223
224fn check_slice_validity(slice: &[u8]) -> bool {
229 slice.iter().all(|&c| {
230 debug_assert!(
236 (b'\x21'..=b'\x7e').contains(&c) | (c >= b'\x80'),
237 "EntityTag expects HeaderValue to have check for control characters"
238 );
239 c != b'"'
240 })
241}
242
243impl EntityTagRange {
246 pub(crate) fn matches_strong(&self, entity: &EntityTag) -> bool {
247 self.matches_if(entity, |a, b| a.strong_eq(b))
248 }
249
250 pub(crate) fn matches_weak(&self, entity: &EntityTag) -> bool {
251 self.matches_if(entity, |a, b| a.weak_eq(b))
252 }
253
254 fn matches_if<F>(&self, entity: &EntityTag, func: F) -> bool
255 where
256 F: Fn(&EntityTag, &EntityTag) -> bool,
257 {
258 match *self {
259 Self::Any => true,
260 Self::Tags(ref tags) => tags.iter().any(|tag| func(tag, entity)),
261 }
262 }
263}
264
265impl super::TryFromValues for EntityTagRange {
266 fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
267 where
268 I: Iterator<Item = &'i HeaderValue>,
269 {
270 match try_decode_flat_csv_header_values_as_non_empty_vec::<EntityTag>(
271 values,
272 FlatCsvSeparator::Comma,
273 ) {
274 Ok(values) => {
275 if values.len() == 1 && values.head.0.as_bytes().eq_ignore_ascii_case(b"*") {
276 Ok(Self::Any)
277 } else {
278 Ok(Self::Tags(values))
279 }
280 }
281 Err(err) => {
282 tracing::trace!("invalid entity tags: {err}");
283 Err(crate::Error::invalid())
284 }
285 }
286 }
287}
288
289impl TryFrom<&EntityTagRange> for HeaderValue {
290 type Error = BoxError;
291
292 fn try_from(tag: &EntityTagRange) -> Result<Self, Self::Error> {
293 match tag {
294 EntityTagRange::Any => Ok(Self::from_static("*")),
295 EntityTagRange::Tags(tags) => {
296 try_encode_non_empty_vec_of_bytes_as_flat_csv_header_value(
297 tags,
298 FlatCsvSeparator::Comma,
299 )
300 }
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn parse(slice: &[u8]) -> Option<EntityTag> {
310 let val = HeaderValue::from_bytes(slice).ok()?;
311 EntityTag::from_val(&val)
312 }
313
314 #[test]
315 fn test_etag_parse_success() {
316 let tag = parse(b"\"foobar\"").unwrap();
318 assert!(!tag.is_weak());
319 assert_eq!(tag.tag(), b"foobar");
320
321 let weak = parse(b"W/\"weaktag\"").unwrap();
322 assert!(weak.is_weak());
323 assert_eq!(weak.tag(), b"weaktag");
324 }
325
326 #[test]
327 fn test_etag_parse_failures() {
328 macro_rules! fails {
330 ($slice:expr) => {
331 assert_eq!(parse($slice), None);
332 };
333 }
334
335 fails!(b"no-dquote");
336 fails!(b"w/\"the-first-w-is-case sensitive\"");
337 fails!(b"W/\"");
338 fails!(b"");
339 fails!(b"\"unmatched-dquotes1");
340 fails!(b"unmatched-dquotes2\"");
341 fails!(b"\"inner\"quotes\"");
342 }
343
344 #[test]
356 fn test_cmp() {
357 let mut etag1 = EntityTag::from_static("W/\"1\"");
364 let mut etag2 = etag1.clone();
365 assert!(!etag1.strong_eq(&etag2));
366 assert!(etag1.weak_eq(&etag2));
367 assert!(etag1.strong_ne(&etag2));
368 assert!(!etag1.weak_ne(&etag2));
369
370 etag2 = EntityTag::from_static("W/\"2\"");
371 assert!(!etag1.strong_eq(&etag2));
372 assert!(!etag1.weak_eq(&etag2));
373 assert!(etag1.strong_ne(&etag2));
374 assert!(etag1.weak_ne(&etag2));
375
376 etag2 = EntityTag::from_static("\"1\"");
377 assert!(!etag1.strong_eq(&etag2));
378 assert!(etag1.weak_eq(&etag2));
379 assert!(etag1.strong_ne(&etag2));
380 assert!(!etag1.weak_ne(&etag2));
381
382 etag1 = EntityTag::from_static("\"1\"");
383 assert!(etag1.strong_eq(&etag2));
384 assert!(etag1.weak_eq(&etag2));
385 assert!(!etag1.strong_ne(&etag2));
386 assert!(!etag1.weak_ne(&etag2));
387 }
388}