use std::collections::HashMap;
use std::process::Command;
use anyhow::{Context, Result};
use tracing::{debug, warn};
use super::crypto::{SCHEMA_VERSION_WITH_DOMAIN_TAG, decrypt_cookie_value};
pub(super) struct CookieRow {
pub(super) name: String,
pub(super) value: String,
pub(super) encrypted_bytes: Vec<u8>,
}
pub(super) fn copy_db_to_temp(cookie_path: &std::path::Path) -> Result<std::path::PathBuf> {
let temp_dir = std::env::temp_dir().join(format!("nab_cookies_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir)?;
let temp_db = temp_dir.join("Cookies");
std::fs::copy(cookie_path, &temp_db)?;
for suffix in ["-wal", "-shm"] {
let wal = cookie_path.with_extension(format!("Cookies{suffix}"));
if wal.exists() {
let _ = std::fs::copy(&wal, temp_db.with_extension(format!("Cookies{suffix}")));
}
}
Ok(temp_db)
}
pub(super) fn query_db_schema_version(temp_db: &std::path::Path) -> u32 {
let Some(db_str) = temp_db.to_str() else {
return 0;
};
let output = Command::new("sqlite3")
.args([db_str, "SELECT value FROM meta WHERE key='version';"])
.output();
let Ok(out) = output else { return 0 };
String::from_utf8_lossy(&out.stdout)
.trim()
.parse()
.unwrap_or(0)
}
pub(super) fn query_cookie_db(temp_db: &std::path::Path, domain: &str) -> Result<Vec<CookieRow>> {
let conditions = build_domain_conditions(domain);
let where_clause = conditions.join(" OR ");
let query =
format!("SELECT name, value, hex(encrypted_value) FROM cookies WHERE {where_clause}");
debug!("Cookie SQL query for '{}': WHERE {}", domain, where_clause);
let temp_db_str = temp_db
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid temp database path"))?;
let output = Command::new("sqlite3")
.args(["-separator", "\t", temp_db_str, &query])
.output()
.context("Failed to query cookie database")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
warn!("SQLite query failed: {}", stderr);
return Ok(Vec::new());
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(parse_cookie_rows(&stdout))
}
pub(super) fn build_domain_conditions(domain: &str) -> Vec<String> {
let parts: Vec<&str> = domain.split('.').collect();
let mut conditions = vec![
format!("host_key = '{domain}'"),
format!("host_key = '.{domain}'"),
];
for i in 1..parts.len() {
let parent = parts[i..].join(".");
conditions.push(format!("host_key = '.{parent}'"));
}
conditions
}
pub(super) fn parse_cookie_rows(stdout: &str) -> Vec<CookieRow> {
let mut rows = Vec::new();
for line in stdout.lines() {
let parts: Vec<&str> = line.splitn(3, '\t').collect();
if parts.len() < 2 {
continue;
}
let encrypted_bytes = if parts.len() >= 3 && !parts[2].is_empty() {
hex::decode(parts[2]).unwrap_or_default()
} else {
Vec::new()
};
rows.push(CookieRow {
name: parts[0].to_string(),
value: parts[1].to_string(),
encrypted_bytes,
});
}
rows
}
pub(super) fn decrypt_rows(
rows: Vec<CookieRow>,
key: Option<&[u8]>,
has_domain_tag: bool,
) -> HashMap<String, String> {
let mut cookies = HashMap::new();
for row in rows {
if !row.value.is_empty() {
cookies.insert(row.name, row.value);
continue;
}
if row.encrypted_bytes.is_empty() {
continue;
}
let Some(k) = key else {
debug!(
"Skipping encrypted cookie '{}' — no key available",
row.name
);
continue;
};
match decrypt_cookie_value(&row.encrypted_bytes, k, has_domain_tag) {
Ok(plain) => {
cookies.insert(row.name, plain);
}
Err(e) => {
warn!("Cookie decryption failed for '{}': {}", row.name, e);
}
}
}
cookies
}
pub(super) fn has_domain_tag(temp_db: &std::path::Path) -> bool {
let version = query_db_schema_version(temp_db);
debug!("Cookie DB schema v{version}");
version >= SCHEMA_VERSION_WITH_DOMAIN_TAG
}