set-cookie-parser 0.1.0

Parse Set-Cookie response headers into structured cookies, and split a comma-joined Set-Cookie header without choking on commas in Expires dates. A faithful port of the set-cookie-parser npm package. Zero dependencies, no_std.
Documentation
//! Behavioral spec for `set-cookie-parser`, cross-checked against the npm package.

use set_cookie_parser::{parse, parse_all, parse_with, split_cookies_string, Cookie};

#[test]
fn simple() {
    let c = parse("foo=bar").unwrap();
    assert_eq!(c.name, "foo");
    assert_eq!(c.value, "bar");
    assert_eq!(
        c,
        Cookie {
            name: "foo".into(),
            value: "bar".into(),
            ..Cookie::default()
        }
    );
}

#[test]
fn attributes() {
    let c = parse("foo=bar; Path=/; HttpOnly").unwrap();
    assert_eq!(c.path.as_deref(), Some("/"));
    assert!(c.http_only);

    let c = parse("a=b; Max-Age=3600; Domain=.example.com").unwrap();
    assert_eq!(c.max_age, Some(3600));
    assert_eq!(c.domain.as_deref(), Some(".example.com"));

    let c = parse("sid=abc; Secure; SameSite=Lax").unwrap();
    assert!(c.secure);
    assert_eq!(c.same_site.as_deref(), Some("Lax"));

    let c = parse("token=x; Expires=Wed, 09 Jun 2021 10:18:14 GMT").unwrap();
    assert_eq!(c.expires.as_deref(), Some("Wed, 09 Jun 2021 10:18:14 GMT"));
}

#[test]
fn other_attributes() {
    let c = parse("x=y; Partitioned; Priority=High").unwrap();
    assert!(c.partitioned);
    assert_eq!(c.other, vec![("priority".to_string(), "High".to_string())]);
}

#[test]
fn name_value_edge_cases() {
    assert_eq!(parse("k=v=w; Path=/").unwrap().value, "v=w");
    let c = parse("justvalue").unwrap();
    assert_eq!(c.name, "");
    assert_eq!(c.value, "justvalue");
    assert_eq!(parse("empty=").unwrap().value, "");
    // empty/extra semicolons are ignored
    assert_eq!(parse("c=d;;;Path=/x").unwrap().path.as_deref(), Some("/x"));
}

#[test]
fn value_decoding() {
    assert_eq!(parse("enc=a%20b%2Fc").unwrap().value, "a b/c");
    assert_eq!(parse("u=%E2%9C%93").unwrap().value, "");
    // a malformed escape keeps the raw value
    assert_eq!(parse("bad=%").unwrap().value, "%");
    // decoding can be disabled
    assert_eq!(parse_with("enc=a%20b", false).unwrap().value, "a%20b");
}

#[test]
fn empty_input() {
    assert!(parse("").is_none());
    assert!(parse("   ").is_none());
    assert_eq!(parse_all(""), Vec::new());
}

#[test]
fn splitting() {
    assert_eq!(split_cookies_string("a=b, c=d"), ["a=b", "c=d"]);
    assert_eq!(
        split_cookies_string("a=b; Path=/, c=d; Path=/"),
        ["a=b; Path=/", "c=d; Path=/"]
    );
    assert_eq!(split_cookies_string("a=b,c=d,e=f"), ["a=b", "c=d", "e=f"]);
    // a comma inside an Expires date is NOT a separator
    assert_eq!(
        split_cookies_string("sid=x; Expires=Wed, 09 Jun 2021 10:18:14 GMT, foo=bar"),
        ["sid=x; Expires=Wed, 09 Jun 2021 10:18:14 GMT", "foo=bar"]
    );
}

#[test]
fn parse_all_combined() {
    let cookies = parse_all("a=b, c=d; Path=/");
    assert_eq!(cookies.len(), 2);
    assert_eq!(cookies[0].name, "a");
    assert_eq!(cookies[1].path.as_deref(), Some("/"));
}

#[test]
fn prototype_pollution_guard() {
    assert!(parse("constructor=x").is_none());
    assert!(parse("__proto__=x").is_none());
}

#[test]
fn decode_edge_cases() {
    // Invalid UTF-8 percent-escapes keep the raw value (matching decodeURIComponent throwing).
    for raw in [
        "%C0%80",
        "%ED%A0%80",
        "%F4%90%80%80",
        "%E2%9C",
        "%80",
        "%",
        "%2",
        "%2G",
    ] {
        assert_eq!(
            parse(&format!("a={raw}")).unwrap().value,
            raw,
            "raw={raw:?}"
        );
    }
    // Valid escapes decode (hex is case-insensitive).
    assert_eq!(parse("a=%e2%9c%93").unwrap().value, "");
    assert_eq!(parse("a=%F0%9F%98%80").unwrap().value, "😀");
    assert_eq!(parse("a=%25").unwrap().value, "%");
    // '+' is not decoded by decodeURIComponent.
    assert_eq!(parse("a=x+y").unwrap().value, "x+y");
}

#[test]
fn split_edge_cases() {
    assert_eq!(split_cookies_string(", a=b"), ["", "a=b"]);
    assert_eq!(split_cookies_string("a=b,"), ["a=b,"]);
    assert_eq!(split_cookies_string(" ,a=b"), [" ", "a=b"]);
    assert_eq!(split_cookies_string("a=b,,c=d"), ["a=b,", "c=d"]);
    assert_eq!(split_cookies_string("=,="), ["=", "="]);
    assert_eq!(split_cookies_string("a=b, c"), ["a=b, c"]); // no '=' after comma -> not a separator
    assert_eq!(split_cookies_string("a=b,c"), ["a=b,c"]);
    assert_eq!(split_cookies_string("a,b"), ["a,b"]);
}