use std::{fmt, str::FromStr};
use rama_core::error::BoxError;
use rama_core::telemetry::tracing;
use rama_http_types::HeaderValue;
use rama_utils::collections::NonEmptyVec;
use super::IterExt;
use crate::{
Error,
util::{
FlatCsvSeparator, try_decode_flat_csv_header_values_as_non_empty_vec,
try_encode_non_empty_vec_of_bytes_as_flat_csv_header_value,
},
};
#[derive(Clone, Eq, PartialEq)]
pub(crate) struct EntityTag<T = HeaderValue>(T);
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum EntityTagRange {
Any,
Tags(NonEmptyVec<EntityTag>),
}
impl<T: AsRef<[u8]>> EntityTag<T> {
pub(crate) fn tag(&self) -> &[u8] {
let bytes = self.0.as_ref();
let end = bytes.len() - 1;
if bytes[0] == b'W' {
&bytes[3..end]
} else {
&bytes[1..end]
}
}
pub(crate) fn is_weak(&self) -> bool {
self.0.as_ref()[0] == b'W'
}
pub(crate) fn strong_eq<R>(&self, other: &EntityTag<R>) -> bool
where
R: AsRef<[u8]>,
{
!self.is_weak() && !other.is_weak() && self.tag() == other.tag()
}
pub(crate) fn weak_eq<R>(&self, other: &EntityTag<R>) -> bool
where
R: AsRef<[u8]>,
{
self.tag() == other.tag()
}
#[cfg(test)]
pub(crate) fn strong_ne(&self, other: &EntityTag) -> bool {
!self.strong_eq(other)
}
#[cfg(test)]
pub(crate) fn weak_ne(&self, other: &EntityTag) -> bool {
!self.weak_eq(other)
}
pub(crate) fn parse(src: T) -> Option<Self> {
let slice = src.as_ref();
let length = slice.len();
if length < 2 || slice[length - 1] != b'"' {
return None;
}
let start = match slice[0] {
b'"' => 1,
b'W' if length >= 4 && slice[1] == b'/' && slice[2] == b'"' => 3,
_ => return None,
};
if check_slice_validity(&slice[start..length - 1]) {
Some(Self(src))
} else {
None
}
}
}
impl EntityTag {
#[cfg(test)]
pub(crate) fn from_static(bytes: &'static str) -> Self {
let val = HeaderValue::from_static(bytes);
match Self::from_val(&val) {
Some(tag) => tag,
None => {
panic!("invalid static string for EntityTag: {bytes:?}");
}
}
}
pub(crate) fn from_owned(val: HeaderValue) -> Option<Self> {
EntityTag::parse(val.as_bytes())?;
Some(Self(val))
}
pub(crate) fn from_val(val: &HeaderValue) -> Option<Self> {
EntityTag::parse(val.as_bytes()).map(|_entity| Self(val.clone()))
}
}
impl<T: AsRef<[u8]>> AsRef<[u8]> for EntityTag<T> {
#[inline(always)]
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl<T: fmt::Debug> fmt::Debug for EntityTag<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl super::TryFromValues for EntityTag {
fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
where
I: Iterator<Item = &'i HeaderValue>,
{
values
.just_one()
.and_then(Self::from_val)
.ok_or_else(Error::invalid)
}
}
impl<T: FromStr> FromStr for EntityTag<T> {
type Err = T::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.parse()?))
}
}
impl From<EntityTag> for HeaderValue {
fn from(tag: EntityTag) -> Self {
tag.0
}
}
impl<'a> From<&'a EntityTag> for HeaderValue {
fn from(tag: &'a EntityTag) -> Self {
tag.0.clone()
}
}
fn check_slice_validity(slice: &[u8]) -> bool {
slice.iter().all(|&c| {
debug_assert!(
(b'\x21'..=b'\x7e').contains(&c) | (c >= b'\x80'),
"EntityTag expects HeaderValue to have check for control characters"
);
c != b'"'
})
}
impl EntityTagRange {
pub(crate) fn matches_strong(&self, entity: &EntityTag) -> bool {
self.matches_if(entity, |a, b| a.strong_eq(b))
}
pub(crate) fn matches_weak(&self, entity: &EntityTag) -> bool {
self.matches_if(entity, |a, b| a.weak_eq(b))
}
fn matches_if<F>(&self, entity: &EntityTag, func: F) -> bool
where
F: Fn(&EntityTag, &EntityTag) -> bool,
{
match *self {
Self::Any => true,
Self::Tags(ref tags) => tags.iter().any(|tag| func(tag, entity)),
}
}
}
impl super::TryFromValues for EntityTagRange {
fn try_from_values<'i, I>(values: &mut I) -> Result<Self, Error>
where
I: Iterator<Item = &'i HeaderValue>,
{
match try_decode_flat_csv_header_values_as_non_empty_vec::<EntityTag>(
values,
FlatCsvSeparator::Comma,
) {
Ok(values) => {
if values.len() == 1 && values.head.0.as_bytes().eq_ignore_ascii_case(b"*") {
Ok(Self::Any)
} else {
Ok(Self::Tags(values))
}
}
Err(err) => {
tracing::trace!("invalid entity tags: {err}");
Err(crate::Error::invalid())
}
}
}
}
impl TryFrom<&EntityTagRange> for HeaderValue {
type Error = BoxError;
fn try_from(tag: &EntityTagRange) -> Result<Self, Self::Error> {
match tag {
EntityTagRange::Any => Ok(Self::from_static("*")),
EntityTagRange::Tags(tags) => {
try_encode_non_empty_vec_of_bytes_as_flat_csv_header_value(
tags,
FlatCsvSeparator::Comma,
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(slice: &[u8]) -> Option<EntityTag> {
let val = HeaderValue::from_bytes(slice).ok()?;
EntityTag::from_val(&val)
}
#[test]
fn test_etag_parse_success() {
let tag = parse(b"\"foobar\"").unwrap();
assert!(!tag.is_weak());
assert_eq!(tag.tag(), b"foobar");
let weak = parse(b"W/\"weaktag\"").unwrap();
assert!(weak.is_weak());
assert_eq!(weak.tag(), b"weaktag");
}
#[test]
fn test_etag_parse_failures() {
macro_rules! fails {
($slice:expr) => {
assert_eq!(parse($slice), None);
};
}
fails!(b"no-dquote");
fails!(b"w/\"the-first-w-is-case sensitive\"");
fails!(b"W/\"");
fails!(b"");
fails!(b"\"unmatched-dquotes1");
fails!(b"unmatched-dquotes2\"");
fails!(b"\"inner\"quotes\"");
}
#[test]
fn test_cmp() {
let mut etag1 = EntityTag::from_static("W/\"1\"");
let mut etag2 = etag1.clone();
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag2 = EntityTag::from_static("W/\"2\"");
assert!(!etag1.strong_eq(&etag2));
assert!(!etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(etag1.weak_ne(&etag2));
etag2 = EntityTag::from_static("\"1\"");
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::from_static("\"1\"");
assert!(etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(!etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
}
}