use serde_json::Value;
const MAX_COOKIE_VALUE_BYTES: usize = 4096;
fn pairs(jar: &str) -> impl Iterator<Item = (&str, &str)> {
jar.split(';').filter_map(|pair| {
let (name, value) = pair.split_once('=')?;
let name = name.trim();
if name.is_empty() {
return None;
}
let value = unquote(value.trim());
if value.is_empty() || value.len() > MAX_COOKIE_VALUE_BYTES {
return None;
}
Some((name, value))
})
}
fn unquote(value: &str) -> &str {
value
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.unwrap_or(value)
}
pub(crate) fn lookup<'a>(jar: impl IntoIterator<Item = &'a str>, name: &str) -> Option<String> {
jar.into_iter()
.flat_map(pairs)
.find(|(n, _)| *n == name)
.map(|(_, v)| v.to_string())
}
pub(crate) fn collect<'a>(
jar: impl IntoIterator<Item = &'a str>,
allowlist: &[String],
) -> serde_json::Map<String, Value> {
let mut out = serde_json::Map::new();
for (name, value) in jar.into_iter().flat_map(pairs) {
if out.contains_key(name) {
continue;
}
if allowlist.iter().any(|a| a == name) {
out.insert(name.to_string(), Value::String(value.to_string()));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn one(jar: &str, name: &str) -> Option<String> {
lookup([jar], name)
}
#[test]
fn a_plain_jar_parses() {
let jar = "a=1; b=2; browser_uuid=abc-123";
assert_eq!(one(jar, "a").as_deref(), Some("1"));
assert_eq!(one(jar, "browser_uuid").as_deref(), Some("abc-123"));
assert_eq!(one(jar, "missing"), None);
}
#[test]
fn quoted_values_and_loose_whitespace_are_handled() {
assert_eq!(one(r#"sid="abc""#, "sid").as_deref(), Some("abc"));
assert_eq!(one("sid = abc ", "sid").as_deref(), Some("abc"));
assert_eq!(one(" sid=abc ; b=2", "sid").as_deref(), Some("abc"));
assert_eq!(one(r#"sid="abc"#, "sid").as_deref(), Some(r#""abc"#));
}
#[test]
fn a_jar_split_across_headers_is_searched_whole() {
assert_eq!(lookup(["a=1", "b=2"], "b").as_deref(), Some("2"));
}
#[test]
fn base64_padding_survives() {
assert_eq!(one("t=YWJjZA==", "t").as_deref(), Some("YWJjZA=="));
}
#[test]
fn name_matching_is_case_sensitive() {
assert_eq!(one("SID=abc", "sid"), None);
assert_eq!(one("SID=abc", "SID").as_deref(), Some("abc"));
}
#[test]
fn the_first_duplicate_wins() {
assert_eq!(one("a=first; a=second", "a").as_deref(), Some("first"));
}
#[test]
fn empty_and_oversized_values_are_absent() {
assert_eq!(one("a=; b=2", "a"), None);
assert_eq!(one("a=\"\"; b=2", "a"), None);
let huge = format!("a={}", "x".repeat(MAX_COOKIE_VALUE_BYTES + 1));
assert_eq!(one(&huge, "a"), None);
}
#[test]
fn a_malformed_pair_is_skipped_not_fatal() {
assert_eq!(one("novalue; =orphan; a=1", "a").as_deref(), Some("1"));
}
#[test]
fn collect_takes_only_the_allowlist() {
let jar = "browser_uuid=abc; session=secret; other=x";
let out = collect([jar], &["browser_uuid".to_string(), "absent".to_string()]);
assert_eq!(out.len(), 1, "{out:?}");
assert_eq!(out["browser_uuid"], "abc");
assert!(
!out.contains_key("session"),
"an unlisted cookie must never be copied"
);
}
}