use crate::types::{Cookie, Error, Result};
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::io::{Cursor, Read};
pub struct IosBinaryCookieParser;
const MAGIC_BYTES: &[u8] = b"cook";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IosCookieMetadata {
pub flags: u32,
pub unknown1: u32,
pub unknown2: u32,
pub url_offset: u32,
pub name_offset: u32,
pub path_offset: u32,
pub value_offset: u32,
pub expiration_date: f64,
pub creation_date: f64,
}
impl IosBinaryCookieParser {
pub fn parse_binary_cookie_file(data: &[u8]) -> Result<Vec<Cookie>> {
let mut cursor = Cursor::new(data);
let mut magic = [0u8; 4];
cursor
.read_exact(&mut magic)
.map_err(|e| Error::ForensicError(format!("Failed to read magic bytes: {e}")))?;
if magic != MAGIC_BYTES {
return Err(Error::ForensicError(
"Invalid binarycookies format: wrong magic bytes".to_string(),
));
}
let num_pages = Self::read_u32_be(&mut cursor)?;
let mut page_sizes = Vec::with_capacity(num_pages as usize);
for _ in 0..num_pages {
page_sizes.push(Self::read_u32_be(&mut cursor)?);
}
let mut all_cookies = Vec::new();
for page_size in page_sizes {
let page_data = Self::read_bytes(&mut cursor, page_size as usize)?;
let cookies = Self::parse_page(&page_data)?;
all_cookies.extend(cookies);
}
Ok(all_cookies)
}
fn parse_page(data: &[u8]) -> Result<Vec<Cookie>> {
let mut cursor = Cursor::new(data);
let page_magic = Self::read_u32_le(&mut cursor)?;
if page_magic != 0x0000_0100 {
return Err(Error::ForensicError(format!(
"Invalid page magic: 0x{page_magic:08X}"
)));
}
let num_cookies = Self::read_u32_le(&mut cursor)?;
let mut cookie_offsets = Vec::with_capacity(num_cookies as usize);
for _ in 0..num_cookies {
cookie_offsets.push(Self::read_u32_le(&mut cursor)?);
}
let mut cookies = Vec::new();
for offset in cookie_offsets {
let cookie = Self::parse_cookie_at_offset(data, offset as usize)?;
cookies.push(cookie);
}
Ok(cookies)
}
fn parse_cookie_at_offset(data: &[u8], offset: usize) -> Result<Cookie> {
let mut cursor = Cursor::new(&data[offset..]);
let _cookie_size = Self::read_u32_le(&mut cursor)?;
let metadata = Self::read_cookie_metadata(&mut cursor)?;
let url = Self::read_string_at_offset(data, offset + metadata.url_offset as usize)?;
let name = Self::read_string_at_offset(data, offset + metadata.name_offset as usize)?;
let path = Self::read_string_at_offset(data, offset + metadata.path_offset as usize)?;
let value = Self::read_string_at_offset(data, offset + metadata.value_offset as usize)?;
let domain = Self::extract_domain(&url);
let created_at = Self::mac_time_to_datetime(metadata.creation_date);
let expires = Some(Self::mac_time_to_datetime(metadata.expiration_date));
let secure = (metadata.flags & 0x01) != 0;
let http_only = (metadata.flags & 0x04) != 0;
Ok(Cookie {
name,
value,
domain: Some(domain),
path: Some(path),
secure,
http_only,
same_site: None,
expires,
max_age: None,
created_at,
size: 0,
extensions: std::collections::HashMap::new(),
is_third_party: false,
source_url: None,
category: None,
})
}
fn read_cookie_metadata(cursor: &mut Cursor<&[u8]>) -> Result<IosCookieMetadata> {
Ok(IosCookieMetadata {
unknown1: Self::read_u32_le(cursor)?,
unknown2: Self::read_u32_le(cursor)?,
flags: Self::read_u32_le(cursor)?,
url_offset: Self::read_u32_le(cursor)?,
name_offset: Self::read_u32_le(cursor)?,
path_offset: Self::read_u32_le(cursor)?,
value_offset: Self::read_u32_le(cursor)?,
expiration_date: Self::read_f64_le(cursor)?,
creation_date: Self::read_f64_le(cursor)?,
})
}
fn read_string_at_offset(data: &[u8], offset: usize) -> Result<String> {
let mut end = offset;
while end < data.len() && data[end] != 0 {
end += 1;
}
String::from_utf8(data[offset..end].to_vec())
.map_err(|e| Error::ForensicError(format!("Invalid UTF-8 in cookie string: {e}")))
}
fn extract_domain(url: &str) -> String {
url.split("//")
.nth(1)
.and_then(|s| s.split('/').next())
.unwrap_or(url)
.to_string()
}
#[allow(clippy::cast_possible_truncation)] fn mac_time_to_datetime(mac_time: f64) -> DateTime<Utc> {
const MAC_EPOCH_UNIX_TIMESTAMP: i64 = 978_307_200; let unix_timestamp = MAC_EPOCH_UNIX_TIMESTAMP + mac_time as i64;
Utc.timestamp_opt(unix_timestamp, 0).unwrap()
}
fn read_u32_be(cursor: &mut Cursor<&[u8]>) -> Result<u32> {
let mut buf = [0u8; 4];
cursor
.read_exact(&mut buf)
.map_err(|e| Error::ForensicError(format!("Failed to read u32: {e}")))?;
Ok(u32::from_be_bytes(buf))
}
fn read_u32_le(cursor: &mut Cursor<&[u8]>) -> Result<u32> {
let mut buf = [0u8; 4];
cursor
.read_exact(&mut buf)
.map_err(|e| Error::ForensicError(format!("Failed to read u32: {e}")))?;
Ok(u32::from_le_bytes(buf))
}
fn read_f64_le(cursor: &mut Cursor<&[u8]>) -> Result<f64> {
let mut buf = [0u8; 8];
cursor
.read_exact(&mut buf)
.map_err(|e| Error::ForensicError(format!("Failed to read f64: {e}")))?;
Ok(f64::from_le_bytes(buf))
}
fn read_bytes(cursor: &mut Cursor<&[u8]>, count: usize) -> Result<Vec<u8>> {
let mut buf = vec![0u8; count];
cursor
.read_exact(&mut buf)
.map_err(|e| Error::ForensicError(format!("Failed to read {count} bytes: {e}")))?;
Ok(buf)
}
pub fn extract_metadata(_data: &[u8]) -> Result<Vec<IosCookieMetadata>> {
todo!("Metadata extraction")
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Datelike;
#[test]
fn test_mac_time_conversion() {
let mac_epoch = IosBinaryCookieParser::mac_time_to_datetime(0.0);
assert_eq!(mac_epoch.year(), 2001);
assert_eq!(mac_epoch.month(), 1);
assert_eq!(mac_epoch.day(), 1);
}
#[test]
fn test_invalid_magic_bytes() {
let invalid_data = b"invalid";
let result = IosBinaryCookieParser::parse_binary_cookie_file(invalid_data);
assert!(result.is_err());
}
}