#![allow(missing_docs)]
use serde::{Serialize, de::DeserializeOwned};
const CODEC_MAGIC: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
const CODEC_VERSION_V2: u8 = 2;
#[derive(Debug)]
pub struct CodecError(pub String);
impl std::fmt::Display for CodecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "codec error: {}", self.0)
}
}
impl std::error::Error for CodecError {}
pub fn persist_encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
let payload = bincode::serde::encode_to_vec(value, bincode::config::standard())
.map_err(|e| CodecError(format!("bincode-2 encode: {e}")))?;
let mut out = Vec::with_capacity(5 + payload.len());
out.extend_from_slice(&CODEC_MAGIC);
out.push(CODEC_VERSION_V2);
out.extend_from_slice(&payload);
Ok(out)
}
pub fn persist_decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
if bytes.len() >= 5 && bytes.get(..4).is_some_and(|s| s == CODEC_MAGIC) {
let version = bytes.get(4).copied().unwrap_or(0);
match version {
CODEC_VERSION_V2 => {
let (value, _consumed) = bincode::serde::decode_from_slice::<T, _>(
bytes.get(5..).unwrap_or_default(),
bincode::config::standard(),
)
.map_err(|e| CodecError(format!("bincode-2 decode: {e}")))?;
Ok(value)
}
other => Err(CodecError(format!(
"unknown codec version byte: 0x{other:02x} (expected 0x{CODEC_VERSION_V2:02x})"
))),
}
} else {
bincode_legacy::deserialize(bytes)
.map_err(|e| CodecError(format!("bincode-1 legacy decode: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Sample {
cycle_index: u64,
actor: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct SampleExtended {
cycle_index: u64,
actor: String,
#[serde(default)]
new_field: Option<String>,
}
#[test]
fn encode_emits_magic_prefix_and_version() {
let s = Sample {
cycle_index: 42,
actor: "alice".into(),
};
let bytes = persist_encode(&s).unwrap();
assert_eq!(&bytes[..4], &CODEC_MAGIC, "first 4 bytes must be magic");
assert_eq!(bytes[4], CODEC_VERSION_V2, "5th byte must be version 2");
assert!(bytes.len() > 5, "must carry payload after header");
}
#[test]
fn legacy_bincode_1_bytes_decode_via_legacy_path() {
let s = Sample {
cycle_index: 1,
actor: "bob".into(),
};
let legacy = bincode_legacy::serialize(&s).unwrap();
assert_ne!(&legacy[..4.min(legacy.len())], &CODEC_MAGIC);
let decoded: Sample = persist_decode(&legacy).expect("legacy path must decode");
assert_eq!(s, decoded);
}
#[test]
fn collision_class_legacy_bytes_first_byte_0x02_decode_via_legacy_path() {
let s = Sample {
cycle_index: 2, actor: "collision".into(),
};
let legacy = bincode_legacy::serialize(&s).unwrap();
assert_eq!(legacy[0], 0x02, "v1.1-defect collision class: first byte must be 0x02");
let decoded: Sample = persist_decode(&legacy)
.expect("collision-class legacy bytes must decode via legacy path under v1.2");
assert_eq!(s, decoded);
}
#[test]
fn round_trip_via_wrapper() {
let s = Sample {
cycle_index: 12345,
actor: "round-trip".into(),
};
let bytes = persist_encode(&s).unwrap();
let decoded: Sample = persist_decode(&bytes).unwrap();
assert_eq!(s, decoded);
}
#[test]
fn trailing_additive_via_v2_not_supported() {
let s = Sample {
cycle_index: 100,
actor: "trailing".into(),
};
let bytes = persist_encode(&s).unwrap();
let result: Result<SampleExtended, _> = persist_decode(&bytes);
assert!(
result.is_err(),
"bincode-2 serde adapter does NOT support trailing-additive: \
#[serde(default)] is not applied before UnexpectedEnd; \
per-table migration required"
);
}
#[test]
fn empty_bytes_error() {
let result: Result<Sample, _> = persist_decode(&[]);
assert!(result.is_err());
}
#[test]
fn unknown_version_byte_error() {
let bytes = [0xFF, 0xFF, 0xFF, 0xFF, 0x99, 0x00, 0x00, 0x00];
let result: Result<Sample, _> = persist_decode(&bytes);
assert!(result.is_err());
}
}