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 },
#[error("file too long: {len} bytes (expected 17)")]
TrailingBytes { len: usize },
}
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::TrailingBytes { len: bytes.len() });
}
let magic: [u8; 4] = read_array(bytes, 0)?;
if &magic != MAGIC {
return Err(RecordError::BadMagic(magic));
}
let version = *bytes.get(4).ok_or(RecordError::TooShort(bytes.len()))?;
if version != VERSION {
return Err(RecordError::UnsupportedVersion(version));
}
let high_water = u64::from_le_bytes(read_array(bytes, 5)?);
let crc_recorded = u32::from_le_bytes(read_array(bytes, 13)?);
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)
}
fn read_array<const N: usize>(bytes: &[u8], start: usize) -> Result<[u8; N], RecordError> {
let end = start
.checked_add(N)
.ok_or(RecordError::TooShort(bytes.len()))?;
bytes
.get(start..end)
.ok_or(RecordError::TooShort(bytes.len()))?
.try_into()
.map_err(|_| RecordError::TooShort(bytes.len()))
}
#[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))));
}
#[test]
fn detects_short_magic_slice() {
assert!(matches!(decode(b"TSO"), Err(RecordError::TooShort(3))));
}
#[test]
fn detects_short_version_byte() {
assert!(matches!(decode(MAGIC), Err(RecordError::TooShort(4))));
}
#[test]
fn detects_short_high_water_slice() {
let bytes = [MAGIC.as_slice(), &[VERSION], &[0; 7]].concat();
assert!(matches!(decode(&bytes), Err(RecordError::TooShort(12))));
}
#[test]
fn detects_short_crc_slice() {
let bytes = [MAGIC.as_slice(), &[VERSION], &[0; 8], &[0; 3]].concat();
assert!(matches!(decode(&bytes), Err(RecordError::TooShort(16))));
}
#[test]
fn detects_trailing_bytes() {
let valid = encode(42);
let mut overlong = valid.to_vec();
overlong.push(0xAB);
assert!(matches!(
decode(&overlong),
Err(RecordError::TrailingBytes { len: 18 })
));
}
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());
}
}
}