use serde::{Deserialize, Serialize};
const WINDOWS_TO_UNIX_EPOCH_SECS: i64 = 11_644_473_600;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlaywrightCookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
pub expires: f64,
#[serde(rename = "httpOnly")]
pub http_only: bool,
pub secure: bool,
#[serde(rename = "sameSite")]
pub same_site: SameSite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SameSite {
Strict,
Lax,
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OriginState {
pub origin: String,
#[serde(rename = "localStorage")]
pub local_storage: Vec<LocalStorageEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalStorageEntry {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StorageState {
pub cookies: Vec<PlaywrightCookie>,
pub origins: Vec<OriginState>,
}
impl StorageState {
#[must_use]
pub fn from_cookies(cookies: Vec<PlaywrightCookie>) -> Self {
Self {
cookies,
origins: Vec::new(),
}
}
pub fn to_json(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
}
#[must_use]
pub fn chromium_expiry_to_unix(expires_utc: i64) -> f64 {
if expires_utc <= 0 {
return -1.0;
}
let unix_secs = expires_utc / 1_000_000 - WINDOWS_TO_UNIX_EPOCH_SECS;
if unix_secs <= 0 {
-1.0
} else {
unix_secs as f64
}
}
#[must_use]
pub fn chromium_samesite_to_playwright(samesite: i64) -> SameSite {
match samesite {
0 => SameSite::None,
2 => SameSite::Strict,
_ => SameSite::Lax,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chromium_session_expiry_maps_to_minus_one() {
assert_eq!(chromium_expiry_to_unix(0), -1.0);
}
#[test]
fn chromium_negative_expiry_maps_to_minus_one() {
assert_eq!(chromium_expiry_to_unix(-5), -1.0);
}
#[test]
fn chromium_real_expiry_uses_exact_epoch_formula() {
assert_eq!(
chromium_expiry_to_unix(13_437_022_686_718_487),
1_792_549_086.0
);
}
#[test]
fn chromium_samesite_integers_map_to_exact_playwright_strings() {
assert_eq!(chromium_samesite_to_playwright(-1), SameSite::Lax); assert_eq!(chromium_samesite_to_playwright(0), SameSite::None);
assert_eq!(chromium_samesite_to_playwright(1), SameSite::Lax);
assert_eq!(chromium_samesite_to_playwright(2), SameSite::Strict);
}
#[test]
fn samesite_serializes_to_playwright_enum_strings() {
assert_eq!(
serde_json::to_string(&SameSite::Strict).unwrap(),
"\"Strict\""
);
assert_eq!(serde_json::to_string(&SameSite::Lax).unwrap(), "\"Lax\"");
assert_eq!(serde_json::to_string(&SameSite::None).unwrap(), "\"None\"");
}
#[test]
fn storage_state_serializes_with_camelcase_playwright_fields() {
let state = StorageState::from_cookies(vec![PlaywrightCookie {
name: "session".into(),
value: "abc".into(),
domain: ".example.com".into(),
path: "/".into(),
expires: -1.0,
http_only: true,
secure: true,
same_site: SameSite::Lax,
}]);
let json = state.to_json().unwrap();
assert!(json.contains("\"httpOnly\": true"), "json: {json}");
assert!(json.contains("\"sameSite\": \"Lax\""), "json: {json}");
assert!(json.contains("\"origins\": []"), "json: {json}");
}
#[test]
fn storage_state_round_trips_through_json() {
let original = StorageState::from_cookies(vec![
PlaywrightCookie {
name: "a".into(),
value: "1".into(),
domain: ".github.com".into(),
path: "/".into(),
expires: 1_792_549_086.0,
http_only: false,
secure: true,
same_site: SameSite::Strict,
},
PlaywrightCookie {
name: "a".into(), value: "2".into(),
domain: "api.github.com".into(),
path: "/v3".into(),
expires: -1.0,
http_only: true,
secure: false,
same_site: SameSite::None,
},
]);
let json = original.to_json().unwrap();
let parsed: StorageState = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, original);
assert_eq!(parsed.cookies.len(), 2);
}
}