#![no_std]
#![doc(html_root_url = "https://docs.rs/set-cookie-parser/0.1.0")]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Cookie {
pub name: String,
pub value: String,
pub expires: Option<String>,
pub max_age: Option<i64>,
pub domain: Option<String>,
pub path: Option<String>,
pub secure: bool,
pub http_only: bool,
pub same_site: Option<String>,
pub partitioned: bool,
pub other: Vec<(String, String)>,
}
#[must_use]
pub fn parse(set_cookie: &str) -> Option<Cookie> {
parse_with(set_cookie, true)
}
#[must_use]
pub fn parse_with(set_cookie: &str, decode_values: bool) -> Option<Cookie> {
let parts: Vec<&str> = set_cookie
.split(';')
.filter(|p| !p.trim_matches(is_js_whitespace).is_empty())
.collect();
let name_value = *parts.first()?;
let (name, raw_value) = parse_name_value_pair(name_value);
if is_forbidden_key(name) {
return None;
}
let value = if decode_values {
decode_uri_component(raw_value).unwrap_or_else(|| raw_value.to_string())
} else {
raw_value.to_string()
};
let mut cookie = Cookie {
name: name.to_string(),
value,
..Cookie::default()
};
for part in &parts[1..] {
let (key, val) = match part.split_once('=') {
Some((k, v)) => (k, v),
None => (*part, ""),
};
let key = key.trim_start_matches(is_js_whitespace).to_lowercase();
if is_forbidden_key(&key) {
continue;
}
match key.as_str() {
"expires" => cookie.expires = Some(val.to_string()),
"max-age" => {
if let Some(n) = parse_int_js(val) {
cookie.max_age = Some(n);
}
}
"domain" => cookie.domain = Some(val.to_string()),
"path" => cookie.path = Some(val.to_string()),
"secure" => cookie.secure = true,
"httponly" => cookie.http_only = true,
"samesite" => cookie.same_site = Some(val.to_string()),
"partitioned" => cookie.partitioned = true,
"" => {}
_ => {
if let Some(slot) = cookie.other.iter_mut().find(|(k, _)| *k == key) {
slot.1 = val.to_string();
} else {
cookie.other.push((key, val.to_string()));
}
}
}
}
Some(cookie)
}
#[must_use]
pub fn split_cookies_string(cookies_string: &str) -> Vec<String> {
let chars: Vec<char> = cookies_string.chars().collect();
let len = chars.len();
let mut result = Vec::new();
let mut pos = 0;
while pos < len {
let mut start = pos;
let mut separator_found = false;
loop {
while pos < len && is_js_whitespace(chars[pos]) {
pos += 1;
}
if pos >= len {
break;
}
if chars[pos] == ',' {
let last_comma = pos;
pos += 1;
while pos < len && is_js_whitespace(chars[pos]) {
pos += 1;
}
let next_start = pos;
while pos < len && {
let c = chars[pos];
c != '=' && c != ';' && c != ','
} {
pos += 1;
}
if pos < len && chars[pos] == '=' {
separator_found = true;
pos = next_start;
result.push(chars[start..last_comma].iter().collect());
start = pos;
} else {
pos = last_comma + 1;
}
} else {
pos += 1;
}
}
if !separator_found || pos >= len {
result.push(chars[start..len].iter().collect());
}
}
result
}
#[must_use]
pub fn parse_all(combined: &str) -> Vec<Cookie> {
parse_all_with(combined, true)
}
#[must_use]
pub fn parse_all_with(combined: &str, decode_values: bool) -> Vec<Cookie> {
if combined.trim_matches(is_js_whitespace).is_empty() {
return Vec::new();
}
split_cookies_string(combined)
.into_iter()
.filter_map(|s| parse_with(&s, decode_values))
.collect()
}
fn parse_name_value_pair(s: &str) -> (&str, &str) {
match s.split_once('=') {
Some((name, value)) => (name, value),
None => ("", s),
}
}
const FORBIDDEN_KEYS: &[&str] = &[
"constructor",
"__proto__",
"__defineGetter__",
"__defineSetter__",
"hasOwnProperty",
"__lookupGetter__",
"__lookupSetter__",
"isPrototypeOf",
"propertyIsEnumerable",
"toString",
"valueOf",
"toLocaleString",
];
fn is_forbidden_key(key: &str) -> bool {
FORBIDDEN_KEYS.contains(&key)
}
fn parse_int_js(s: &str) -> Option<i64> {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() && is_js_whitespace(chars[i]) {
i += 1;
}
let negative = match chars.get(i) {
Some('+') => {
i += 1;
false
}
Some('-') => {
i += 1;
true
}
_ => false,
};
let start = i;
let mut acc: i64 = 0;
while i < chars.len() && chars[i].is_ascii_digit() {
let d = i64::from(chars[i] as u32 - '0' as u32);
acc = acc.saturating_mul(10).saturating_add(d);
i += 1;
}
if i == start {
return None;
}
Some(if negative { -acc } else { acc })
}
fn decode_uri_component(s: &str) -> Option<String> {
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
let mut out = String::new();
let mut i = 0;
while i < len {
if chars[i] == '%' {
let b0 = read_hex_byte(&chars, i)?;
i += 3;
if b0 < 0x80 {
out.push(b0 as char);
} else {
let n = utf8_sequence_len(b0)?;
let mut buf = vec![b0];
for _ in 1..n {
if i >= len || chars[i] != '%' {
return None;
}
let bn = read_hex_byte(&chars, i)?;
if bn & 0xC0 != 0x80 {
return None;
}
buf.push(bn);
i += 3;
}
out.push_str(core::str::from_utf8(&buf).ok()?);
}
} else {
out.push(chars[i]);
i += 1;
}
}
Some(out)
}
fn read_hex_byte(chars: &[char], i: usize) -> Option<u8> {
let hi = u8::try_from(chars.get(i + 1)?.to_digit(16)?).ok()?;
let lo = u8::try_from(chars.get(i + 2)?.to_digit(16)?).ok()?;
Some(hi * 16 + lo)
}
fn utf8_sequence_len(b: u8) -> Option<usize> {
match b {
0xC0..=0xDF => Some(2),
0xE0..=0xEF => Some(3),
0xF0..=0xF7 => Some(4),
_ => None,
}
}
fn is_js_whitespace(c: char) -> bool {
(c.is_whitespace() && c != '\u{0085}') || c == '\u{feff}'
}