use crate::{
class,
error::{ErrorKind, ParseError, Result, WriteError, err},
limits::Limits,
list,
text::trim_ows,
write::{Sink, reserve},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EntityTag<'a> {
weak: bool,
opaque: &'a [u8],
}
const fn first_bad(opaque: &[u8]) -> Option<usize> {
let mut i = 0;
while i < opaque.len() {
#[allow(clippy::indexing_slicing)]
let b = opaque[i];
if !class::is_etagc(b) {
return Some(i);
}
i += 1;
}
None
}
impl<'a> EntityTag<'a> {
pub fn parse(value: &'a [u8]) -> Result<Self> {
let lead = crate::text::skip_ows(value);
let (tag, used) = parse_prefix(trim_ows(value)).map_err(|e| e.shifted(lead))?;
if used != trim_ows(value).len() {
return err(ErrorKind::TrailingData, lead + used);
}
Ok(tag)
}
pub const fn strong(opaque: &'a [u8]) -> Result<Self> {
match first_bad(opaque) {
Some(at) => Err(ParseError::new(ErrorKind::InvalidEntityTag, at)),
None => Ok(Self {
weak: false,
opaque,
}),
}
}
pub const fn weak(opaque: &'a [u8]) -> Result<Self> {
match first_bad(opaque) {
Some(at) => Err(ParseError::new(ErrorKind::InvalidEntityTag, at)),
None => Ok(Self { weak: true, opaque }),
}
}
#[must_use]
pub const fn is_weak(&self) -> bool {
self.weak
}
#[must_use]
pub const fn opaque(&self) -> &'a [u8] {
self.opaque
}
#[must_use]
pub fn strong_eq(&self, other: &EntityTag<'_>) -> bool {
!self.weak && !other.weak && self.opaque == other.opaque
}
#[must_use]
pub fn weak_eq(&self, other: &EntityTag<'_>) -> bool {
self.opaque == other.opaque
}
pub fn write_to(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
let prefix: &[u8] = if self.weak { b"W/\"" } else { b"\"" };
reserve(out, prefix.len() + self.opaque.len() + 1)?;
out.put(prefix)?;
out.put(self.opaque)?;
out.put(b"\"")?;
Ok(())
}
}
fn parse_prefix(s: &[u8]) -> Result<(EntityTag<'_>, usize)> {
let (weak, start) = match s {
[b'W', b'/', ..] => (true, 2),
_ => (false, 0),
};
if s.get(start) != Some(&b'"') {
return err(ErrorKind::InvalidEntityTag, start);
}
let body = s.get(start + 1..).unwrap_or(&[]);
let close = body.iter().position(|&b| !class::is_etagc(b));
match close {
Some(i) if body.get(i) == Some(&b'"') => {
let opaque = body.get(..i).unwrap_or(&[]);
Ok((EntityTag { weak, opaque }, start + i + 2))
}
Some(i) => {
let b = body.get(i).copied().unwrap_or(0);
let kind = if crate::scan::is_control(b) {
crate::scan::control_kind(b)
} else {
ErrorKind::InvalidEntityTag
};
err(kind, start + 1 + i)
}
None => err(ErrorKind::InvalidEntityTag, s.len()),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Condition<'a> {
Any,
Tags(TagList<'a>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TagList<'a> {
list: list::List<'a>,
}
impl<'a> TagList<'a> {
pub fn iter(&self) -> impl Iterator<Item = EntityTag<'a>> + use<'a> {
self.list
.iter()
.filter_map(|e| parse_prefix(e).ok().map(|(t, _)| t))
}
#[must_use]
pub const fn len(&self) -> usize {
self.list.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.list.is_empty()
}
}
pub fn parse_condition<'a>(value: &'a [u8], limits: &Limits) -> Result<Condition<'a>> {
let l = list::parse(value, limits)?;
let mut star = false;
for e in l.iter() {
let at = (e.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
if e == b"*" {
star = true;
continue;
}
let (_, used) = parse_prefix(e).map_err(|x| x.shifted(at))?;
if used != e.len() {
return err(ErrorKind::InvalidEntityTag, at + used);
}
}
if star {
if l.len() != 1 {
return err(ErrorKind::Malformed, 0);
}
return Ok(Condition::Any);
}
Ok(Condition::Tags(TagList { list: l }))
}
pub fn if_none_match(value: &[u8], current: &EntityTag<'_>) -> Result<bool> {
Ok(match parse_condition(value, &Limits::DEFAULT)? {
Condition::Any => true,
Condition::Tags(tags) => tags.iter().any(|t| t.weak_eq(current)),
})
}
pub fn if_match(value: &[u8], current: Option<&EntityTag<'_>>) -> Result<bool> {
Ok(match parse_condition(value, &Limits::DEFAULT)? {
Condition::Any => current.is_some(),
Condition::Tags(tags) => current.is_some_and(|c| tags.iter().any(|t| t.strong_eq(c))),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entity_tag_edge_cases() {
assert_eq!(EntityTag::parse(b"\"\"").unwrap().opaque(), b"");
assert!(EntityTag::parse(b"W/\"\"").unwrap().is_weak());
let k = |v: &[u8]| EntityTag::parse(v).unwrap_err().kind();
assert_eq!(k(b"w/\"a\""), ErrorKind::InvalidEntityTag);
assert_eq!(k(b"\"a\"b"), ErrorKind::TrailingData);
assert_eq!(k(b"\"a"), ErrorKind::InvalidEntityTag);
assert_eq!(k(b"W/ \"a\""), ErrorKind::InvalidEntityTag);
assert_eq!(k(b"\"a\x7f\""), ErrorKind::Control);
assert_eq!(k(b"\"a b\""), ErrorKind::InvalidEntityTag);
assert_eq!(k(b"abc"), ErrorKind::InvalidEntityTag);
assert_eq!(k(b""), ErrorKind::InvalidEntityTag);
assert!(EntityTag::parse(b"\"\x80\xff!\"").is_ok());
assert!(EntityTag::strong(b"a b").is_err());
assert!(EntityTag::weak(b"\"").is_err());
}
#[test]
fn test_comparison_table_rfc_9110() {
let w1 = EntityTag::weak(b"1").unwrap();
let w2 = EntityTag::weak(b"2").unwrap();
let s1 = EntityTag::strong(b"1").unwrap();
assert!(!w1.strong_eq(&w1) && w1.weak_eq(&w1));
assert!(!w1.strong_eq(&w2) && !w1.weak_eq(&w2));
assert!(!w1.strong_eq(&s1) && w1.weak_eq(&s1));
assert!(s1.strong_eq(&s1) && s1.weak_eq(&s1));
}
#[test]
fn test_conditions() {
let l = Limits::DEFAULT;
assert_eq!(
parse_condition(b"*, \"a\"", &l).unwrap_err().kind(),
ErrorKind::Malformed
);
assert_eq!(
parse_condition(b"\"a\" \"b\"", &l).unwrap_err().kind(),
ErrorKind::InvalidEntityTag
);
let e = parse_condition(b"\"a\", w/\"b\"", &l).unwrap_err();
assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidEntityTag, 5));
let s = EntityTag::strong(b"b").unwrap();
assert!(if_none_match(b"\"a\", , \"b\"", &s).unwrap());
assert!(if_none_match(b"*", &s).unwrap());
assert!(!if_match(b"*", None).unwrap());
assert!(if_match(b"\"a\", \"b\"", Some(&s)).unwrap());
assert!(if_none_match(b"\"a", &s).is_err());
}
#[test]
fn test_write_round_trip() {
let mut buf = [0u8; 8];
let mut out = crate::SliceSink::new(&mut buf);
let t = EntityTag::strong(b"xyz").unwrap();
t.write_to(&mut out).unwrap();
assert_eq!(EntityTag::parse(out.written()).unwrap(), t);
let mut small = [0u8; 3];
let mut out = crate::SliceSink::new(&mut small);
assert_eq!(t.write_to(&mut out), Err(WriteError::Full));
}
}