use crate::common::{date, enums::*};
use byteorder::{BigEndian, ByteOrder, LittleEndian};
use eyre::{anyhow, bail, Context, Result};
use std::{fs::File, io::Read, path::PathBuf, vec::Vec};
pub fn safari_based(db_path: PathBuf, domains: Option<Vec<String>>) -> Result<Vec<Cookie>> {
let mut file = File::open(&db_path).context(format!(
"Failed to open {}\n\
Make sure you have full disk access for the current process.\n\
For example, in VSCode or Terminal:\n\
1. Open Settings\n\
2. Privacy & Security\n\
3. Full Disk Access\n\
---\n\
You can also open the disk access page with: \n\
open \"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles\"\n\
",
db_path.display()
))?;
let mut bs: Vec<u8> = Vec::new();
file.read_to_end(&mut bs)?;
let cookies = parse_content(&bs)?;
if let Some(domain_filters) = domains {
let filtered_cookies: Vec<Cookie> = cookies
.into_iter()
.filter(|cookie| {
domain_filters.iter().any(|domain| {
cookie.domain.ends_with(domain)
})
})
.collect();
Ok(filtered_cookies)
} else {
Ok(cookies)
}
}
fn parse_page(bs: &[u8]) -> Result<Vec<Cookie>> {
if slice(bs, 0, 4)? != [0x00, 0x00, 0x01, 0x00] {
bail!("bad page header");
}
let count = slice(bs, 4, 4).map(LittleEndian::read_u32)? as usize;
let parsed_table = parse_table::<LittleEndian>(&bs[8..], count)?;
let mut cookies: Vec<Cookie> = vec![];
for off in parsed_table {
let slice_result = slice(bs, off, 4);
let u32_value = slice_result.map(LittleEndian::read_u32);
let parsed_slice = u32_value.and_then(|len| slice(bs, off, len as usize));
let cookie = parsed_slice.and_then(parse_cookie::<LittleEndian>)?;
cookies.push(cookie);
}
if slice(bs, count * 4 + 8, 4)? != [0x00, 0x00, 0x00, 0x00] {
bail!("bad page trailer");
}
Ok(cookies)
}
fn parse_cookie<T: ByteOrder>(bs: &[u8]) -> Result<Cookie> {
if bs.len() < 0x30 {
bail!("cookie data underflow");
}
let flags = T::read_u32(&bs[0x08..0x0c]);
let url_off = T::read_u32(&bs[0x10..0x14]) as usize;
let name_off = T::read_u32(&bs[0x14..0x18]) as usize;
let path_off = T::read_u32(&bs[0x18..0x1c]) as usize;
let value_off = T::read_u32(&bs[0x1c..0x20]) as usize;
let expires = T::read_u64(&bs[0x28..0x30]);
let expires = date::safari_timestamp(expires);
let url = slice_to(bs, url_off, name_off).and_then(c_str)?;
let name = slice_to(bs, name_off, path_off).and_then(c_str)?;
let path = slice_to(bs, path_off, value_off).and_then(c_str)?;
let value = slice_to(bs, value_off, bs.len()).and_then(c_str)?;
let is_secure = (flags & 0x01) == 0x01;
let is_http_only = (flags & 0x04) == 0x04;
let cookie = Cookie {
expires,
domain: url,
http_only: is_http_only,
name,
path,
value,
same_site: 0,
secure: is_secure,
};
Ok(cookie)
}
fn parse_content(bs: &[u8]) -> Result<Vec<Cookie>> {
if slice(bs, 0, 4)? != [0x63, 0x6f, 0x6f, 0x6b] {
bail!("not a cookie file");
}
let count = slice(bs, 4, 4).map(BigEndian::read_u32)? as usize;
let table_iter = parse_table::<BigEndian>(&bs[8..], count)?;
let table_iter = table_iter.iter();
let mut pages = Vec::new();
let mut off = count * 4 + 8;
for &len in table_iter {
let page_slice = match slice(bs, off, len) {
Ok(slice) => slice,
Err(_) => {
bail!("Can't get slice from page");
}
};
pages.push(page_slice.to_vec());
off += len;
}
let mut cookies: Vec<Cookie> = vec![];
for page in pages {
let cookie = parse_page(page.as_slice())?;
cookies.extend(cookie);
}
Ok(cookies)
}
fn slice(bs: &[u8], off: usize, len: usize) -> Result<&[u8]> {
if off + len > bs.len() {
bail!("data underflow: {}", off + len - bs.len())
} else {
Ok(&bs[off..off + len])
}
}
fn parse_table<T: ByteOrder>(bs: &[u8], count: usize) -> Result<Vec<usize>> {
let end = count * 4;
if end > bs.len() {
bail!("table data underflow");
}
let data = bs[..end]
.chunks(4)
.map(|u| T::read_u32(u) as usize)
.collect();
Ok(data)
}
fn slice_to(bs: &[u8], off: usize, to: usize) -> Result<&[u8]> {
if to < off {
bail!("negative data length: {}", to - off)
} else {
slice(bs, off, to - off)
}
}
fn c_str(bs: &[u8]) -> Result<String> {
bs.split_last()
.ok_or_else(|| anyhow!("null c string"))
.and_then(|(&last, elements)| {
if last == 0x00 {
Ok(elements)
} else {
bail!("c string non null terminator")
}
})
.and_then(|elements| {
String::from_utf8(elements.to_vec()).map_err(|err| anyhow!(err.to_string()))
})
}