1#![forbid(unsafe_code)]
2
3#[must_use]
5pub fn crate_id() -> &'static str {
6 "wombatkv-format"
7}
8
9const WAL_MAGIC: &[u8; 12] = b"WMBT_KV_WAL1";
10const WAL_HEADER_SIZE: usize = 24;
11const SEGMENT_FOOTER_MAGIC: &[u8; 12] = b"WMBT_KV_SEG1";
12const SEGMENT_FOOTER_SIZE: usize = 20;
13const SEGMENT_MAGIC: &[u8; 12] = b"WMBT_KV_SGV1";
14const SEGMENT_HEADER_SIZE: usize = 16;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct WalRecord {
19 pub key: Vec<u8>,
20 pub meta: Vec<u8>,
21 pub payload: Vec<u8>,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum FormatError {
27 InvalidMagic,
28 UnexpectedEof,
29 ChecksumMismatch,
30 FieldTooLarge,
31}
32
33pub fn encode_wal_record(record: &WalRecord) -> Result<Vec<u8>, FormatError> {
35 let key_len = u16::try_from(record.key.len()).map_err(|_| FormatError::FieldTooLarge)?;
36 let meta_len = u16::try_from(record.meta.len()).map_err(|_| FormatError::FieldTooLarge)?;
37 let payload_len =
38 u32::try_from(record.payload.len()).map_err(|_| FormatError::FieldTooLarge)?;
39
40 let mut body = Vec::with_capacity(
41 usize::from(key_len) + usize::from(meta_len) + usize::try_from(payload_len).unwrap_or(0),
42 );
43 body.extend_from_slice(&record.key);
44 body.extend_from_slice(&record.meta);
45 body.extend_from_slice(&record.payload);
46
47 let checksum = checksum32(&body);
48
49 let mut out = Vec::with_capacity(WAL_HEADER_SIZE + body.len());
50 out.extend_from_slice(WAL_MAGIC);
51 out.extend_from_slice(&key_len.to_le_bytes());
52 out.extend_from_slice(&meta_len.to_le_bytes());
53 out.extend_from_slice(&payload_len.to_le_bytes());
54 out.extend_from_slice(&checksum.to_le_bytes());
55 out.extend_from_slice(&body);
56 Ok(out)
57}
58
59pub fn decode_wal_record(data: &[u8]) -> Result<WalRecord, FormatError> {
61 if data.len() < WAL_HEADER_SIZE {
62 return Err(FormatError::UnexpectedEof);
63 }
64
65 if &data[..WAL_MAGIC.len()] != WAL_MAGIC {
66 return Err(FormatError::InvalidMagic);
67 }
68
69 let key_len = usize::from(u16::from_le_bytes([data[12], data[13]]));
70 let meta_len = usize::from(u16::from_le_bytes([data[14], data[15]]));
71 let payload_len_u32 = u32::from_le_bytes([data[16], data[17], data[18], data[19]]);
72 let payload_len = usize::try_from(payload_len_u32).map_err(|_| FormatError::FieldTooLarge)?;
73 let checksum = u32::from_le_bytes([data[20], data[21], data[22], data[23]]);
74
75 let body_len = key_len
76 .checked_add(meta_len)
77 .and_then(|x| x.checked_add(payload_len))
78 .ok_or(FormatError::FieldTooLarge)?;
79
80 let total_len = WAL_HEADER_SIZE.checked_add(body_len).ok_or(FormatError::FieldTooLarge)?;
81 if data.len() < total_len {
82 return Err(FormatError::UnexpectedEof);
83 }
84
85 let body = &data[WAL_HEADER_SIZE..total_len];
86 if checksum32(body) != checksum {
87 return Err(FormatError::ChecksumMismatch);
88 }
89
90 let key_end = key_len;
91 let meta_end = key_end + meta_len;
92 Ok(WalRecord {
93 key: body[..key_end].to_vec(),
94 meta: body[key_end..meta_end].to_vec(),
95 payload: body[meta_end..].to_vec(),
96 })
97}
98
99#[must_use]
101pub fn checksum32(bytes: &[u8]) -> u32 {
102 let mut state = 0x811c_9dc5_u32;
103 for byte in bytes {
104 state ^= u32::from(*byte);
105 state = state.wrapping_mul(16_777_619);
106 }
107 state
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub struct SegmentFooter {
113 pub index_len: u32,
114 pub index_checksum: u32,
115}
116
117#[must_use]
119pub fn encode_segment_footer(footer: SegmentFooter) -> [u8; SEGMENT_FOOTER_SIZE] {
120 let mut out = [0_u8; SEGMENT_FOOTER_SIZE];
121 out[..12].copy_from_slice(SEGMENT_FOOTER_MAGIC);
122 out[12..16].copy_from_slice(&footer.index_len.to_le_bytes());
123 out[16..20].copy_from_slice(&footer.index_checksum.to_le_bytes());
124 out
125}
126
127pub fn decode_segment_footer(
129 footer_bytes: &[u8],
130 index_bytes: &[u8],
131) -> Result<SegmentFooter, FormatError> {
132 if footer_bytes.len() < SEGMENT_FOOTER_SIZE {
133 return Err(FormatError::UnexpectedEof);
134 }
135
136 if &footer_bytes[..12] != SEGMENT_FOOTER_MAGIC {
137 return Err(FormatError::InvalidMagic);
138 }
139
140 let index_len = u32::from_le_bytes([
141 footer_bytes[12],
142 footer_bytes[13],
143 footer_bytes[14],
144 footer_bytes[15],
145 ]);
146 let index_checksum = u32::from_le_bytes([
147 footer_bytes[16],
148 footer_bytes[17],
149 footer_bytes[18],
150 footer_bytes[19],
151 ]);
152
153 if usize::try_from(index_len).map_err(|_| FormatError::FieldTooLarge)? != index_bytes.len() {
154 return Err(FormatError::UnexpectedEof);
155 }
156
157 if checksum32(index_bytes) != index_checksum {
158 return Err(FormatError::ChecksumMismatch);
159 }
160
161 Ok(SegmentFooter { index_len, index_checksum })
162}
163
164#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct SegmentIndexEntry {
167 pub key: Vec<u8>,
168 pub payload_offset: u32,
169 pub payload_len: u32,
170}
171
172pub fn encode_segment_v1(blocks: &[(Vec<u8>, Vec<u8>)]) -> Result<Vec<u8>, FormatError> {
174 let block_count = u32::try_from(blocks.len()).map_err(|_| FormatError::FieldTooLarge)?;
175 let mut out = Vec::new();
176 out.extend_from_slice(SEGMENT_MAGIC);
177 out.extend_from_slice(&block_count.to_le_bytes());
178
179 let mut index_entries = Vec::with_capacity(blocks.len());
180 for (key, payload) in blocks {
181 let key_len = u16::try_from(key.len()).map_err(|_| FormatError::FieldTooLarge)?;
182 let payload_len = u32::try_from(payload.len()).map_err(|_| FormatError::FieldTooLarge)?;
183 let payload_offset = u32::try_from(out.len() + 2 + 4 + usize::from(key_len))
184 .map_err(|_| FormatError::FieldTooLarge)?;
185 out.extend_from_slice(&key_len.to_le_bytes());
186 out.extend_from_slice(&payload_len.to_le_bytes());
187 out.extend_from_slice(key);
188 out.extend_from_slice(payload);
189 index_entries.push(SegmentIndexEntry { key: key.clone(), payload_offset, payload_len });
190 }
191
192 let index_bytes = encode_segment_index(&index_entries)?;
193 let footer = encode_segment_footer(SegmentFooter {
194 index_len: u32::try_from(index_bytes.len()).map_err(|_| FormatError::FieldTooLarge)?,
195 index_checksum: checksum32(&index_bytes),
196 });
197 out.extend_from_slice(&index_bytes);
198 out.extend_from_slice(&footer);
199 Ok(out)
200}
201
202pub fn decode_segment_index(segment: &[u8]) -> Result<Vec<SegmentIndexEntry>, FormatError> {
204 if segment.len() < SEGMENT_HEADER_SIZE + SEGMENT_FOOTER_SIZE {
205 return Err(FormatError::UnexpectedEof);
206 }
207 if &segment[..SEGMENT_MAGIC.len()] != SEGMENT_MAGIC {
208 return Err(FormatError::InvalidMagic);
209 }
210
211 let footer_start =
212 segment.len().checked_sub(SEGMENT_FOOTER_SIZE).ok_or(FormatError::UnexpectedEof)?;
213 let footer_bytes = &segment[footer_start..];
214 let index_len_u32 = u32::from_le_bytes([
215 footer_bytes[12],
216 footer_bytes[13],
217 footer_bytes[14],
218 footer_bytes[15],
219 ]);
220 let index_len = usize::try_from(index_len_u32).map_err(|_| FormatError::FieldTooLarge)?;
221 let index_start = footer_start.checked_sub(index_len).ok_or(FormatError::UnexpectedEof)?;
222 let index_bytes = &segment[index_start..footer_start];
223 let _ = decode_segment_footer(footer_bytes, index_bytes)?;
224 parse_segment_index(index_bytes)
225}
226
227fn encode_segment_index(entries: &[SegmentIndexEntry]) -> Result<Vec<u8>, FormatError> {
228 let mut out = Vec::new();
229 for entry in entries {
230 let key_len = u16::try_from(entry.key.len()).map_err(|_| FormatError::FieldTooLarge)?;
231 out.extend_from_slice(&key_len.to_le_bytes());
232 out.extend_from_slice(&entry.key);
233 out.extend_from_slice(&entry.payload_offset.to_le_bytes());
234 out.extend_from_slice(&entry.payload_len.to_le_bytes());
235 }
236 Ok(out)
237}
238
239fn parse_segment_index(index_bytes: &[u8]) -> Result<Vec<SegmentIndexEntry>, FormatError> {
240 let mut cursor = 0_usize;
241 let mut entries = Vec::new();
242 while cursor < index_bytes.len() {
243 if index_bytes.len().saturating_sub(cursor) < 2 {
244 return Err(FormatError::UnexpectedEof);
245 }
246 let key_len =
247 usize::from(u16::from_le_bytes([index_bytes[cursor], index_bytes[cursor + 1]]));
248 cursor = cursor.saturating_add(2);
249 if index_bytes.len().saturating_sub(cursor) < key_len + 8 {
250 return Err(FormatError::UnexpectedEof);
251 }
252 let key = index_bytes[cursor..cursor + key_len].to_vec();
253 cursor = cursor.saturating_add(key_len);
254 let payload_offset = u32::from_le_bytes([
255 index_bytes[cursor],
256 index_bytes[cursor + 1],
257 index_bytes[cursor + 2],
258 index_bytes[cursor + 3],
259 ]);
260 cursor = cursor.saturating_add(4);
261 let payload_len = u32::from_le_bytes([
262 index_bytes[cursor],
263 index_bytes[cursor + 1],
264 index_bytes[cursor + 2],
265 index_bytes[cursor + 3],
266 ]);
267 cursor = cursor.saturating_add(4);
268 entries.push(SegmentIndexEntry { key, payload_offset, payload_len });
269 }
270 Ok(entries)
271}
272
273#[cfg(test)]
274mod tests {
275 use super::{
276 checksum32, decode_segment_footer, decode_segment_index, decode_wal_record,
277 encode_segment_footer, encode_segment_v1, encode_wal_record, FormatError, SegmentFooter,
278 WalRecord,
279 };
280
281 #[test]
282 fn crate_id_is_stable() {
283 assert_eq!(super::crate_id(), "wombatkv-format");
284 }
285
286 #[test]
287 fn wal_record_round_trip_is_lossless() {
288 let record = WalRecord {
289 key: b"k/tenant/f/hash".to_vec(),
290 meta: b"{\"epoch\":1700}".to_vec(),
291 payload: vec![1, 2, 3, 4, 5, 6],
292 };
293 let encoded = encode_wal_record(&record).expect("encode");
294 let decoded = decode_wal_record(&encoded).expect("decode");
295 assert_eq!(decoded, record);
296 }
297
298 #[test]
299 fn wal_checksum_mismatch_is_rejected() {
300 let record = WalRecord {
301 key: b"key".to_vec(),
302 meta: b"meta".to_vec(),
303 payload: b"payload".to_vec(),
304 };
305 let mut encoded = encode_wal_record(&record).expect("encode");
306 let last = encoded.len() - 1;
307 encoded[last] ^= 0xFF;
308 assert_eq!(decode_wal_record(&encoded), Err(FormatError::ChecksumMismatch));
309 }
310
311 #[test]
312 fn wal_golden_vector_v1_stable() {
313 let record = WalRecord {
314 key: vec![0x6B], meta: vec![0x6D], payload: vec![0x70, 0x71, 0x72], };
318 let encoded = encode_wal_record(&record).expect("encode");
319 let expected = include_bytes!("../tests/golden/wal_v1_record.bin");
320 assert_eq!(encoded, expected);
321 }
322
323 #[test]
324 fn segment_footer_round_trip_with_checksum_validation() {
325 let index_bytes = b"index-v1".to_vec();
326 let index_len = u32::try_from(index_bytes.len()).expect("small test index");
327 let footer = SegmentFooter { index_len, index_checksum: checksum32(&index_bytes) };
328 let encoded = encode_segment_footer(footer);
329 let decoded = decode_segment_footer(&encoded, &index_bytes).expect("decode footer");
330 assert_eq!(decoded, footer);
331 }
332
333 #[test]
334 fn segment_footer_rejects_corrupt_index_checksum() {
335 let index_bytes = b"index-v1".to_vec();
336 let index_len = u32::try_from(index_bytes.len()).expect("small test index");
337 let footer = SegmentFooter { index_len, index_checksum: checksum32(&index_bytes) };
338 let encoded = encode_segment_footer(footer);
339 let mut corrupted_index = index_bytes.clone();
340 corrupted_index[0] ^= 0xFF;
341 assert_eq!(
342 decode_segment_footer(&encoded, &corrupted_index),
343 Err(FormatError::ChecksumMismatch)
344 );
345 }
346
347 #[test]
348 fn segment_footer_rejects_unknown_magic_for_backward_compat() {
349 let index_bytes = b"index-v1".to_vec();
350 let index_len = u32::try_from(index_bytes.len()).expect("small test index");
351 let mut encoded = encode_segment_footer(SegmentFooter {
352 index_len,
353 index_checksum: checksum32(&index_bytes),
354 });
355 encoded[0] = b'X';
356 assert_eq!(decode_segment_footer(&encoded, &index_bytes), Err(FormatError::InvalidMagic));
357 }
358
359 #[test]
360 fn segment_v1_encode_decode_index_round_trip() {
361 let segment = encode_segment_v1(&[
362 (b"a".to_vec(), b"payload-a".to_vec()),
363 (b"b".to_vec(), b"payload-b".to_vec()),
364 (b"c".to_vec(), b"payload-c".to_vec()),
365 ])
366 .expect("encode segment");
367 let index = decode_segment_index(&segment).expect("decode index");
368 assert_eq!(index.len(), 3);
369 assert_eq!(index[0].key, b"a".to_vec());
370 assert_eq!(index[1].key, b"b".to_vec());
371 assert_eq!(index[2].key, b"c".to_vec());
372 }
373}