use std::fmt;
pub(crate) fn lossy_escaped(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).escape_debug().to_string()
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PairIssue<'a> {
#[non_exhaustive]
MissingEquals {
segment: &'a [u8],
},
#[non_exhaustive]
InvalidName {
name: &'a [u8],
},
#[non_exhaustive]
InvalidValue {
name: &'a str,
value: &'a [u8],
},
}
impl fmt::Display for PairIssue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingEquals { segment } => {
write!(f, "cookie pair `{}` has no `=`", lossy_escaped(segment))
}
Self::InvalidName { name } => {
write!(
f,
"cookie name `{}` is empty or not a token",
lossy_escaped(name)
)
}
Self::InvalidValue { name, value } => {
write!(
f,
"cookie `{name}` value `{}` carries a byte outside the accepted set \
or percent-escapes that are not valid UTF-8",
lossy_escaped(value)
)
}
}
}
}
impl std::error::Error for PairIssue<'_> {}
#[must_use = "the report carries both the parsed value and what was refused"]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Reported<T, I> {
pub value: T,
pub issues: Vec<I>,
}
impl<T, I> Reported<T, I> {
pub fn is_clean(&self) -> bool {
self.issues.is_empty()
}
pub fn into_result(self) -> Result<T, (T, Vec<I>)> {
if self.issues.is_empty() {
Ok(self.value)
} else {
Err((self.value, self.issues))
}
}
pub fn into_value(self) -> T {
self.value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_never_echoes_control_bytes() {
let issues = [
PairIssue::MissingEquals {
segment: b"ga;rbage\r\n\x00",
},
PairIssue::InvalidName {
name: b"a\xffb\r\n\x00;",
},
PairIssue::InvalidValue {
name: "n",
value: b"a\x01b;\r\n\x00\xe9",
},
];
for issue in issues {
let rendered = issue.to_string();
assert!(
!rendered.bytes().any(|b| b.is_ascii_control()),
"{rendered:?} carries a raw control byte"
);
}
}
#[test]
fn into_result_splits_by_cleanliness() {
let clean: Reported<u8, PairIssue<'_>> = Reported {
value: 1,
issues: Vec::new(),
};
assert!(clean.is_clean());
assert_eq!(clean.into_result(), Ok(1));
let dirty = Reported {
value: 2,
issues: vec![PairIssue::MissingEquals { segment: b"junk" }],
};
assert!(!dirty.is_clean());
let (value, issues) = dirty.into_result().unwrap_err();
assert_eq!(value, 2);
assert_eq!(issues.len(), 1);
}
#[test]
fn into_value_discards_the_report() {
let dirty = Reported {
value: 3,
issues: vec![PairIssue::MissingEquals { segment: b"junk" }],
};
assert_eq!(dirty.into_value(), 3);
}
}