use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use crate::constants::paths::SESSION_FILENAME;
use crate::contracts::SessionState;
use crate::fsutil;
use crate::git::error::git_head_commit;
const SESSION_QUARANTINE_DIR: &str = "session-quarantine";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionCacheCorruption {
pub path: PathBuf,
pub diagnostic: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionQuarantineResult {
pub original_path: PathBuf,
pub quarantine_path: PathBuf,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionLoadResult {
Missing,
Loaded(SessionState),
Corrupt(SessionCacheCorruption),
}
fn session_cache_corruption(
path: &Path,
context: &str,
err: &(dyn std::error::Error + 'static),
) -> SessionCacheCorruption {
SessionCacheCorruption {
path: path.to_path_buf(),
diagnostic: crate::redaction::redact_text(&format!("{context}: {err:#}")),
}
}
pub fn session_path(cache_dir: &Path) -> PathBuf {
cache_dir.join(SESSION_FILENAME)
}
pub fn session_exists(cache_dir: &Path) -> bool {
session_path(cache_dir).exists()
}
pub fn save_session(cache_dir: &Path, session: &SessionState) -> Result<()> {
let path = session_path(cache_dir);
let json = serde_json::to_string_pretty(session).context("serialize session state")?;
fsutil::write_atomic(&path, json.as_bytes()).context("write session file")?;
log::debug!("Session saved: task_id={}", session.task_id);
Ok(())
}
pub fn load_session_checked(cache_dir: &Path) -> SessionLoadResult {
let path = session_path(cache_dir);
match path.try_exists() {
Ok(true) => {}
Ok(false) => return SessionLoadResult::Missing,
Err(err) => {
return SessionLoadResult::Corrupt(session_cache_corruption(
&path,
"inspect session file",
&err,
));
}
}
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(err) => {
return SessionLoadResult::Corrupt(session_cache_corruption(
&path,
"read session file",
&err,
));
}
};
match serde_json::from_str::<SessionState>(&content) {
Ok(session) => {
if session.version > crate::contracts::SESSION_STATE_VERSION {
log::warn!(
"Session file version {} is newer than supported version {}. Attempting to load anyway.",
session.version,
crate::contracts::SESSION_STATE_VERSION
);
}
SessionLoadResult::Loaded(session)
}
Err(err) => {
SessionLoadResult::Corrupt(session_cache_corruption(&path, "parse session file", &err))
}
}
}
pub fn load_session(cache_dir: &Path) -> Result<Option<SessionState>> {
match load_session_checked(cache_dir) {
SessionLoadResult::Missing => Ok(None),
SessionLoadResult::Loaded(session) => Ok(Some(session)),
SessionLoadResult::Corrupt(corruption) => {
bail!("{}: {}", corruption.path.display(), corruption.diagnostic)
}
}
}
pub fn quarantine_session_cache(cache_dir: &Path) -> Result<Option<SessionQuarantineResult>> {
let original_path = session_path(cache_dir);
if !original_path.exists() {
return Ok(None);
}
let quarantine_dir = cache_dir.join(SESSION_QUARANTINE_DIR);
std::fs::create_dir_all(&quarantine_dir).with_context(|| {
format!(
"create session quarantine directory {}",
quarantine_dir.display()
)
})?;
let timestamp = crate::timeutil::now_utc_rfc3339_or_fallback().replace([':', '.'], "-");
let quarantine_path = quarantine_dir.join(format!("session.jsonc.corrupt.{timestamp}"));
match std::fs::rename(&original_path, &quarantine_path) {
Ok(()) => {}
Err(rename_err) => {
std::fs::copy(&original_path, &quarantine_path).with_context(|| {
format!("quarantine session cache to {}", quarantine_path.display())
})?;
std::fs::remove_file(&original_path).with_context(|| {
format!(
"remove quarantined session cache {}",
original_path.display()
)
})?;
log::debug!(
"session cache rename failed during quarantine; used copy/remove fallback: {rename_err}"
);
}
}
Ok(Some(SessionQuarantineResult {
original_path,
quarantine_path,
}))
}
pub fn clear_session(cache_dir: &Path) -> Result<()> {
let path = session_path(cache_dir);
if path.exists() {
std::fs::remove_file(&path).context("remove session file")?;
log::debug!("Session cleared");
}
Ok(())
}
pub fn get_git_head_commit(repo_root: &Path) -> Option<String> {
git_head_commit(repo_root).ok()
}