#![no_std]
#![deny(warnings)]
#[cfg(feature = "std")]
extern crate std;
use core::mem;
use core::fmt::{self, Write};
type Buffer = str_buf::StrBuf::<62>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntityTag {
pub weak: bool,
tag: Buffer,
}
impl EntityTag {
pub fn new(weak: bool, tag: &str) -> Self {
let mut result = Self {
weak,
tag: Buffer::new(),
};
debug_assert!(tag.is_ascii());
let written = result.tag.push_str(tag);
debug_assert_eq!(written, tag.len());
result
}
#[inline]
pub fn weak(tag: &str) -> Self {
Self::new(true, tag)
}
#[inline]
pub fn strong(tag: &str) -> Self {
Self::new(false, tag)
}
pub fn checked_new(weak: bool, tag: &str) -> Result<Self, ParseError> {
if tag.is_ascii() {
let mut result = Self {
weak,
tag: Buffer::new(),
};
match result.tag.push_str(tag) == tag.len() {
true => Ok(result),
false => Err(ParseError::Overflow)
}
} else {
Err(ParseError::NotAscii)
}
}
#[inline]
pub fn checked_weak(tag: &str) -> Result<Self, ParseError> {
Self::checked_new(true, tag)
}
#[inline]
pub fn checked_strong(tag: &str) -> Result<Self, ParseError> {
Self::checked_new(false, tag)
}
#[cfg(feature = "std")]
pub fn from_file_meta(metadata: &std::fs::Metadata) -> Self {
let mut tag = Buffer::new();
let _ = match metadata.modified().map(|modified| modified.duration_since(std::time::UNIX_EPOCH).expect("Modified is earlier than time::UNIX_EPOCH!")) {
Ok(modified) => write!(tag, "{}.{}-{}", modified.as_secs(), modified.subsec_nanos(), metadata.len()),
_ => write!(tag, "{}", metadata.len())
};
Self {
weak: true,
tag
}
}
pub const fn const_from_data(bytes: &[u8]) -> Self {
const SEP: u8 = b'-';
let mut bytes_len = bytes.len() as u64;
let mut hash = xxhash_rust::const_xxh3::xxh3_128(bytes);
let mut storage_len = 0;
let mut storage = [mem::MaybeUninit::<u8>::uninit(); 62];
while bytes_len > 9 {
let digit = bytes_len % 10;
bytes_len = bytes_len / 10;
storage[storage_len] = mem::MaybeUninit::new(b'0' + digit as u8);
storage_len += 1;
}
storage[storage_len] = mem::MaybeUninit::new(b'0' + (bytes_len % 10) as u8);
storage_len += 1;
let mut idx = 0;
let mut storage_end = storage_len - 1;
while idx < storage_end {
let temp = storage[idx];
storage[idx] = storage[storage_end];
storage[storage_end] = temp;
idx += 1;
storage_end -= 1;
}
storage[storage_len] = mem::MaybeUninit::new(SEP);
storage_len += 1;
let first_part_cursor = storage_len;
while hash > 9 {
let digit = hash % 10;
hash = hash / 10;
storage[storage_len] = mem::MaybeUninit::new(b'0' + digit as u8);
storage_len += 1;
}
storage[storage_len] = mem::MaybeUninit::new(b'0' + (hash % 10) as u8);
storage_len += 1;
idx = first_part_cursor;
storage_end = storage_len - 1;
while idx < storage_end {
let temp = storage[idx];
storage[idx] = storage[storage_end];
storage[storage_end] = temp;
idx += 1;
storage_end -= 1;
}
Self {
weak: false,
tag: unsafe {
Buffer::from_storage(storage, storage_len as u8)
}
}
}
pub fn from_data(bytes: &[u8]) -> Self {
let hash = xxhash_rust::xxh3::xxh3_128(bytes);
let mut tag = Buffer::new();
let _ = write!(tag, "{}-{}", bytes.len(), hash);
Self {
weak: false,
tag
}
}
pub fn tag(&self) -> &str {
self.tag.as_str()
}
pub fn strong_eq(&self, other: &EntityTag) -> bool {
!self.weak && !other.weak && self.tag.as_str() == other.tag.as_str()
}
pub fn weak_eq(&self, other: &EntityTag) -> bool {
self.tag.as_str() == other.tag.as_str()
}
pub fn strong_ne(&self, other: &EntityTag) -> bool {
!self.strong_eq(other)
}
pub fn weak_ne(&self, other: &EntityTag) -> bool {
!self.weak_eq(other)
}
}
impl fmt::Display for EntityTag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.weak {
f.write_str("W/")?;
}
f.write_char('"')?;
f.write_str(self.tag.as_str())?;
f.write_char('"')
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum ParseError {
InvalidFormat,
NotAscii,
Overflow,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParseError::InvalidFormat => f.write_str("EntityTag uses invalid format"),
ParseError::NotAscii => f.write_str("EntityTag uses non-ASCII characters"),
ParseError::Overflow => f.write_str("EntityTag size overflows buffer"),
}
}
}
impl core::str::FromStr for EntityTag {
type Err = ParseError;
fn from_str(text: &str) -> Result<EntityTag, ParseError> {
let len = text.len();
let slice = &text[..];
if !slice.ends_with('"') || len < 2 {
return Err(ParseError::InvalidFormat);
}
if slice.starts_with('"') {
let slice = &slice[1..len-1];
EntityTag::checked_strong(slice)
} else if len >= 4 && slice.starts_with("W/\"") {
let slice = &slice[3..len-1];
EntityTag::checked_weak(slice)
} else {
Err(ParseError::InvalidFormat)
}
}
}
#[cfg(test)]
mod tests {
use super::{EntityTag, Buffer};
#[test]
fn assert_buffer_fits() {
assert_eq!(core::mem::size_of::<EntityTag>(), 64);
let expected = std::format!("{0}.{0}-{0}", u64::max_value());
let res = Buffer::from_str_checked(&expected).expect("To fit");
assert_eq!(expected.as_str(), res);
let expected = std::format!("{0}-{1}", u64::max_value(), u128::max_value());
let res = Buffer::from_str_checked(&expected).expect("To fit");
assert_eq!(expected.as_str(), res);
}
}