entity_tag/
lib.rs

1/*!
2# Entity Tag
3
4This crate provides a `EntityTag` structure and functions to deal with the ETag header field of HTTP.
5
6## Examples
7
8```rust
9use entity_tag::EntityTag;
10
11let etag1 = EntityTag::with_str(true, "foo").unwrap();
12let etag2 = EntityTag::from_str("\"foo\"").unwrap();
13
14assert_eq!(true, etag1.weak);
15assert_eq!(false, etag2.weak);
16
17assert!(etag1.weak_eq(&etag2));
18assert!(etag1.strong_ne(&etag2));
19
20let etag3 = EntityTag::from_data(&[102, 111, 111]);
21assert_eq!("\"972Sf7Z4eu8\"", etag3.to_string());
22
23# #[cfg(feature = "std")]
24# {
25let etag4 = EntityTag::from_file_meta(&std::fs::File::open("tests/data/P1060382.JPG").unwrap().metadata().unwrap());
26println!("{}", etag4) // W/"HRScBWR0Mf4"
27# }
28```
29
30## No Std
31
32Disable the default features to compile this crate without std.
33
34```toml
35[dependencies.entity-tag]
36version = "*"
37default-features = false
38```
39*/
40
41#![cfg_attr(not(feature = "std"), no_std)]
42
43extern crate alloc;
44
45mod entity_tag_error;
46
47use alloc::{borrow::Cow, string::String};
48use core::{
49    fmt::{self, Display, Formatter, Write},
50    hash::Hasher,
51};
52#[cfg(feature = "std")]
53use std::fs::Metadata;
54#[cfg(feature = "std")]
55use std::time::UNIX_EPOCH;
56
57use base64::Engine;
58pub use entity_tag_error::EntityTagError;
59use highway::HighwayHasher;
60
61/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3).
62#[derive(Debug, Clone, Eq, PartialEq)]
63pub struct EntityTag<'t> {
64    /// Whether to have a weakness indicator.
65    pub weak: bool,
66    /// *etagc
67    tag:      Cow<'t, str>,
68}
69
70impl<'t> EntityTag<'t> {
71    /// `ETag`
72    pub const HEADER_NAME: &'static str = "ETag";
73}
74
75impl<'t> EntityTag<'t> {
76    /// Construct a new EntityTag without checking.
77    #[allow(clippy::missing_safety_doc)]
78    #[inline]
79    pub const unsafe fn new_unchecked(weak: bool, tag: Cow<'t, str>) -> Self {
80        EntityTag {
81            weak,
82            tag,
83        }
84    }
85
86    /// Get the tag. The double quotes are not included.
87    #[inline]
88    pub const fn get_tag_cow(&self) -> &Cow<'t, str> {
89        &self.tag
90    }
91}
92
93impl<'t> EntityTag<'t> {
94    /// Construct a new EntityTag without checking.
95    #[allow(clippy::missing_safety_doc)]
96    #[inline]
97    pub unsafe fn with_string_unchecked<S: Into<String>>(weak: bool, tag: S) -> EntityTag<'static> {
98        EntityTag {
99            weak,
100            tag: Cow::from(tag.into()),
101        }
102    }
103
104    /// Construct a new EntityTag without checking.
105    #[allow(clippy::missing_safety_doc)]
106    #[inline]
107    pub unsafe fn with_str_unchecked<S: ?Sized + AsRef<str>>(weak: bool, tag: &'t S) -> Self {
108        EntityTag {
109            weak,
110            tag: Cow::from(tag.as_ref()),
111        }
112    }
113}
114
115impl<'t> EntityTag<'t> {
116    #[inline]
117    fn check_unquoted_tag(s: &str) -> Result<(), EntityTagError> {
118        if s.bytes().all(|c| c == b'\x21' || (b'\x23'..=b'\x7e').contains(&c) || c >= b'\x80') {
119            Ok(())
120        } else {
121            Err(EntityTagError::InvalidTag)
122        }
123    }
124
125    fn check_tag(s: &str) -> Result<bool, EntityTagError> {
126        let (s, quoted) =
127            if let Some(stripped) = s.strip_prefix('"') { (stripped, true) } else { (s, false) };
128
129        let s = if quoted {
130            if let Some(stripped) = s.strip_suffix('"') {
131                stripped
132            } else {
133                return Err(EntityTagError::MissingClosingDoubleQuote);
134            }
135        } else {
136            s
137        };
138
139        // now check the ETag characters
140
141        Self::check_unquoted_tag(s)?;
142
143        Ok(quoted)
144    }
145
146    /// Construct a new EntityTag.
147    #[inline]
148    pub fn with_string<S: AsRef<str> + Into<String>>(
149        weak: bool,
150        tag: S,
151    ) -> Result<EntityTag<'static>, EntityTagError> {
152        let quoted = Self::check_tag(tag.as_ref())?;
153
154        let mut tag = tag.into();
155
156        if quoted {
157            tag.remove(tag.len() - 1);
158            tag.remove(0);
159        }
160
161        Ok(EntityTag {
162            weak,
163            tag: Cow::from(tag),
164        })
165    }
166
167    /// Construct a new EntityTag.
168    #[inline]
169    pub fn with_str<S: ?Sized + AsRef<str>>(
170        weak: bool,
171        tag: &'t S,
172    ) -> Result<Self, EntityTagError> {
173        let tag = tag.as_ref();
174
175        let quoted = Self::check_tag(tag)?;
176
177        let tag = if quoted { &tag[1..(tag.len() - 1)] } else { tag };
178
179        Ok(EntityTag {
180            weak,
181            tag: Cow::from(tag),
182        })
183    }
184}
185
186impl<'t> EntityTag<'t> {
187    #[inline]
188    fn check_opaque_tag(s: &str) -> Result<(), EntityTagError> {
189        if let Some(s) = s.strip_prefix('"') {
190            if let Some(s) = s.strip_suffix('"') {
191                // now check the ETag characters
192                Self::check_unquoted_tag(s)
193            } else {
194                Err(EntityTagError::MissingClosingDoubleQuote)
195            }
196        } else {
197            Err(EntityTagError::MissingStartingDoubleQuote)
198        }
199    }
200
201    /// Parse and construct a new EntityTag from a `String`.
202    pub fn from_string<S: AsRef<str> + Into<String>>(
203        etag: S,
204    ) -> Result<EntityTag<'static>, EntityTagError> {
205        let weak = {
206            let s = etag.as_ref();
207
208            let (weak, opaque_tag) = if let Some(opaque_tag) = s.strip_prefix("W/") {
209                (true, opaque_tag)
210            } else {
211                (false, s)
212            };
213
214            Self::check_opaque_tag(opaque_tag)?;
215
216            weak
217        };
218
219        let mut tag = etag.into();
220
221        tag.remove(tag.len() - 1);
222
223        if weak {
224            unsafe {
225                tag.as_mut_vec().drain(0..3);
226            }
227        } else {
228            tag.remove(0);
229        }
230
231        Ok(EntityTag {
232            weak,
233            tag: Cow::from(tag),
234        })
235    }
236
237    /// Parse and construct a new EntityTag from a `str`.
238    #[allow(clippy::should_implement_trait)]
239    pub fn from_str<S: ?Sized + AsRef<str>>(etag: &'t S) -> Result<Self, EntityTagError> {
240        let s = etag.as_ref();
241
242        let (weak, opaque_tag) = if let Some(opaque_tag) = s.strip_prefix("W/") {
243            (true, opaque_tag)
244        } else {
245            (false, s)
246        };
247
248        Self::check_opaque_tag(opaque_tag)?;
249
250        Ok(EntityTag {
251            weak,
252            tag: Cow::from(&opaque_tag[1..(opaque_tag.len() - 1)]),
253        })
254    }
255
256    /// Construct a strong EntityTag.
257    #[inline]
258    pub fn from_data<S: ?Sized + AsRef<[u8]>>(data: &S) -> EntityTag<'static> {
259        let mut hasher = HighwayHasher::default();
260        hasher.write(data.as_ref());
261
262        let tag =
263            base64::engine::general_purpose::STANDARD_NO_PAD.encode(hasher.finish().to_le_bytes());
264
265        EntityTag {
266            weak: false, tag: Cow::from(tag)
267        }
268    }
269
270    #[cfg(feature = "std")]
271    /// Construct a weak EntityTag.
272    pub fn from_file_meta(metadata: &Metadata) -> EntityTag<'static> {
273        let mut hasher = HighwayHasher::default();
274
275        hasher.write(&metadata.len().to_le_bytes());
276
277        if let Ok(modified_time) = metadata.modified() {
278            if let Ok(time) = modified_time.duration_since(UNIX_EPOCH) {
279                hasher.write(&time.as_nanos().to_le_bytes());
280            } else {
281                hasher.write(b"-");
282
283                let time = UNIX_EPOCH.duration_since(modified_time).unwrap();
284                hasher.write(&time.as_nanos().to_le_bytes());
285            }
286        }
287
288        let tag =
289            base64::engine::general_purpose::STANDARD_NO_PAD.encode(hasher.finish().to_le_bytes());
290
291        EntityTag {
292            weak: true, tag: Cow::from(tag)
293        }
294    }
295}
296
297impl<'t> EntityTag<'t> {
298    /// Get the tag. The double quotes are not included.
299    #[inline]
300    pub fn get_tag(&'t self) -> &'t str {
301        self.tag.as_ref()
302    }
303
304    /// Into the tag. The double quotes are not included.
305    #[inline]
306    pub fn into_tag(self) -> Cow<'t, str> {
307        self.tag
308    }
309
310    /// Extracts the owned data.
311    #[inline]
312    pub fn into_owned(self) -> EntityTag<'static> {
313        let tag = self.tag.into_owned();
314
315        EntityTag {
316            weak: self.weak, tag: Cow::from(tag)
317        }
318    }
319}
320
321impl<'t> EntityTag<'t> {
322    /// For strong comparison two entity-tags are equivalent if both are not weak and their opaque-tags match character-by-character.
323    #[inline]
324    pub fn strong_eq(&self, other: &EntityTag) -> bool {
325        !self.weak && !other.weak && self.tag == other.tag
326    }
327
328    /// For weak comparison two entity-tags are equivalent if their opaque-tags match character-by-character, regardless of either or both being tagged as "weak".
329    #[inline]
330    pub fn weak_eq(&self, other: &EntityTag) -> bool {
331        self.tag == other.tag
332    }
333
334    /// The inverse of `strong_eq`.
335    #[inline]
336    pub fn strong_ne(&self, other: &EntityTag) -> bool {
337        !self.strong_eq(other)
338    }
339
340    /// The inverse of `weak_eq`.
341    #[inline]
342    pub fn weak_ne(&self, other: &EntityTag) -> bool {
343        !self.weak_eq(other)
344    }
345}
346
347impl<'t> Display for EntityTag<'t> {
348    #[inline]
349    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
350        if self.weak {
351            f.write_str("W/")?;
352        }
353
354        f.write_char('"')?;
355        f.write_str(self.tag.as_ref())?;
356        f.write_char('"')
357    }
358}