use std::path::Path;
use crate::error::{Result, WalError};
use crate::segment::atomic_io::{atomic_write_fsync, read_checkpoint_dontneed};
const MAGIC: [u8; 4] = *b"NCKF";
const VERSION: u8 = 1;
const HEADER_LEN: usize = 17;
pub fn write_checkpoint_framed(tmp: &Path, dst: &Path, payload: &[u8]) -> Result<()> {
let crc = crc32c::crc32c(payload);
let payload_len = payload.len() as u64;
let mut framed = Vec::with_capacity(HEADER_LEN + payload.len());
framed.extend_from_slice(&MAGIC);
framed.push(VERSION);
framed.extend_from_slice(&crc.to_le_bytes());
framed.extend_from_slice(&payload_len.to_le_bytes());
framed.extend_from_slice(payload);
atomic_write_fsync(tmp, dst, &framed)
}
pub fn read_checkpoint_framed(path: &Path) -> Result<Vec<u8>> {
let bytes = read_checkpoint_dontneed(path)?;
if bytes.len() < HEADER_LEN || bytes[0..4] != MAGIC {
return Ok(bytes);
}
let version = bytes[4];
if version != VERSION {
return Err(WalError::CheckpointCorrupt {
path: path.display().to_string(),
detail: format!("unsupported checkpoint frame version {version}"),
});
}
let stored_crc = u32::from_le_bytes([bytes[5], bytes[6], bytes[7], bytes[8]]);
let payload_len = u64::from_le_bytes([
bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16],
]);
let payload = &bytes[HEADER_LEN..];
let actual_len = payload.len() as u64;
if actual_len != payload_len {
return Err(WalError::CheckpointCorrupt {
path: path.display().to_string(),
detail: format!("truncated: header len {payload_len} != actual {actual_len}"),
});
}
let computed = crc32c::crc32c(payload);
if computed != stored_crc {
return Err(WalError::CheckpointCorrupt {
path: path.display().to_string(),
detail: format!("crc mismatch: header {stored_crc:#x} != computed {computed:#x}"),
});
}
Ok(payload.to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn roundtrip() {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path().join("payload.ckpt");
let tmp = dir.path().join("payload.ckpt.tmp");
write_checkpoint_framed(&tmp, &dst, b"hello framed world").unwrap();
let read_back = read_checkpoint_framed(&dst).unwrap();
assert_eq!(read_back, b"hello framed world");
}
#[test]
fn corruption_detected() {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path().join("payload.ckpt");
let tmp = dir.path().join("payload.ckpt.tmp");
write_checkpoint_framed(&tmp, &dst, b"integrity matters").unwrap();
let mut bytes = fs::read(&dst).unwrap();
let flip_idx = HEADER_LEN + 2;
bytes[flip_idx] ^= 0xFF;
fs::write(&dst, &bytes).unwrap();
let err = read_checkpoint_framed(&dst).unwrap_err();
match err {
WalError::CheckpointCorrupt { detail, .. } => {
assert!(detail.contains("crc mismatch"), "detail was: {detail}");
}
other => panic!("expected CheckpointCorrupt, got {other:?}"),
}
}
#[test]
fn truncation_detected() {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path().join("payload.ckpt");
let tmp = dir.path().join("payload.ckpt.tmp");
write_checkpoint_framed(&tmp, &dst, b"a longer payload body here").unwrap();
let mut bytes = fs::read(&dst).unwrap();
bytes.truncate(bytes.len() - 5);
fs::write(&dst, &bytes).unwrap();
let err = read_checkpoint_framed(&dst).unwrap_err();
match err {
WalError::CheckpointCorrupt { detail, .. } => {
assert!(detail.contains("truncated"), "detail was: {detail}");
}
other => panic!("expected CheckpointCorrupt, got {other:?}"),
}
}
#[test]
fn legacy_unframed_accepted() {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path().join("payload.ckpt");
let tmp = dir.path().join("payload.ckpt.tmp");
atomic_write_fsync(&tmp, &dst, b"legacy plaintext checkpoint").unwrap();
let read_back = read_checkpoint_framed(&dst).unwrap();
assert_eq!(read_back, b"legacy plaintext checkpoint");
}
}