use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum CorruptionPolicy {
#[default]
Quarantine,
Fail,
}
#[derive(Debug, Clone)]
pub struct CorruptionError(pub String);
impl std::fmt::Display for CorruptionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[must_use]
pub fn frame(crc: bool, body: Vec<u8>) -> Vec<u8> {
if !crc {
return body;
}
let sum = crc32c::crc32c(&body);
let mut out = Vec::with_capacity(body.len() + 4);
out.extend_from_slice(&sum.to_le_bytes());
out.extend_from_slice(&body);
out
}
pub fn unframe(crc: bool, raw: Vec<u8>) -> Result<Vec<u8>, CorruptionError> {
if !crc {
return Ok(raw);
}
if raw.len() < 4 {
return Err(CorruptionError(
"record shorter than the 4-byte CRC header".to_string(),
));
}
let (header, body) = raw.split_at(4);
let expected = u32::from_le_bytes(header.try_into().unwrap_or([0; 4]));
let actual = crc32c::crc32c(body);
if expected != actual {
return Err(CorruptionError(format!(
"CRC32C mismatch (expected {expected:08x}, computed {actual:08x}) -- torn write or bit-rot"
)));
}
Ok(body.to_vec())
}
pub fn quarantine_dir(path: &Path) -> std::io::Result<Option<PathBuf>> {
if !path.exists() {
return Ok(None);
}
let stamp = chrono::Local::now().format("%Y%m%d-%H%M%S");
let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("spool");
let dest = path.with_file_name(format!("{name}.corrupt-{stamp}"));
std::fs::rename(path, &dest)?;
Ok(Some(dest))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_unframe_round_trips() {
let body = b"the payload bytes".to_vec();
let framed = frame(true, body.clone());
assert_eq!(framed.len(), body.len() + 4, "4-byte CRC header prepended");
assert_eq!(unframe(true, framed).unwrap(), body);
}
#[test]
fn frame_off_is_identity() {
let body = b"x".to_vec();
assert_eq!(frame(false, body.clone()), body);
assert_eq!(unframe(false, body.clone()).unwrap(), body);
}
#[test]
fn unframe_detects_corruption() {
let mut framed = frame(true, b"original".to_vec());
let last = framed.len() - 1;
framed[last] ^= 0xFF; assert!(unframe(true, framed).is_err());
assert!(unframe(true, vec![1, 2]).is_err());
}
}