tsoracle_driver_file/
record.rs1pub const MAGIC: &[u8; 4] = b"TSOR";
27pub const VERSION: u8 = 1;
28pub const RECORD_LEN: usize = 4 + 1 + 8 + 4;
29
30#[derive(Debug, thiserror::Error)]
31pub enum RecordError {
32 #[error("file too short: {0} bytes (expected 17)")]
33 TooShort(usize),
34 #[error("bad magic: {0:?}")]
35 BadMagic([u8; 4]),
36 #[error("unsupported version: {0}")]
37 UnsupportedVersion(u8),
38 #[error("crc mismatch: expected {expected:#010x}, computed {computed:#010x}")]
39 CrcMismatch { expected: u32, computed: u32 },
40 #[error("file too long: {len} bytes (expected 17)")]
41 TrailingBytes { len: usize },
42}
43
44pub fn encode(high_water: u64) -> [u8; RECORD_LEN] {
45 let mut buf = [0u8; RECORD_LEN];
46 buf[0..4].copy_from_slice(MAGIC);
47 buf[4] = VERSION;
48 buf[5..13].copy_from_slice(&high_water.to_le_bytes());
49 let crc = crc32c::crc32c(&buf[0..13]);
50 buf[13..17].copy_from_slice(&crc.to_le_bytes());
51 buf
52}
53
54pub fn decode(bytes: &[u8]) -> Result<u64, RecordError> {
55 if bytes.len() > RECORD_LEN {
56 return Err(RecordError::TrailingBytes { len: bytes.len() });
57 }
58 let magic: [u8; 4] = read_array(bytes, 0)?;
59 if &magic != MAGIC {
60 return Err(RecordError::BadMagic(magic));
61 }
62 let version = *bytes.get(4).ok_or(RecordError::TooShort(bytes.len()))?;
63 if version != VERSION {
64 return Err(RecordError::UnsupportedVersion(version));
65 }
66 let high_water = u64::from_le_bytes(read_array(bytes, 5)?);
67 let crc_recorded = u32::from_le_bytes(read_array(bytes, 13)?);
68 let crc_computed = crc32c::crc32c(&bytes[0..13]);
69 if crc_recorded != crc_computed {
70 return Err(RecordError::CrcMismatch {
71 expected: crc_recorded,
72 computed: crc_computed,
73 });
74 }
75 Ok(high_water)
76}
77
78fn read_array<const N: usize>(bytes: &[u8], start: usize) -> Result<[u8; N], RecordError> {
79 let end = start
80 .checked_add(N)
81 .ok_or(RecordError::TooShort(bytes.len()))?;
82 bytes
83 .get(start..end)
84 .ok_or(RecordError::TooShort(bytes.len()))?
85 .try_into()
86 .map_err(|_| RecordError::TooShort(bytes.len()))
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn roundtrip() {
95 for v in [0u64, 1, 1_700_000_000_000, u64::MAX >> 18] {
96 let buf = encode(v);
97 assert_eq!(decode(&buf).unwrap(), v);
98 }
99 }
100
101 #[test]
102 fn detects_bad_magic() {
103 let mut buf = encode(42);
104 buf[0] = b'X';
105 assert!(matches!(decode(&buf), Err(RecordError::BadMagic(_))));
106 }
107
108 #[test]
109 fn detects_bad_crc() {
110 let mut buf = encode(42);
111 buf[5] ^= 0x01;
112 assert!(matches!(decode(&buf), Err(RecordError::CrcMismatch { .. })));
113 }
114
115 #[test]
116 fn detects_short() {
117 let buf = encode(42);
118 assert!(matches!(decode(&buf[..10]), Err(RecordError::TooShort(10))));
119 }
120
121 #[test]
122 fn detects_short_magic_slice() {
123 assert!(matches!(decode(b"TSO"), Err(RecordError::TooShort(3))));
124 }
125
126 #[test]
127 fn detects_short_version_byte() {
128 assert!(matches!(decode(MAGIC), Err(RecordError::TooShort(4))));
129 }
130
131 #[test]
132 fn detects_short_high_water_slice() {
133 let bytes = [MAGIC.as_slice(), &[VERSION], &[0; 7]].concat();
134 assert!(matches!(decode(&bytes), Err(RecordError::TooShort(12))));
135 }
136
137 #[test]
138 fn detects_short_crc_slice() {
139 let bytes = [MAGIC.as_slice(), &[VERSION], &[0; 8], &[0; 3]].concat();
140 assert!(matches!(decode(&bytes), Err(RecordError::TooShort(16))));
141 }
142
143 #[test]
144 fn detects_trailing_bytes() {
145 let valid = encode(42);
146 let mut overlong = valid.to_vec();
147 overlong.push(0xAB);
148 assert!(matches!(
149 decode(&overlong),
150 Err(RecordError::TrailingBytes { len: 18 })
151 ));
152 }
153
154 use proptest::prelude::*;
155
156 proptest! {
157 #[test]
161 fn encode_decode_roundtrip(high_water in any::<u64>()) {
162 let encoded = encode(high_water);
163 prop_assert_eq!(decode(&encoded).unwrap(), high_water);
164 }
165
166 #[test]
174 fn any_single_bit_flip_is_detected(
175 high_water in any::<u64>(),
176 byte_index in 0usize..RECORD_LEN,
177 bit_index in 0u8..8,
178 ) {
179 let original = encode(high_water);
180 let mut corrupted = original;
181 corrupted[byte_index] ^= 1 << bit_index;
182 prop_assert_ne!(corrupted, original);
183 match decode(&corrupted) {
184 Err(_) => {} Ok(decoded) => prop_assert_ne!(decoded, high_water),
186 }
187 }
188
189 #[test]
193 fn well_formed_record_is_never_rejected(high_water in any::<u64>()) {
194 let encoded = encode(high_water);
195 prop_assert!(decode(&encoded).is_ok());
196 }
197 }
198}