use time::OffsetDateTime;
use crate::cursor::Cursor;
use crate::error::{Component, Error};
use crate::model::{BinaryCookies, Cookie, Flags, Page};
const MAGIC: [u8; 4] = *b"cook";
const PAGE_TAG: [u8; 4] = [0x00, 0x00, 0x01, 0x00];
const ZERO_TAG: [u8; 4] = [0x00; 4];
const MAX_PAGES: u32 = 1 << 16;
const MAX_COOKIES_PER_PAGE: u32 = 1 << 20;
const MAX_COOKIE_SIZE: u32 = 4096;
const MAC_EPOCH_OFFSET: f64 = 978_307_200.0;
const MIN_UNIX_TIMESTAMP: i64 = -377_705_116_800;
const MAX_UNIX_TIMESTAMP: i64 = 253_402_300_799;
pub fn from_bytes(input: &[u8]) -> Result<BinaryCookies, Error> {
let mut decoder = Decoder::new(input);
decoder.signature()?;
let page_count = decoder.page_count()?;
decoder.skip_page_size_table(page_count)?;
let mut pages = Vec::new();
for _ in 0..page_count {
pages.push(decoder.page()?);
}
let checksum = decoder.checksum()?;
Ok(BinaryCookies { pages, checksum })
}
#[derive(Debug, Clone)]
pub(crate) struct Decoder<'a> {
cursor: Cursor<'a>,
}
impl<'a> Decoder<'a> {
pub(crate) const fn new(input: &'a [u8]) -> Self {
Self {
cursor: Cursor::new(input),
}
}
pub(crate) fn signature(&mut self) -> Result<(), Error> {
let signature: [u8; 4] = self.cursor.array()?;
if signature == MAGIC {
Ok(())
} else {
Err(Error::InvalidSignature(signature))
}
}
pub(crate) fn page_count(&mut self) -> Result<u32, Error> {
let count = self.cursor.u32_be()?;
if count > MAX_PAGES {
return Err(Error::TooManyPages(count));
}
Ok(count)
}
pub(crate) fn skip_page_size_table(&mut self, page_count: u32) -> Result<(), Error> {
self.cursor.skip(table_len(page_count)?)
}
fn page(&mut self) -> Result<Page, Error> {
self.page_tag()?;
let cookie_count = self.cookie_count()?;
let offsets = (0..cookie_count)
.map(|_| self.cursor.u32_le())
.collect::<Result<Vec<u32>, Error>>()?;
self.page_end()?;
let mut cookies = Vec::new();
for _ in 0..cookie_count {
cookies.push(self.cookie()?);
}
Ok(Page { cookies, offsets })
}
pub(crate) fn page_tag(&mut self) -> Result<(), Error> {
self.cursor.expect_tag(PAGE_TAG, Error::InvalidPageTag)
}
pub(crate) fn page_end(&mut self) -> Result<(), Error> {
self.cursor.expect_tag(ZERO_TAG, Error::InvalidPageEnd)
}
pub(crate) fn cookie_count(&mut self) -> Result<u32, Error> {
let count = self.cursor.u32_le()?;
if count > MAX_COOKIES_PER_PAGE {
return Err(Error::TooManyCookies(count));
}
if table_len(count)? > self.cursor.remaining() {
return Err(Error::UnexpectedEof);
}
Ok(count)
}
pub(crate) fn skip_cookie_offset_table(&mut self, count: u32) -> Result<(), Error> {
self.cursor.skip(table_len(count)?)
}
pub(crate) fn checksum(&mut self) -> Result<[u8; 8], Error> {
self.cursor.array()
}
pub(crate) fn cookie(&mut self) -> Result<Cookie, Error> {
let size = self.cursor.u32_le()?;
self.cursor.skip(4)?;
let flags = Flags::new(self.cursor.u32_le()?);
self.cursor.skip(4)?;
let domain_offset = self.cursor.u32_le()?;
let name_offset = self.cursor.u32_le()?;
let path_offset = self.cursor.u32_le()?;
let value_offset = self.cursor.u32_le()?;
let comment_offset = self.cursor.u32_le()?;
self.cursor
.expect_tag(ZERO_TAG, Error::InvalidCookieHeaderEnd)?;
let expires = mac_epoch_time(self.cursor.f64_le()?);
let creation = mac_epoch_time(self.cursor.f64_le()?);
let comment_len = component_len(Component::Comment, comment_offset, domain_offset)?;
let domain_len = component_len(Component::Domain, domain_offset, name_offset)?;
let name_len = component_len(Component::Name, name_offset, path_offset)?;
let path_len = component_len(Component::Path, path_offset, value_offset)?;
let value_len = component_len(Component::Value, value_offset, size)?;
let total = comment_len + domain_len + name_len + path_len + value_len;
if total > MAX_COOKIE_SIZE {
return Err(Error::CookieTotalTooLarge(total));
}
let comment = if comment_offset == 0 {
None
} else {
Some(lossy(self.cursor.take(to_len(comment_len)?)?))
};
let domain = trim_terminator(self.cursor.take(to_len(domain_len)?)?);
let name = trim_terminator(self.cursor.take(to_len(name_len)?)?);
let path = trim_terminator(self.cursor.take(to_len(path_len)?)?);
let value = truncate_at_nul(self.cursor.take(to_len(value_len)?)?);
Ok(Cookie {
domain,
name,
path,
value,
comment,
flags,
expires,
creation,
})
}
}
fn component_len(component: Component, start: u32, end: u32) -> Result<u32, Error> {
let len = end.checked_sub(start).ok_or(Error::MalformedOffsets)?;
if len > MAX_COOKIE_SIZE {
return Err(Error::CookieTooLarge {
component,
size: len,
});
}
Ok(len)
}
fn table_len(count: u32) -> Result<usize, Error> {
usize::try_from(u64::from(count) * 4).map_err(|_| Error::UnexpectedEof)
}
fn to_len(len: u32) -> Result<usize, Error> {
usize::try_from(len).map_err(|_| Error::UnexpectedEof)
}
fn mac_epoch_time(mac_seconds: f64) -> OffsetDateTime {
#[expect(
clippy::cast_possible_truncation,
reason = "the saturating f64-to-i64 cast is the rule-4 contract"
)]
let unix = (mac_seconds + MAC_EPOCH_OFFSET) as i64;
let clamped = unix.clamp(MIN_UNIX_TIMESTAMP, MAX_UNIX_TIMESTAMP);
OffsetDateTime::from_unix_timestamp(clamped).unwrap_or(OffsetDateTime::UNIX_EPOCH)
}
fn lossy(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}
fn trim_terminator(bytes: &[u8]) -> String {
lossy(bytes.strip_suffix(&[0x00]).unwrap_or(bytes))
}
fn truncate_at_nul(bytes: &[u8]) -> String {
lossy(bytes.split(|&byte| byte == 0x00).next().unwrap_or(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn component_len_rejects_non_monotonic_offsets() {
assert!(matches!(
component_len(Component::Domain, 10, 9),
Err(Error::MalformedOffsets)
));
assert!(matches!(component_len(Component::Name, 10, 10), Ok(0)));
assert!(matches!(component_len(Component::Path, 10, 14), Ok(4)));
}
#[test]
fn component_len_caps_at_4096() {
assert!(matches!(
component_len(Component::Comment, 0, 4096),
Ok(4096)
));
assert!(matches!(
component_len(Component::Comment, 0, 4097),
Err(Error::CookieTooLarge {
component: Component::Comment,
size: 4097
})
));
}
#[test]
fn mac_epoch_time_handles_extreme_values() {
assert_eq!(mac_epoch_time(0.0).unix_timestamp(), 978_307_200);
assert_eq!(mac_epoch_time(f64::NAN).unix_timestamp(), 0);
assert_eq!(
mac_epoch_time(f64::INFINITY).unix_timestamp(),
MAX_UNIX_TIMESTAMP
);
assert_eq!(
mac_epoch_time(f64::NEG_INFINITY).unix_timestamp(),
MIN_UNIX_TIMESTAMP
);
assert_eq!(mac_epoch_time(1e300).unix_timestamp(), MAX_UNIX_TIMESTAMP);
}
#[test]
fn string_truncation_policies_differ_per_field() {
assert_eq!(trim_terminator(b"abc\x00"), "abc");
assert_eq!(trim_terminator(b"abc\x00\x00"), "abc\x00");
assert_eq!(trim_terminator(b"abc"), "abc");
assert_eq!(trim_terminator(b""), "");
assert_eq!(truncate_at_nul(b"abc\x00tail"), "abc");
assert_eq!(truncate_at_nul(b"\x00tail"), "");
assert_eq!(truncate_at_nul(b"abc"), "abc");
assert_eq!(truncate_at_nul(b""), "");
}
#[test]
fn lossy_replaces_invalid_utf8() {
assert_eq!(lossy(&[0x61, 0xff, 0x62]), "a\u{fffd}b");
}
}