use std::borrow::Cow;
use percent_encoding::{percent_decode, utf8_percent_encode};
use rfc_6265::grammar::{is_cookie_octet, is_ws};
use crate::grammar::{ENCODE_FULL, ENCODE_IN_QUOTES};
use crate::wire::trim_ws;
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ValueEncoding {
Auto,
#[default]
Percent,
Quoted,
Raw,
}
pub fn encode_value(value: &str, encoding: ValueEncoding) -> Cow<'_, str> {
match encoding {
ValueEncoding::Raw => Cow::Borrowed(value),
ValueEncoding::Percent => utf8_percent_encode(value, ENCODE_FULL).into(),
ValueEncoding::Quoted => Cow::Owned(quote(value)),
ValueEncoding::Auto => {
if value.bytes().all(|b| is_cookie_octet(b) && b != b'%') {
Cow::Borrowed(value)
} else if value.bytes().any(is_ws) {
Cow::Owned(quote(value))
} else {
utf8_percent_encode(value, ENCODE_FULL).into()
}
}
}
}
fn quote(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
out.extend(utf8_percent_encode(value, ENCODE_IN_QUOTES));
out.push('"');
out
}
pub(crate) fn decode_cookie_value(raw_value: &[u8], allow_ws: bool) -> Option<Cow<'_, str>> {
let value = trim_ws(raw_value);
let value = value
.strip_prefix(b"\"")
.and_then(|inner| inner.strip_suffix(b"\""))
.unwrap_or(value);
let mut has_escape_introducer = false;
for &b in value {
if !(is_cookie_octet(b) || (allow_ws && is_ws(b))) {
return None;
}
has_escape_introducer |= b == b'%';
}
if !has_escape_introducer {
return std::str::from_utf8(value).ok().map(Cow::Borrowed);
}
percent_decode(value).decode_utf8().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_borrows_exactly_when_no_escape_decodes() {
for (raw, decoded) in [
(&b"deadbeef"[..], "deadbeef"),
(b" deadbeef\t", "deadbeef"),
(b"\"deadbeef\"", "deadbeef"),
(b"", ""),
] {
assert!(
matches!(decode_cookie_value(raw, false), Some(Cow::Borrowed(v)) if v == decoded),
"{raw:?}"
);
}
for raw in [&b"100%zz"[..], b"50%", b"%", b"a%2xb"] {
assert!(
matches!(decode_cookie_value(raw, false), Some(Cow::Borrowed(_))),
"{raw:?}"
);
}
for (raw, decoded) in [(&b"a%20b"[..], "a b"), (b"caf%C3%A9", "café")] {
assert!(
matches!(decode_cookie_value(raw, false), Some(Cow::Owned(v)) if v == decoded),
"{raw:?}"
);
}
assert_eq!(decode_cookie_value(b"a%FFb", false), None);
assert_eq!(decode_cookie_value(b"a,b", true), None);
assert_eq!(decode_cookie_value(b"a b", false), None);
assert!(matches!(
decode_cookie_value(b"a b", true),
Some(Cow::Borrowed("a b"))
));
}
#[test]
fn auto_emits_bare_quoted_or_percent() {
assert_eq!(
encode_value("deadBEEF09", ValueEncoding::Auto),
"deadBEEF09"
);
assert_eq!(encode_value("a b", ValueEncoding::Auto), "\"a b\"");
assert_eq!(
encode_value("hello world", ValueEncoding::Auto),
"\"hello world\""
);
assert_eq!(encode_value("a b,c", ValueEncoding::Auto), "\"a b%2Cc\"");
assert_eq!(encode_value("a;b", ValueEncoding::Auto), "a%3Bb");
assert_eq!(encode_value("café", ValueEncoding::Auto), "caf%C3%A9");
assert_eq!(encode_value("100%", ValueEncoding::Auto), "100%25");
assert_eq!(encode_value("%41", ValueEncoding::Auto), "%2541");
assert_eq!(encode_value("a\"b", ValueEncoding::Auto), "a%22b");
}
#[test]
fn percent_always_encodes_never_quotes() {
assert_eq!(encode_value("a b", ValueEncoding::Percent), "a%20b");
assert_eq!(encode_value("a;b", ValueEncoding::Percent), "a%3Bb");
assert_eq!(encode_value("deadbeef", ValueEncoding::Percent), "deadbeef");
assert_eq!(encode_value("%41", ValueEncoding::Percent), "%2541");
}
#[test]
fn quoted_always_wraps_losslessly() {
assert_eq!(encode_value("plain", ValueEncoding::Quoted), "\"plain\"");
assert_eq!(encode_value("a b", ValueEncoding::Quoted), "\"a b\"");
assert_eq!(encode_value("a;b", ValueEncoding::Quoted), "\"a%3Bb\"");
assert_eq!(encode_value("café", ValueEncoding::Quoted), "\"caf%C3%A9\"");
}
#[test]
fn raw_is_verbatim() {
assert_eq!(encode_value("a b;c\"\\", ValueEncoding::Raw), "a b;c\"\\");
}
#[test]
fn raw_passes_non_ascii_verbatim() {
assert_eq!(encode_value("café", ValueEncoding::Raw), "café");
}
#[test]
fn managed_encodings_never_emit_injection_bytes() {
let hostile = [
"a;b",
"a\r\nX: y",
"a b",
"café",
"a,b",
"a\"b",
"a\\b",
"\u{0}\u{1f}\u{7f}",
"%41",
"a b\nc",
];
for v in hostile {
for enc in [
ValueEncoding::Auto,
ValueEncoding::Percent,
ValueEncoding::Quoted,
] {
let out = encode_value(v, enc);
assert!(
!out.contains(';')
&& !out.contains('\r')
&& !out.contains('\n')
&& !out.contains('\0'),
"{enc:?} of {v:?} leaked an unsafe wire byte: {out:?}"
);
}
}
}
}