use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExpectError {
Empty,
InvalidFormat,
InvalidToken,
InvalidValue,
}
impl fmt::Display for ExpectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExpectError::Empty => write!(f, "empty Expect header"),
ExpectError::InvalidFormat => write!(f, "invalid Expect header format"),
ExpectError::InvalidToken => write!(f, "invalid Expect token"),
ExpectError::InvalidValue => write!(f, "invalid Expect value"),
}
}
}
impl std::error::Error for ExpectError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Expect {
items: Vec<Expectation>,
}
impl Expect {
pub fn parse(input: &str) -> Result<Self, ExpectError> {
let input = input.trim();
let mut items = Vec::new();
for part in split_with_quotes(input, ',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let (token, value) = if let Some((token, value)) = part.split_once('=') {
let token = token.trim();
if token.is_empty() {
return Err(ExpectError::InvalidFormat);
}
let value = parse_value(value)?;
(token, Some(value))
} else {
(part, None)
};
if !is_valid_token(token) {
return Err(ExpectError::InvalidToken);
}
items.push(Expectation {
token: token.to_ascii_lowercase(),
value,
});
}
Ok(Expect { items })
}
pub fn items(&self) -> &[Expectation] {
&self.items
}
pub fn has_100_continue(&self) -> bool {
self.items
.iter()
.any(|item| item.token.eq_ignore_ascii_case("100-continue"))
}
}
impl fmt::Display for Expect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let values: Vec<String> = self.items.iter().map(|item| item.to_string()).collect();
write!(f, "{}", values.join(", "))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Expectation {
token: String,
value: Option<String>,
}
impl Expectation {
pub fn token(&self) -> &str {
&self.token
}
pub fn value(&self) -> Option<&str> {
self.value.as_deref()
}
pub fn is_100_continue(&self) -> bool {
self.token.eq_ignore_ascii_case("100-continue")
}
}
impl fmt::Display for Expectation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.value {
Some(value) => {
if needs_quoting(value) {
write!(f, "{}=\"{}\"", self.token, escape_quotes(value))
} else {
write!(f, "{}={}", self.token, value)
}
}
None => write!(f, "{}", self.token),
}
}
}
fn parse_value(input: &str) -> Result<String, ExpectError> {
let input = input.trim();
if input.is_empty() {
return Err(ExpectError::InvalidValue);
}
if let Some(rest) = input.strip_prefix('"') {
let (value, remaining) = parse_quoted_string(rest)?;
if !remaining.trim().is_empty() {
return Err(ExpectError::InvalidValue);
}
Ok(value)
} else {
if !is_valid_token(input) {
return Err(ExpectError::InvalidValue);
}
Ok(input.to_string())
}
}
fn parse_quoted_string(input: &str) -> Result<(String, &str), ExpectError> {
let mut result = String::new();
let mut escaped = false;
for (i, c) in input.char_indices() {
if escaped {
result.push(c);
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
return Ok((result, &input[i + 1..]));
} else {
result.push(c);
}
}
Err(ExpectError::InvalidValue)
}
fn split_with_quotes(input: &str, delimiter: char) -> Vec<String> {
let mut parts = Vec::new();
let mut start = 0;
let mut in_quote = false;
let mut escaped = false;
for (i, c) in input.char_indices() {
if escaped {
escaped = false;
continue;
}
if c == '\\' && in_quote {
escaped = true;
continue;
}
if c == '"' {
in_quote = !in_quote;
continue;
}
if c == delimiter && !in_quote {
parts.push(input[start..i].to_string());
start = i + c.len_utf8();
}
}
parts.push(input[start..].to_string());
parts
}
fn is_valid_token(s: &str) -> bool {
!s.is_empty() && s.bytes().all(is_token_char)
}
fn is_token_char(b: u8) -> bool {
matches!(
b,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
b'0'..=b'9' | b'A'..=b'Z' | b'^' | b'_' | b'`' | b'a'..=b'z' | b'|' | b'~'
)
}
fn needs_quoting(s: &str) -> bool {
s.is_empty() || s.bytes().any(|b| !is_token_char(b))
}
fn escape_quotes(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple() {
let expect = Expect::parse("100-continue").unwrap();
assert!(expect.has_100_continue());
assert_eq!(expect.items().len(), 1);
}
#[test]
fn parse_extension() {
let expect = Expect::parse("foo=bar, 100-continue").unwrap();
assert_eq!(expect.items().len(), 2);
assert_eq!(expect.items()[0].token(), "foo");
assert_eq!(expect.items()[0].value(), Some("bar"));
}
#[test]
fn parse_quoted_value() {
let expect = Expect::parse("token=\"va\\\\lue\"").unwrap();
assert_eq!(expect.items()[0].value(), Some("va\\lue"));
}
#[test]
fn parse_invalid() {
assert!(Expect::parse("bad value").is_err());
assert!(Expect::parse("token=").is_err());
}
#[test]
fn parse_empty_elements() {
let expect = Expect::parse("").unwrap();
assert!(expect.items().is_empty());
let expect = Expect::parse(",").unwrap();
assert!(expect.items().is_empty());
let expect = Expect::parse("100-continue,,foo=bar").unwrap();
assert_eq!(expect.items().len(), 2);
}
#[test]
fn display() {
let expect = Expect::parse("foo=bar, 100-continue").unwrap();
assert_eq!(expect.to_string(), "foo=bar, 100-continue");
}
#[test]
fn empty_value_roundtrip() {
let expect = Expect::parse("token=\"\"").unwrap();
assert_eq!(expect.items()[0].value(), Some(""));
let displayed = expect.to_string();
assert_eq!(displayed, "token=\"\"");
let reparsed = Expect::parse(&displayed).unwrap();
assert_eq!(expect, reparsed);
}
}