pub const MAGIC: &[u8; 4] = b"TSOR";
pub const VERSION: u8 = 1;
pub const RECORD_LEN: usize = 4 + 1 + 8 + 4;
#[derive(Debug, thiserror::Error)]
pub enum RecordError {
#[error("file too short: {0} bytes (expected 17)")]
TooShort(usize),
#[error("bad magic: {0:?}")]
BadMagic([u8; 4]),
#[error("unsupported version: {0}")]
UnsupportedVersion(u8),
#[error("crc mismatch: expected {expected:#010x}, computed {computed:#010x}")]
CrcMismatch { expected: u32, computed: u32 },
}
pub fn encode(high_water: u64) -> [u8; RECORD_LEN] {
let mut buf = [0u8; RECORD_LEN];
buf[0..4].copy_from_slice(MAGIC);
buf[4] = VERSION;
buf[5..13].copy_from_slice(&high_water.to_le_bytes());
let crc = crc32c::crc32c(&buf[0..13]);
buf[13..17].copy_from_slice(&crc.to_le_bytes());
buf
}
pub fn decode(bytes: &[u8]) -> Result<u64, RecordError> {
if bytes.len() < RECORD_LEN {
return Err(RecordError::TooShort(bytes.len()));
}
let magic: [u8; 4] = bytes[0..4].try_into().unwrap();
if &magic != MAGIC {
return Err(RecordError::BadMagic(magic));
}
let version = bytes[4];
if version != VERSION {
return Err(RecordError::UnsupportedVersion(version));
}
let high_water = u64::from_le_bytes(bytes[5..13].try_into().unwrap());
let crc_recorded = u32::from_le_bytes(bytes[13..17].try_into().unwrap());
let crc_computed = crc32c::crc32c(&bytes[0..13]);
if crc_recorded != crc_computed {
return Err(RecordError::CrcMismatch {
expected: crc_recorded,
computed: crc_computed,
});
}
Ok(high_water)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip() {
for v in [0u64, 1, 1_700_000_000_000, u64::MAX >> 18] {
let buf = encode(v);
assert_eq!(decode(&buf).unwrap(), v);
}
}
#[test]
fn detects_bad_magic() {
let mut buf = encode(42);
buf[0] = b'X';
assert!(matches!(decode(&buf), Err(RecordError::BadMagic(_))));
}
#[test]
fn detects_bad_crc() {
let mut buf = encode(42);
buf[5] ^= 0x01;
assert!(matches!(decode(&buf), Err(RecordError::CrcMismatch { .. })));
}
#[test]
fn detects_short() {
let buf = encode(42);
assert!(matches!(decode(&buf[..10]), Err(RecordError::TooShort(10))));
}
use proptest::prelude::*;
proptest! {
#[test]
fn encode_decode_roundtrip(high_water in any::<u64>()) {
let encoded = encode(high_water);
prop_assert_eq!(decode(&encoded).unwrap(), high_water);
}
#[test]
fn any_single_bit_flip_is_detected(
high_water in any::<u64>(),
byte_index in 0usize..RECORD_LEN,
bit_index in 0u8..8,
) {
let original = encode(high_water);
let mut corrupted = original;
corrupted[byte_index] ^= 1 << bit_index;
prop_assert_ne!(corrupted, original);
match decode(&corrupted) {
Err(_) => {} Ok(decoded) => prop_assert_ne!(decoded, high_water),
}
}
#[test]
fn well_formed_record_is_never_rejected(high_water in any::<u64>()) {
let encoded = encode(high_water);
prop_assert!(decode(&encoded).is_ok());
}
}
}