#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
mod entity_tag_error;
use alloc::{borrow::Cow, string::String};
use core::{
fmt::{self, Display, Formatter, Write},
hash::Hasher,
};
#[cfg(feature = "std")]
use std::fs::Metadata;
#[cfg(feature = "std")]
use std::time::UNIX_EPOCH;
use base64::Engine;
pub use entity_tag_error::EntityTagError;
use highway::HighwayHasher;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct EntityTag<'t> {
pub weak: bool,
tag: Cow<'t, str>,
}
impl<'t> EntityTag<'t> {
pub const HEADER_NAME: &'static str = "ETag";
}
impl<'t> EntityTag<'t> {
#[allow(clippy::missing_safety_doc)]
#[inline]
pub const unsafe fn new_unchecked(weak: bool, tag: Cow<'t, str>) -> Self {
EntityTag {
weak,
tag,
}
}
#[inline]
pub const fn get_tag_cow(&self) -> &Cow<'t, str> {
&self.tag
}
}
impl<'t> EntityTag<'t> {
#[allow(clippy::missing_safety_doc)]
#[inline]
pub unsafe fn with_string_unchecked<S: Into<String>>(weak: bool, tag: S) -> EntityTag<'static> {
EntityTag {
weak,
tag: Cow::from(tag.into()),
}
}
#[allow(clippy::missing_safety_doc)]
#[inline]
pub unsafe fn with_str_unchecked<S: ?Sized + AsRef<str>>(weak: bool, tag: &'t S) -> Self {
EntityTag {
weak,
tag: Cow::from(tag.as_ref()),
}
}
}
impl<'t> EntityTag<'t> {
#[inline]
fn check_unquoted_tag(s: &str) -> Result<(), EntityTagError> {
if s.bytes().all(|c| c == b'\x21' || (b'\x23'..=b'\x7e').contains(&c) || c >= b'\x80') {
Ok(())
} else {
Err(EntityTagError::InvalidTag)
}
}
fn check_tag(s: &str) -> Result<bool, EntityTagError> {
let (s, quoted) =
if let Some(stripped) = s.strip_prefix('"') { (stripped, true) } else { (s, false) };
let s = if quoted {
if let Some(stripped) = s.strip_suffix('"') {
stripped
} else {
return Err(EntityTagError::MissingClosingDoubleQuote);
}
} else {
s
};
Self::check_unquoted_tag(s)?;
Ok(quoted)
}
#[inline]
pub fn with_string<S: AsRef<str> + Into<String>>(
weak: bool,
tag: S,
) -> Result<EntityTag<'static>, EntityTagError> {
let quoted = Self::check_tag(tag.as_ref())?;
let mut tag = tag.into();
if quoted {
tag.remove(tag.len() - 1);
tag.remove(0);
}
Ok(EntityTag {
weak,
tag: Cow::from(tag),
})
}
#[inline]
pub fn with_str<S: ?Sized + AsRef<str>>(
weak: bool,
tag: &'t S,
) -> Result<Self, EntityTagError> {
let tag = tag.as_ref();
let quoted = Self::check_tag(tag)?;
let tag = if quoted { &tag[1..(tag.len() - 1)] } else { tag };
Ok(EntityTag {
weak,
tag: Cow::from(tag),
})
}
}
impl<'t> EntityTag<'t> {
#[inline]
fn check_opaque_tag(s: &str) -> Result<(), EntityTagError> {
if let Some(s) = s.strip_prefix('"') {
if let Some(s) = s.strip_suffix('"') {
Self::check_unquoted_tag(s)
} else {
Err(EntityTagError::MissingClosingDoubleQuote)
}
} else {
Err(EntityTagError::MissingStartingDoubleQuote)
}
}
pub fn from_string<S: AsRef<str> + Into<String>>(
etag: S,
) -> Result<EntityTag<'static>, EntityTagError> {
let weak = {
let s = etag.as_ref();
let (weak, opaque_tag) = if let Some(opaque_tag) = s.strip_prefix("W/") {
(true, opaque_tag)
} else {
(false, s)
};
Self::check_opaque_tag(opaque_tag)?;
weak
};
let mut tag = etag.into();
tag.remove(tag.len() - 1);
if weak {
unsafe {
tag.as_mut_vec().drain(0..3);
}
} else {
tag.remove(0);
}
Ok(EntityTag {
weak,
tag: Cow::from(tag),
})
}
#[allow(clippy::should_implement_trait)]
pub fn from_str<S: ?Sized + AsRef<str>>(etag: &'t S) -> Result<Self, EntityTagError> {
let s = etag.as_ref();
let (weak, opaque_tag) = if let Some(opaque_tag) = s.strip_prefix("W/") {
(true, opaque_tag)
} else {
(false, s)
};
Self::check_opaque_tag(opaque_tag)?;
Ok(EntityTag {
weak,
tag: Cow::from(&opaque_tag[1..(opaque_tag.len() - 1)]),
})
}
#[inline]
pub fn from_data<S: ?Sized + AsRef<[u8]>>(data: &S) -> EntityTag<'static> {
let mut hasher = HighwayHasher::default();
hasher.write(data.as_ref());
let tag =
base64::engine::general_purpose::STANDARD_NO_PAD.encode(hasher.finish().to_le_bytes());
EntityTag {
weak: false, tag: Cow::from(tag)
}
}
#[cfg(feature = "std")]
pub fn from_file_meta(metadata: &Metadata) -> EntityTag<'static> {
let mut hasher = HighwayHasher::default();
hasher.write(&metadata.len().to_le_bytes());
if let Ok(modified_time) = metadata.modified() {
if let Ok(time) = modified_time.duration_since(UNIX_EPOCH) {
hasher.write(&time.as_nanos().to_le_bytes());
} else {
hasher.write(b"-");
let time = UNIX_EPOCH.duration_since(modified_time).unwrap();
hasher.write(&time.as_nanos().to_le_bytes());
}
}
let tag =
base64::engine::general_purpose::STANDARD_NO_PAD.encode(hasher.finish().to_le_bytes());
EntityTag {
weak: true, tag: Cow::from(tag)
}
}
}
impl<'t> EntityTag<'t> {
#[inline]
pub fn get_tag(&'t self) -> &'t str {
self.tag.as_ref()
}
#[inline]
pub fn into_tag(self) -> Cow<'t, str> {
self.tag
}
#[inline]
pub fn into_owned(self) -> EntityTag<'static> {
let tag = self.tag.into_owned();
EntityTag {
weak: self.weak, tag: Cow::from(tag)
}
}
}
impl<'t> EntityTag<'t> {
#[inline]
pub fn strong_eq(&self, other: &EntityTag) -> bool {
!self.weak && !other.weak && self.tag == other.tag
}
#[inline]
pub fn weak_eq(&self, other: &EntityTag) -> bool {
self.tag == other.tag
}
#[inline]
pub fn strong_ne(&self, other: &EntityTag) -> bool {
!self.strong_eq(other)
}
#[inline]
pub fn weak_ne(&self, other: &EntityTag) -> bool {
!self.weak_eq(other)
}
}
impl<'t> Display for EntityTag<'t> {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
if self.weak {
f.write_str("W/")?;
}
f.write_char('"')?;
f.write_str(self.tag.as_ref())?;
f.write_char('"')
}
}