1#![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#[derive(Debug, Clone, Eq, Hash, PartialEq)]
133pub struct EntityTag<'t> {
134 pub weak: bool,
136 tag: Cow<'t, str>,
138}
139
140impl<'t> EntityTag<'t> {
141 pub const HEADER_NAME: &'static str = "ETag";
143}
144
145impl<'t> EntityTag<'t> {
146 #[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 #[inline]
160 pub const fn get_tag_cow(&self) -> &Cow<'t, str> {
161 &self.tag
162 }
163}
164
165impl<'t> EntityTag<'t> {
166 #[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 #[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 Self::check_unquoted_tag(s)?;
218
219 Ok(quoted)
220 }
221
222 #[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 #[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 Self::check_unquoted_tag(s)
269 } else {
270 Err(EntityTagError::MissingClosingDoubleQuote)
271 }
272 } else {
273 Err(EntityTagError::MissingStartingDoubleQuote)
274 }
275 }
276
277 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 #[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 #[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 #[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 #[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 #[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 #[inline]
378 pub fn get_tag(&self) -> &str {
379 self.tag.as_ref()
380 }
381
382 #[inline]
384 pub fn into_tag(self) -> Cow<'t, str> {
385 self.tag
386 }
387
388 #[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 #[inline]
402 pub fn strong_eq(&self, other: &EntityTag) -> bool {
403 !self.weak && !other.weak && self.tag == other.tag
404 }
405
406 #[inline]
408 pub fn weak_eq(&self, other: &EntityTag) -> bool {
409 self.tag == other.tag
410 }
411
412 #[inline]
414 pub fn strong_ne(&self, other: &EntityTag) -> bool {
415 !self.strong_eq(other)
416 }
417
418 #[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}