1#![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#[derive(Debug, Clone, Eq, PartialEq)]
63pub struct EntityTag<'t> {
64 pub weak: bool,
66 tag: Cow<'t, str>,
68}
69
70impl<'t> EntityTag<'t> {
71 pub const HEADER_NAME: &'static str = "ETag";
73}
74
75impl<'t> EntityTag<'t> {
76 #[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 #[inline]
88 pub const fn get_tag_cow(&self) -> &Cow<'t, str> {
89 &self.tag
90 }
91}
92
93impl<'t> EntityTag<'t> {
94 #[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 #[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 Self::check_unquoted_tag(s)?;
142
143 Ok(quoted)
144 }
145
146 #[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 #[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 Self::check_unquoted_tag(s)
193 } else {
194 Err(EntityTagError::MissingClosingDoubleQuote)
195 }
196 } else {
197 Err(EntityTagError::MissingStartingDoubleQuote)
198 }
199 }
200
201 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 #[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 #[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 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 #[inline]
300 pub fn get_tag(&'t self) -> &'t str {
301 self.tag.as_ref()
302 }
303
304 #[inline]
306 pub fn into_tag(self) -> Cow<'t, str> {
307 self.tag
308 }
309
310 #[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 #[inline]
324 pub fn strong_eq(&self, other: &EntityTag) -> bool {
325 !self.weak && !other.weak && self.tag == other.tag
326 }
327
328 #[inline]
330 pub fn weak_eq(&self, other: &EntityTag) -> bool {
331 self.tag == other.tag
332 }
333
334 #[inline]
336 pub fn strong_ne(&self, other: &EntityTag) -> bool {
337 !self.strong_eq(other)
338 }
339
340 #[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}