use crate::{
class,
error::{ErrorKind, ParseError, Result, WriteError, err},
limits::Limits,
text::{skip_ows, trim_ows},
write::{Sink, reserve},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EntityTag<'a> {
pub(crate) weak: bool,
pub(crate) 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> {
text: &'a [u8],
count: usize,
}
impl<'a> TagList<'a> {
pub fn iter(&self) -> impl Iterator<Item = EntityTag<'a>> + use<'a> {
TagIter { s: self.text, i: 0 }
}
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
struct TagIter<'a> {
s: &'a [u8],
i: usize,
}
impl<'a> Iterator for TagIter<'a> {
type Item = EntityTag<'a>;
fn next(&mut self) -> Option<EntityTag<'a>> {
while matches!(self.s.get(self.i), Some(b' ' | b'\t' | b',')) {
self.i += 1;
}
let (tag, used) = parse_prefix(self.s.get(self.i..)?).ok()?;
self.i += used;
Some(tag)
}
}
enum Walked {
Any,
Tags(usize),
}
fn walk<'a>(v: &'a [u8], limits: &Limits, mut f: impl FnMut(EntityTag<'a>)) -> Result<Walked> {
let (mut i, mut count, mut empty, mut star) = (0usize, 0usize, 0usize, false);
loop {
while matches!(v.get(i), Some(b' ' | b'\t')) {
i += 1;
}
match v.get(i) {
None => break,
Some(b',') => {
empty += 1;
if empty > usize::from(limits.empty_elements) {
return err(ErrorKind::TooManyEmptyElements, i);
}
i += 1;
continue;
}
Some(b'*') if !star && count == 0 => {
star = true;
i += 1;
}
Some(_) if star => return err(ErrorKind::Malformed, i),
Some(_) => {
let (tag, used) =
parse_prefix(v.get(i..).unwrap_or(&[])).map_err(|e| match e.kind() {
ErrorKind::InvalidEntityTag
if e.offset() + i >= v.len() && v.get(i) == Some(&b'"') =>
{
ParseError::new(ErrorKind::UnterminatedQuote, v.len())
}
_ => e.shifted(i),
})?;
count += 1;
if count > usize::from(limits.list_members) {
return err(ErrorKind::TooManyMembers, i);
}
f(tag);
i += used;
}
}
while matches!(v.get(i), Some(b' ' | b'\t')) {
i += 1;
}
match v.get(i) {
None => break,
Some(b',') => {}
Some(&b) if crate::scan::is_control(b) => return err(crate::scan::control_kind(b), i),
Some(_) if star => return err(ErrorKind::Malformed, i),
Some(_) => return err(ErrorKind::InvalidEntityTag, i),
}
}
Ok(if star {
Walked::Any
} else {
Walked::Tags(count)
})
}
pub fn parse_condition<'a>(value: &'a [u8], limits: &Limits) -> Result<Condition<'a>> {
let lead = skip_ows(value);
let v = trim_ows(value);
Ok(
match walk(v, limits, |_| {}).map_err(|e| e.shifted(lead))? {
Walked::Any => Condition::Any,
Walked::Tags(count) => Condition::Tags(TagList { text: v, count }),
},
)
}
pub fn if_none_match(value: &[u8], current: &EntityTag<'_>) -> Result<bool> {
let lead = skip_ows(value);
let mut matched = false;
let walked = walk(trim_ows(value), &Limits::DEFAULT, |t| {
matched |= t.weak_eq(current)
})
.map_err(|e| e.shifted(lead))?;
Ok(matches!(walked, Walked::Any) || matched)
}
pub fn if_match(value: &[u8], current: Option<&EntityTag<'_>>) -> Result<bool> {
let lead = skip_ows(value);
let mut matched = false;
let walked = walk(trim_ows(value), &Limits::DEFAULT, |t| {
matched |= current.is_some_and(|c| t.strong_eq(c));
})
.map_err(|e| e.shifted(lead))?;
Ok(match walked {
Walked::Any => current.is_some(),
Walked::Tags(_) => matched,
})
}
#[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));
}
}