#![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},
str::FromStr,
};
#[cfg(all(feature = "std", any(feature = "weak-hasher", feature = "strong-hasher")))]
use std::fs::Metadata;
#[cfg(all(feature = "std", any(feature = "weak-hasher", feature = "strong-hasher")))]
use std::time::UNIX_EPOCH;
#[cfg(any(feature = "weak-hasher", feature = "strong-hasher"))]
use base64::{Engine, engine::general_purpose::STANDARD_NO_PAD};
pub use entity_tag_error::EntityTagError;
#[cfg(feature = "weak-hasher")]
use xxhash_rust::xxh3::xxh3_128;
#[cfg(feature = "strong-hasher")]
#[inline]
fn encode_blake3_256(data: &[u8]) -> String {
STANDARD_NO_PAD.encode(blake3::hash(data).as_bytes())
}
#[cfg(feature = "weak-hasher")]
#[inline]
fn encode_xxh3_128_data(data: &[u8]) -> String {
encode_xxh3_128(xxh3_128(data))
}
#[cfg(feature = "weak-hasher")]
#[inline]
fn encode_xxh3_128(hash: u128) -> String {
STANDARD_NO_PAD.encode(hash.to_be_bytes())
}
#[cfg(all(feature = "std", any(feature = "weak-hasher", feature = "strong-hasher")))]
fn encode_file_meta<F>(metadata: &Metadata, encode: F) -> String
where
F: Fn(&[u8]) -> String, {
let len_bytes = metadata.len().to_le_bytes();
if let Ok(modified_time) = metadata.modified() {
match modified_time.duration_since(UNIX_EPOCH) {
Ok(time) => {
let mut bytes = [0; 24];
bytes[..8].copy_from_slice(&len_bytes);
bytes[8..].copy_from_slice(&time.as_nanos().to_le_bytes());
encode(&bytes)
},
Err(err) => {
let mut bytes = [0; 25];
bytes[..8].copy_from_slice(&len_bytes);
bytes[8] = b'-';
bytes[9..].copy_from_slice(&err.duration().as_nanos().to_le_bytes());
encode(&bytes)
},
}
} else {
encode(&len_bytes)
}
}
#[derive(Debug, Clone, Eq, Hash, 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> {
#[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> {
#[inline]
pub unsafe fn with_string_unchecked<S: Into<String>>(weak: bool, tag: S) -> EntityTag<'static> {
EntityTag {
weak,
tag: Cow::from(tag.into()),
}
}
#[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 {
tag.replace_range(..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)]),
})
}
#[cfg(feature = "weak-hasher")]
#[inline]
pub fn from_data<S: ?Sized + AsRef<[u8]>>(data: &S) -> EntityTag<'static> {
let tag = encode_xxh3_128_data(data.as_ref());
EntityTag {
weak: true, tag: Cow::from(tag)
}
}
#[cfg(feature = "strong-hasher")]
#[inline]
pub fn from_data_strong<S: ?Sized + AsRef<[u8]>>(data: &S) -> EntityTag<'static> {
let tag = encode_blake3_256(data.as_ref());
EntityTag {
weak: false, tag: Cow::from(tag)
}
}
#[cfg(all(feature = "std", feature = "weak-hasher"))]
#[inline]
pub fn from_file_meta(metadata: &Metadata) -> EntityTag<'static> {
let tag = encode_file_meta(metadata, encode_xxh3_128_data);
EntityTag {
weak: true, tag: Cow::from(tag)
}
}
#[cfg(all(feature = "std", feature = "strong-hasher"))]
#[inline]
pub fn from_file_meta_strong(metadata: &Metadata) -> EntityTag<'static> {
let tag = encode_file_meta(metadata, encode_blake3_256);
EntityTag {
weak: false, tag: Cow::from(tag)
}
}
}
impl<'t> EntityTag<'t> {
#[inline]
pub fn get_tag(&self) -> &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 FromStr for EntityTag<'static> {
type Err = EntityTagError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
EntityTag::from_string(s)
}
}
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('"')
}
}