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);
if !value
.iter()
.all(|&b| is_cookie_octet(b) || (allow_ws && is_ws(b)))
{
return None;
}
percent_decode(value).decode_utf8().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[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:?}"
);
}
}
}
}