Skip to main content

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
20# #[cfg(feature = "weak-hasher")]
21# {
22let etag3 = EntityTag::from_data(&[102, 111, 111]);
23assert_eq!(r#"W/"ea75LoNFQSGrbl9kB359ig""#, etag3.to_string());
24# }
25
26# #[cfg(feature = "strong-hasher")]
27# {
28let etag4 = EntityTag::from_data_strong(&[102, 111, 111]);
29assert_eq!(r#""BOC7OfMLGj/rifU2yTvhUFVILfdIZ0sA0m5adXd3Auk""#, etag4.to_string());
30# }
31
32# #[cfg(all(feature = "std", feature = "weak-hasher"))]
33# {
34let etag5 = EntityTag::from_file_meta(&std::fs::File::open("tests/data/P1060382.JPG").unwrap().metadata().unwrap());
35println!("{}", etag5.to_string()); // W/"wMRGbc5gr2L/keSkynH4KQ"
36# }
37
38# #[cfg(all(feature = "std", feature = "strong-hasher"))]
39# {
40let etag6 = EntityTag::from_file_meta_strong(&std::fs::File::open("tests/data/P1060382.JPG").unwrap().metadata().unwrap());
41println!("{}", etag6.to_string()); // "mmkAoOwMqROVvws3tlxJ9tIInflE3JGuOCn1REERmPo"
42# }
43```
44
45## No Std
46
47Disable the default features to compile this crate without std.
48
49```toml
50[dependencies.entity-tag]
51version = "*"
52default-features = false
53```
54
55## Hashers
56
57Generated ETag constructors are optional. Enable `weak-hasher` to use XXH3-128 weak ETag generation and `strong-hasher` to use BLAKE3-256 strong ETag generation.
58*/
59
60#![cfg_attr(not(feature = "std"), no_std)]
61
62extern crate alloc;
63
64mod entity_tag_error;
65
66use alloc::{borrow::Cow, string::String};
67use core::{
68    fmt::{self, Display, Formatter, Write},
69    str::FromStr,
70};
71#[cfg(all(feature = "std", any(feature = "weak-hasher", feature = "strong-hasher")))]
72use std::fs::Metadata;
73#[cfg(all(feature = "std", any(feature = "weak-hasher", feature = "strong-hasher")))]
74use std::time::UNIX_EPOCH;
75
76#[cfg(any(feature = "weak-hasher", feature = "strong-hasher"))]
77use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD};
78pub use entity_tag_error::EntityTagError;
79#[cfg(feature = "weak-hasher")]
80use xxhash_rust::xxh3::xxh3_128;
81
82#[cfg(feature = "strong-hasher")]
83#[inline]
84fn encode_blake3_256(data: &[u8]) -> String {
85    STANDARD_NO_PAD.encode(blake3::hash(data).as_bytes())
86}
87
88#[cfg(feature = "weak-hasher")]
89#[inline]
90fn encode_xxh3_128_data(data: &[u8]) -> String {
91    encode_xxh3_128(xxh3_128(data))
92}
93
94#[cfg(feature = "weak-hasher")]
95#[inline]
96fn encode_xxh3_128(hash: u128) -> String {
97    STANDARD_NO_PAD.encode(hash.to_be_bytes())
98}
99
100#[cfg(all(feature = "std", any(feature = "weak-hasher", feature = "strong-hasher")))]
101fn encode_file_meta<F>(metadata: &Metadata, encode: F) -> String
102where
103    F: Fn(&[u8]) -> String, {
104    let len_bytes = metadata.len().to_le_bytes();
105
106    if let Ok(modified_time) = metadata.modified() {
107        match modified_time.duration_since(UNIX_EPOCH) {
108            Ok(time) => {
109                let mut bytes = [0; 24];
110
111                bytes[..8].copy_from_slice(&len_bytes);
112                bytes[8..].copy_from_slice(&time.as_nanos().to_le_bytes());
113
114                encode(&bytes)
115            },
116            Err(err) => {
117                let mut bytes = [0; 25];
118
119                bytes[..8].copy_from_slice(&len_bytes);
120                bytes[8] = b'-';
121                bytes[9..].copy_from_slice(&err.duration().as_nanos().to_le_bytes());
122
123                encode(&bytes)
124            },
125        }
126    } else {
127        encode(&len_bytes)
128    }
129}
130
131/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3).
132#[derive(Debug, Clone, Eq, Hash, PartialEq)]
133pub struct EntityTag<'t> {
134    /// Whether to have a weakness indicator.
135    pub weak: bool,
136    /// The opaque tag content without the surrounding double quotes.
137    tag:      Cow<'t, str>,
138}
139
140impl<'t> EntityTag<'t> {
141    /// `ETag`
142    pub const HEADER_NAME: &'static str = "ETag";
143}
144
145impl<'t> EntityTag<'t> {
146    /// Construct a new EntityTag without checking.
147    ///
148    /// # Safety
149    /// The caller must ensure `tag` is a valid unquoted entity-tag value, because this function skips all validation.
150    #[inline]
151    pub const unsafe fn new_unchecked(weak: bool, tag: Cow<'t, str>) -> Self {
152        EntityTag {
153            weak,
154            tag,
155        }
156    }
157
158    /// Get the tag. The double quotes are not included.
159    #[inline]
160    pub const fn get_tag_cow(&self) -> &Cow<'t, str> {
161        &self.tag
162    }
163}
164
165impl<'t> EntityTag<'t> {
166    /// Construct a new EntityTag without checking.
167    ///
168    /// # Safety
169    /// The caller must ensure `tag` is a valid unquoted entity-tag value, because this function skips all validation.
170    #[inline]
171    pub unsafe fn with_string_unchecked<S: Into<String>>(weak: bool, tag: S) -> EntityTag<'static> {
172        EntityTag {
173            weak,
174            tag: Cow::from(tag.into()),
175        }
176    }
177
178    /// Construct a new EntityTag without checking.
179    ///
180    /// # Safety
181    /// The caller must ensure `tag` is a valid unquoted entity-tag value, because this function skips all validation.
182    #[inline]
183    pub unsafe fn with_str_unchecked<S: ?Sized + AsRef<str>>(weak: bool, tag: &'t S) -> Self {
184        EntityTag {
185            weak,
186            tag: Cow::from(tag.as_ref()),
187        }
188    }
189}
190
191impl<'t> EntityTag<'t> {
192    #[inline]
193    fn check_unquoted_tag(s: &str) -> Result<(), EntityTagError> {
194        if s.bytes().all(|c| c == b'\x21' || (b'\x23'..=b'\x7e').contains(&c) || c >= b'\x80') {
195            Ok(())
196        } else {
197            Err(EntityTagError::InvalidTag)
198        }
199    }
200
201    fn check_tag(s: &str) -> Result<bool, EntityTagError> {
202        let (s, quoted) =
203            if let Some(stripped) = s.strip_prefix('"') { (stripped, true) } else { (s, false) };
204
205        let s = if quoted {
206            if let Some(stripped) = s.strip_suffix('"') {
207                stripped
208            } else {
209                return Err(EntityTagError::MissingClosingDoubleQuote);
210            }
211        } else {
212            s
213        };
214
215        // now check the ETag characters
216
217        Self::check_unquoted_tag(s)?;
218
219        Ok(quoted)
220    }
221
222    /// Construct a new EntityTag from an opaque tag, with or without surrounding double quotes.
223    #[inline]
224    pub fn with_string<S: AsRef<str> + Into<String>>(
225        weak: bool,
226        tag: S,
227    ) -> Result<EntityTag<'static>, EntityTagError> {
228        let quoted = Self::check_tag(tag.as_ref())?;
229
230        let mut tag = tag.into();
231
232        if quoted {
233            tag.remove(tag.len() - 1);
234            tag.remove(0);
235        }
236
237        Ok(EntityTag {
238            weak,
239            tag: Cow::from(tag),
240        })
241    }
242
243    /// Construct a new EntityTag from an opaque tag, with or without surrounding double quotes.
244    #[inline]
245    pub fn with_str<S: ?Sized + AsRef<str>>(
246        weak: bool,
247        tag: &'t S,
248    ) -> Result<Self, EntityTagError> {
249        let tag = tag.as_ref();
250
251        let quoted = Self::check_tag(tag)?;
252
253        let tag = if quoted { &tag[1..(tag.len() - 1)] } else { tag };
254
255        Ok(EntityTag {
256            weak,
257            tag: Cow::from(tag),
258        })
259    }
260}
261
262impl<'t> EntityTag<'t> {
263    #[inline]
264    fn check_opaque_tag(s: &str) -> Result<(), EntityTagError> {
265        if let Some(s) = s.strip_prefix('"') {
266            if let Some(s) = s.strip_suffix('"') {
267                // now check the ETag characters
268                Self::check_unquoted_tag(s)
269            } else {
270                Err(EntityTagError::MissingClosingDoubleQuote)
271            }
272        } else {
273            Err(EntityTagError::MissingStartingDoubleQuote)
274        }
275    }
276
277    /// Parse and construct a new EntityTag from a full ETag header value.
278    pub fn from_string<S: AsRef<str> + Into<String>>(
279        etag: S,
280    ) -> Result<EntityTag<'static>, EntityTagError> {
281        let weak = {
282            let s = etag.as_ref();
283
284            let (weak, opaque_tag) = if let Some(opaque_tag) = s.strip_prefix("W/") {
285                (true, opaque_tag)
286            } else {
287                (false, s)
288            };
289
290            Self::check_opaque_tag(opaque_tag)?;
291
292            weak
293        };
294
295        let mut tag = etag.into();
296
297        tag.remove(tag.len() - 1);
298
299        if weak {
300            tag.replace_range(..3, "");
301        } else {
302            tag.remove(0);
303        }
304
305        Ok(EntityTag {
306            weak,
307            tag: Cow::from(tag),
308        })
309    }
310
311    /// Parse and construct a new EntityTag from a full ETag header value.
312    #[allow(clippy::should_implement_trait)]
313    pub fn from_str<S: ?Sized + AsRef<str>>(etag: &'t S) -> Result<Self, EntityTagError> {
314        let s = etag.as_ref();
315
316        let (weak, opaque_tag) = if let Some(opaque_tag) = s.strip_prefix("W/") {
317            (true, opaque_tag)
318        } else {
319            (false, s)
320        };
321
322        Self::check_opaque_tag(opaque_tag)?;
323
324        Ok(EntityTag {
325            weak,
326            tag: Cow::from(&opaque_tag[1..(opaque_tag.len() - 1)]),
327        })
328    }
329
330    #[cfg(feature = "weak-hasher")]
331    /// Construct a weak EntityTag from data with an XXH3-128 fingerprint.
332    #[inline]
333    pub fn from_data<S: ?Sized + AsRef<[u8]>>(data: &S) -> EntityTag<'static> {
334        let tag = encode_xxh3_128_data(data.as_ref());
335
336        EntityTag {
337            weak: true, tag: Cow::from(tag)
338        }
339    }
340
341    #[cfg(feature = "strong-hasher")]
342    /// Construct a strong EntityTag from data with a BLAKE3-256 digest.
343    #[inline]
344    pub fn from_data_strong<S: ?Sized + AsRef<[u8]>>(data: &S) -> EntityTag<'static> {
345        let tag = encode_blake3_256(data.as_ref());
346
347        EntityTag {
348            weak: false, tag: Cow::from(tag)
349        }
350    }
351
352    #[cfg(all(feature = "std", feature = "weak-hasher"))]
353    /// Construct a weak EntityTag from file metadata with an XXH3-128 fingerprint.
354    #[inline]
355    pub fn from_file_meta(metadata: &Metadata) -> EntityTag<'static> {
356        let tag = encode_file_meta(metadata, encode_xxh3_128_data);
357
358        EntityTag {
359            weak: true, tag: Cow::from(tag)
360        }
361    }
362
363    #[cfg(all(feature = "std", feature = "strong-hasher"))]
364    /// Construct a strong EntityTag from file metadata with a BLAKE3-256 digest.
365    #[inline]
366    pub fn from_file_meta_strong(metadata: &Metadata) -> EntityTag<'static> {
367        let tag = encode_file_meta(metadata, encode_blake3_256);
368
369        EntityTag {
370            weak: false, tag: Cow::from(tag)
371        }
372    }
373}
374
375impl<'t> EntityTag<'t> {
376    /// Get the tag. The double quotes are not included.
377    #[inline]
378    pub fn get_tag(&self) -> &str {
379        self.tag.as_ref()
380    }
381
382    /// Into the tag. The double quotes are not included.
383    #[inline]
384    pub fn into_tag(self) -> Cow<'t, str> {
385        self.tag
386    }
387
388    /// Extracts the owned data.
389    #[inline]
390    pub fn into_owned(self) -> EntityTag<'static> {
391        let tag = self.tag.into_owned();
392
393        EntityTag {
394            weak: self.weak, tag: Cow::from(tag)
395        }
396    }
397}
398
399impl<'t> EntityTag<'t> {
400    /// For strong comparison two entity-tags are equivalent if both are not weak and their opaque-tags match character-by-character.
401    #[inline]
402    pub fn strong_eq(&self, other: &EntityTag) -> bool {
403        !self.weak && !other.weak && self.tag == other.tag
404    }
405
406    /// 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".
407    #[inline]
408    pub fn weak_eq(&self, other: &EntityTag) -> bool {
409        self.tag == other.tag
410    }
411
412    /// The inverse of `strong_eq`.
413    #[inline]
414    pub fn strong_ne(&self, other: &EntityTag) -> bool {
415        !self.strong_eq(other)
416    }
417
418    /// The inverse of `weak_eq`.
419    #[inline]
420    pub fn weak_ne(&self, other: &EntityTag) -> bool {
421        !self.weak_eq(other)
422    }
423}
424
425impl FromStr for EntityTag<'static> {
426    type Err = EntityTagError;
427
428    #[inline]
429    fn from_str(s: &str) -> Result<Self, Self::Err> {
430        EntityTag::from_string(s)
431    }
432}
433
434impl<'t> Display for EntityTag<'t> {
435    #[inline]
436    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
437        if self.weak {
438            f.write_str("W/")?;
439        }
440
441        f.write_char('"')?;
442        f.write_str(self.tag.as_ref())?;
443        f.write_char('"')
444    }
445}