use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::core::observer::Observe;
use crate::core::state::State;
use crate::core::termination::TerminationReason;
const MAGIC: &[u8; 8] = b"BASINST\0";
const FORMAT_VERSION: u32 = 1;
#[derive(Clone, Debug)]
pub struct CheckpointWriter {
path: PathBuf,
}
impl CheckpointWriter {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
fn write<S: Serialize>(&self, state: &S) -> io::Result<()> {
let payload = postcard::to_allocvec(state).map_err(io::Error::other)?;
let mut bytes =
Vec::with_capacity(MAGIC.len() + size_of::<u32>() + payload.len());
bytes.extend_from_slice(MAGIC);
bytes.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
bytes.extend_from_slice(&payload);
let tmp = self.path.with_extension("tmp");
fs::write(&tmp, &bytes)?;
fs::rename(&tmp, &self.path)
}
fn try_write<S: Serialize>(&self, state: &S) {
if let Err(err) = self.write(state) {
eprintln!(
"CheckpointWriter: failed to write {}: {err}",
self.path.display()
);
}
}
}
impl<S> Observe<S> for CheckpointWriter
where
S: State + Serialize,
{
fn observe_iter(&mut self, state: &S) {
self.try_write(state);
}
fn observe_final(&mut self, state: &S, _reason: &TerminationReason) {
self.try_write(state);
}
}
pub fn read_checkpoint<S: DeserializeOwned>(
path: impl AsRef<Path>,
) -> io::Result<S> {
let bytes = fs::read(path)?;
if let Some(body) = bytes.strip_prefix(MAGIC) {
let invalid_data =
|message: &str| io::Error::new(io::ErrorKind::InvalidData, message);
if body.len() < size_of::<u32>() {
return Err(invalid_data("truncated state checkpoint prefix"));
}
let version = u32::from_le_bytes(
body[..size_of::<u32>()]
.try_into()
.expect("version slice has fixed length"),
);
if version != FORMAT_VERSION {
return Err(invalid_data(&format!(
"unsupported state checkpoint format version {version}; expected {FORMAT_VERSION}"
)));
}
let (state, remaining) =
postcard::take_from_bytes(&body[size_of::<u32>()..])
.map_err(|error| invalid_data(&error.to_string()))?;
if !remaining.is_empty() {
return Err(invalid_data("trailing data in state checkpoint"));
}
return Ok(state);
}
let (state, _) =
bincode::serde::decode_from_slice(&bytes, bincode::config::standard())
.map_err(io::Error::other)?;
Ok(state)
}