use std::path::Path;
use crate::TailFinError;
pub fn build_cookie_header(cookies: &[(String, String)]) -> String {
cookies
.iter()
.map(|(name, value)| format!("{}={}", name, value))
.collect::<Vec<_>>()
.join("; ")
}
pub fn parse_netscape_cookies(data: &str) -> Vec<(String, String)> {
let mut cookies = Vec::new();
for line in data.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.splitn(7, '\t').collect();
if parts.len() < 6 {
continue;
}
let name = parts[5].to_string();
let value = if parts.len() > 6 {
parts[6].to_string()
} else {
String::new()
};
cookies.push((name, value));
}
cookies
}
pub fn load_netscape_file(path: &Path) -> Result<Vec<(String, String)>, TailFinError> {
let data = std::fs::read_to_string(path)
.map_err(|e| TailFinError::Io(format!("Cannot read cookie file: {}", e)))?;
let cookies = parse_netscape_cookies(&data);
if cookies.is_empty() {
return Err(TailFinError::AuthRequired);
}
Ok(cookies)
}
pub fn write_netscape_file(path: &Path, cookies: &[serde_json::Value]) -> Result<(), TailFinError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| TailFinError::Io(format!("Cannot create directory: {}", e)))?;
}
let mut lines = vec![
"# Netscape HTTP Cookie File".to_string(),
"# Exported by tail-fin".to_string(),
String::new(),
];
for cookie in cookies {
let domain = cookie.get("domain").and_then(|v| v.as_str()).unwrap_or("");
let flag = if domain.starts_with('.') {
"TRUE"
} else {
"FALSE"
};
let path_val = cookie.get("path").and_then(|v| v.as_str()).unwrap_or("/");
let secure = if cookie
.get("secure")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
"TRUE"
} else {
"FALSE"
};
let expires = cookie
.get("expires")
.and_then(|v| v.as_f64())
.map(|v| v as u64)
.unwrap_or(0);
let name = cookie.get("name").and_then(|v| v.as_str()).unwrap_or("");
let value = cookie.get("value").and_then(|v| v.as_str()).unwrap_or("");
lines.push(format!(
"{}\t{}\t{}\t{}\t{}\t{}\t{}",
domain, flag, path_val, secure, expires, name, value
));
}
std::fs::write(path, lines.join("\n"))
.map_err(|e| TailFinError::Io(format!("Cannot write cookie file: {}", e)))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_cookie_header() {
let cookies = vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string()),
];
assert_eq!(build_cookie_header(&cookies), "a=1; b=2");
}
#[test]
fn test_build_cookie_header_empty() {
let cookies: Vec<(String, String)> = vec![];
assert_eq!(build_cookie_header(&cookies), "");
}
#[test]
fn parse_empty_string_returns_empty_vec() {
assert!(parse_netscape_cookies("").is_empty());
}
#[test]
fn parse_ignores_comment_lines() {
let data = "# Netscape HTTP Cookie File\n# another comment\n";
assert!(parse_netscape_cookies(data).is_empty());
}
#[test]
fn parse_ignores_blank_lines() {
let data = "\n \n\n";
assert!(parse_netscape_cookies(data).is_empty());
}
#[test]
fn parse_valid_7_field_line() {
let data = ".example.com\tTRUE\t/\tFALSE\t0\tsession_id\tabc123";
let cookies = parse_netscape_cookies(data);
assert_eq!(cookies.len(), 1);
assert_eq!(cookies[0], ("session_id".to_string(), "abc123".to_string()));
}
#[test]
fn parse_6_field_line_gives_empty_value() {
let data = ".example.com\tTRUE\t/\tFALSE\t0\ttoken";
let cookies = parse_netscape_cookies(data);
assert_eq!(cookies.len(), 1);
assert_eq!(cookies[0], ("token".to_string(), String::new()));
}
#[test]
fn parse_skips_lines_with_fewer_than_6_fields() {
let data = "only\ttwo\tfields";
assert!(parse_netscape_cookies(data).is_empty());
}
#[test]
fn parse_multiple_cookies() {
let data = "\
# Netscape HTTP Cookie File\n\
.example.com\tTRUE\t/\tFALSE\t0\tsid\tval1\n\
\n\
.other.com\tFALSE\t/path\tTRUE\t999\ttoken\tval2\n";
let cookies = parse_netscape_cookies(data);
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].0, "sid");
assert_eq!(cookies[0].1, "val1");
assert_eq!(cookies[1].0, "token");
assert_eq!(cookies[1].1, "val2");
}
#[test]
fn write_and_read_back_roundtrip() {
let dir = std::env::temp_dir().join("tail_fin_test_cookies");
let path = dir.join("cookies.txt");
let cookies = vec![serde_json::json!({
"domain": ".example.com",
"path": "/",
"secure": true,
"expires": 1700000000.0,
"name": "sid",
"value": "abc"
})];
write_netscape_file(&path, &cookies).unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.starts_with("# Netscape HTTP Cookie File"));
assert!(contents.contains(".example.com\tTRUE\t/\tTRUE\t1700000000\tsid\tabc"));
let parsed = parse_netscape_cookies(&contents);
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0], ("sid".to_string(), "abc".to_string()));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn write_domain_without_dot_sets_flag_false() {
let dir = std::env::temp_dir().join("tail_fin_test_cookies_flag");
let path = dir.join("cookies.txt");
let cookies = vec![serde_json::json!({
"domain": "example.com",
"path": "/",
"secure": false,
"expires": 0,
"name": "tok",
"value": "x"
})];
write_netscape_file(&path, &cookies).unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains("example.com\tFALSE\t/\tFALSE\t0\ttok\tx"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn write_domain_with_dot_sets_flag_true() {
let dir = std::env::temp_dir().join("tail_fin_test_cookies_dot");
let path = dir.join("cookies.txt");
let cookies = vec![serde_json::json!({
"domain": ".example.com",
"path": "/api",
"secure": false,
"expires": 0,
"name": "k",
"value": "v"
})];
write_netscape_file(&path, &cookies).unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains(".example.com\tTRUE\t/api\tFALSE\t0\tk\tv"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn load_empty_file_returns_auth_required() {
let dir = std::env::temp_dir().join("tail_fin_test_load_empty");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("empty.txt");
std::fs::write(&path, "").unwrap();
let result = load_netscape_file(&path);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), TailFinError::AuthRequired));
let _ = std::fs::remove_dir_all(&dir);
}
}