use std::path::Path;
use crate::session::{SessionError, SessionStore};
enum LoadFailure {
Unreadable(SessionError),
Unparsable(SessionError),
}
impl LoadFailure {
fn into_error(self) -> SessionError {
match self {
Self::Unreadable(e) | Self::Unparsable(e) => e,
}
}
}
fn read_and_parse(path: &Path) -> Result<SessionStore, LoadFailure> {
if !path.exists() {
return Ok(SessionStore::default());
}
let contents = std::fs::read_to_string(path).map_err(|e| {
LoadFailure::Unreadable(SessionError(format!("Failed to read {}: {e}", path.display())))
})?;
let mut store: SessionStore = serde_json::from_str(&contents).map_err(|e| {
LoadFailure::Unparsable(SessionError(format!("Failed to parse {}: {e}", path.display())))
})?;
store.take_baseline();
Ok(store)
}
pub fn load_from(path: &Path) -> Result<SessionStore, SessionError> {
read_and_parse(path).map_err(LoadFailure::into_error)
}
pub fn reread_for_merge(path: &Path) -> Result<SessionStore, SessionError> {
match read_and_parse(path) {
Ok(store) => Ok(store),
Err(LoadFailure::Unparsable(_)) => Ok(SessionStore::default()),
Err(LoadFailure::Unreadable(e)) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_dir(label: &str) -> std::path::PathBuf {
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(1);
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir()
.join(format!("chrome-agent-session-load-{label}-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create the test's own directory");
dir
}
#[test]
fn a_present_but_unreadable_store_is_refused_rather_than_emptied() {
let dir = temp_dir("unreadable");
let path = dir.join("sessions.json");
std::fs::create_dir(&path).expect("stand in for a file that will not read");
let err = reread_for_merge(&path).expect_err("a merge may not start from a guess");
assert!(err.0.contains("Failed to read"), "{}", err.0);
assert!(err.0.contains("sessions.json"), "the file that failed is the fact: {}", err.0);
assert!(load_from(&path).is_err(), "the command-level read refuses it too");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn bug_session_corrupt_json() {
let dir = temp_dir("corrupt");
let path = dir.join("sessions.json");
std::fs::write(&path, "NOT VALID JSON {{{").unwrap();
let err = load_from(&path).expect_err("corrupt JSON should error").to_string();
assert!(err.contains("Failed to parse"), "unexpected error: {err}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn bug_session_empty_file() {
let dir = temp_dir("empty");
let path = dir.join("sessions.json");
std::fs::write(&path, "").unwrap();
let err = load_from(&path).expect_err("empty file should error").to_string();
assert!(err.contains("Failed to parse"), "unexpected error: {err}");
std::fs::remove_file(&path).unwrap();
let default = load_from(&path).expect("absent file should default");
assert!(default.browsers.is_empty());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn an_absent_store_is_empty_and_an_unparsable_one_is_replaced() {
let dir = temp_dir("absent-and-corrupt");
let path = dir.join("sessions.json");
let fresh = reread_for_merge(&path).expect("no file yet is the ordinary state");
assert!(fresh.browsers.is_empty());
std::fs::write(&path, "{ not json").expect("write the corrupted file");
let recovered = reread_for_merge(&path).expect("a corrupt store must not wedge the tool");
assert!(recovered.browsers.is_empty());
let err = load_from(&path).expect_err("a corrupt store is an error to a command");
assert!(err.0.contains("Failed to parse"), "{}", err.0);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn an_unreadable_store_fails_the_save_instead_of_replacing_it() {
use std::os::unix::fs::PermissionsExt;
let dir = temp_dir("unreadable");
let path = dir.join("sessions.json");
let mut theirs = SessionStore::default();
crate::session::ensure_browser(&mut theirs, "someone-else", "ws://theirs", None, true, None, Vec::new());
crate::session::save_to(&path, &mut theirs).unwrap();
let before = std::fs::read_to_string(&path).unwrap();
assert!(before.contains("someone-else"));
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
if std::fs::read_to_string(&path).is_ok() {
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
let _ = std::fs::remove_dir_all(&dir);
eprintln!("SKIP: this user can read a mode-0 file, so the read cannot be made to fail");
return;
}
let mut mine = SessionStore::default();
crate::session::ensure_browser(&mut mine, "mine", "ws://mine", None, true, None, Vec::new());
let err = crate::session::save_to(&path, &mut mine)
.expect_err("a save that cannot read the store must not publish over it")
.0;
assert!(err.contains("Failed to read"), "{err}");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
before,
"the other agent's entries were replaced by the view of a process that read nothing"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_absent_store_still_saves_from_empty() {
let dir = temp_dir("fresh");
let path = dir.join("sessions.json");
assert!(!path.exists());
let mut store = SessionStore::default();
crate::session::ensure_browser(&mut store, "first", "ws://first", None, true, None, Vec::new());
crate::session::save_to(&path, &mut store).expect("no file yet is the ordinary state, not a failure");
let disk = load_from(&path).unwrap();
assert!(disk.browsers.contains_key("first"));
let _ = std::fs::remove_dir_all(&dir);
}
}