use cookie::{Cookie, ParseError};
use std::borrow::Cow;
pub trait CookieBuilder: Sized {
fn new(name: String, value: String) -> Self;
#[cfg(feature = "percent-encode")]
fn parse_encoded(cookie_str: String) -> Result<Self, ParseError>;
}
pub struct HeaderStringCookies<'c, C: CookieBuilder> {
string: Cow<'c, str>,
last: usize,
_phantom: std::marker::PhantomData<C>,
}
#[inline(always)]
fn is_cookie_name_start(b: u8) -> bool {
matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_')
}
impl<'c, C: CookieBuilder> Iterator for HeaderStringCookies<'c, C> {
type Item = Result<C, ParseError>;
fn next(&mut self) -> Option<Self::Item> {
let s = self.string.as_ref();
let len = s.len();
while self.last < len {
let i = self.last;
let j = s[i..].find(';').map(|k| i + k).unwrap_or(len);
let end_pos = if j < len {
let after = &s[j + 1..];
let trimmed = after.trim_start();
if trimmed.is_empty() || trimmed.starts_with(';') {
j } else if let Some(first) = trimmed.as_bytes().first().copied() {
if is_cookie_name_start(first) {
if let Some(eq_pos) = trimmed.find('=') {
let name_part = &trimmed[..eq_pos].trim();
if !name_part.is_empty()
&& name_part.chars().all(|c| {
let b = c as u8;
matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'-')
})
{
j } else {
self.find_real_separator(j)
}
} else {
self.find_real_separator(j)
}
} else {
self.find_real_separator(j)
}
} else {
j }
} else {
j };
self.last = end_pos + 1;
let cookie_str = s[i..end_pos].trim();
if cookie_str.is_empty() {
continue;
}
let eq_pos = match cookie_str.find('=') {
Some(p) => p,
None => continue,
};
let name = cookie_str[..eq_pos].trim();
let val = cookie_str[eq_pos + 1..].trim();
if name.is_empty() {
continue;
}
let cookie_result = if val.contains('%') {
#[cfg(feature = "percent-encode")]
{
let mut cookie_str_buf = String::with_capacity(name.len() + val.len() + 1);
cookie_str_buf.push_str(name);
cookie_str_buf.push('=');
cookie_str_buf.push_str(val);
C::parse_encoded(cookie_str_buf)
}
#[cfg(not(feature = "percent-encode"))]
{
Ok(C::new(name.to_string(), val.to_string()))
}
} else {
Ok(C::new(name.to_string(), val.to_string()))
};
return Some(cookie_result);
}
None
}
}
impl<'c, C: CookieBuilder> HeaderStringCookies<'c, C> {
#[inline]
fn find_real_separator(&self, start: usize) -> usize {
let s = self.string.as_ref();
let bytes = s.as_bytes();
let len = s.len();
let mut i = start + 1;
while i < len && bytes[i].is_ascii_whitespace() {
i += 1;
}
while i < len {
if bytes[i] == b';' {
let mut j = i + 1;
while j < len && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j >= len || bytes[j] == b';' {
return i; }
if j < len && is_cookie_name_start(bytes[j]) {
let mut k = j;
while k < len && matches!(bytes[k], b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'-') {
k += 1;
}
if k < len && bytes[k] == b'=' {
return i; }
}
}
i += 1;
}
len }
}
pub trait CookieHeaderStringExt<'c, C: CookieBuilder> {
fn header_string_parse<S>(string: S) -> HeaderStringCookies<'c, C>
where
S: Into<Cow<'c, str>>;
}
impl CookieBuilder for Cookie<'static> {
fn new(name: String, value: String) -> Self {
Cookie::new(name, value)
}
#[cfg(feature = "percent-encode")]
fn parse_encoded(cookie_str: String) -> Result<Self, ParseError> {
Cookie::parse_encoded(cookie_str)
}
}
impl<'c> CookieHeaderStringExt<'c, Cookie<'static>> for Cookie<'c> {
#[inline(always)]
fn header_string_parse<S>(string: S) -> HeaderStringCookies<'c, Cookie<'static>>
where
S: Into<Cow<'c, str>>,
{
HeaderStringCookies {
string: string.into(),
last: 0,
_phantom: std::marker::PhantomData,
}
}
}
#[cfg(feature = "reqwest")]
pub mod reqwest_support {
use super::*;
pub fn parse_for_reqwest<'c, S>(string: S) -> HeaderStringCookies<'c, Cookie<'static>>
where
S: Into<Cow<'c, str>>,
{
HeaderStringCookies {
string: string.into(),
last: 0,
_phantom: std::marker::PhantomData,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_string_parse() {
let cases = [
("", vec![]),
(";;", vec![]),
("name=val;ue", vec![("name", "val;ue")]),
("name=val;ue;hello=world", vec![("name", "val;ue"), ("hello", "world")]),
];
for (string, expected) in cases {
let cookies: Vec<_> = Cookie::header_string_parse(string).filter_map(|parse| parse.ok()).collect();
let actual: Vec<_> = cookies.iter().map(|c| c.name_value()).collect();
assert_eq!(expected, actual);
}
}
#[test]
fn header_string_parse_empty_values() {
let cookie_header = "name=; other=value";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].value(), "");
assert_eq!(cookies[1].value(), "value");
}
#[test]
fn header_string_parse_whitespace_handling() {
let cookie_header = " name = value ; other = val ";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].name_value(), ("name", "value"));
assert_eq!(cookies[1].name_value(), ("other", "val"));
}
#[test]
fn header_string_parse_multiple_consecutive_semicolons() {
let cookie_header = "name=;;;value;;;other=val";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert!(!cookies.is_empty());
}
#[test]
fn header_string_parse_special_characters() {
let cookie_header = "session=!@#$%^&*(){}[]; other=value";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].value(), "!@#$%^&*(){}[]");
}
#[test]
fn header_string_parse_value_with_equals() {
let cookie_header = "session=abc=123; other=value";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].value(), "abc=123");
}
#[test]
fn header_string_parse_long_values() {
let long_value = "x".repeat(1000);
let cookie_header = format!("name={long_value}; other=val");
let cookies: Vec<_> = Cookie::header_string_parse(&cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].value().len(), 1000);
}
#[test]
fn header_string_parse_complex_semicolons() {
let cookie_header = "session=abc;def;ghi; other=value";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].value(), "abc;def;ghi");
assert_eq!(cookies[1].value(), "value");
}
#[test]
#[cfg(feature = "percent-encode")]
fn header_string_parse_percent_encoded() {
let cookie_header = "name=val%20ue";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 1);
assert_eq!(cookies[0].name_value(), ("name", "val ue"));
}
#[test]
#[cfg(feature = "percent-encode")]
fn header_string_parse_percent_encoded_semicolon() {
let cookie_header = "name=val%3B123; other=value";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].value(), "val;123");
}
#[test]
fn header_string_parse_numeric_names() {
let cookie_header = "123=value; _456=other";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].name(), "123");
}
#[test]
fn header_string_parse_hyphenated_names() {
let cookie_header = "session-id=value; other-val=data";
let cookies: Vec<_> = Cookie::header_string_parse(cookie_header).filter_map(|parse| parse.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].name(), "session-id");
}
#[test]
#[cfg(feature = "reqwest")]
fn header_string_parse_reqwest() {
use crate::reqwest_support::parse_for_reqwest;
let cookie_header = "session=abc;123; other=value";
let cookies: Vec<_> = parse_for_reqwest(cookie_header).filter_map(|result| result.ok()).collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].value(), "abc;123");
assert_eq!(cookies[1].value(), "value");
}
}