use std::collections::BTreeMap;
use sha2::{Digest, Sha256};
use crate::persist::{FlushBatch, RecordKey};
const HASH_LEN: usize = 32;
fn write_u32(out: &mut Vec<u8>, value: u32) {
out.extend_from_slice(&value.to_le_bytes());
}
fn write_u64(out: &mut Vec<u8>, value: u64) {
out.extend_from_slice(&value.to_le_bytes());
}
fn write_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
write_u32(out, bytes.len() as u32);
out.extend_from_slice(bytes);
}
fn encode_payload(batch_id: u64, records: &[(&str, &[u8])], deletes: &[&str]) -> Vec<u8> {
let mut payload = Vec::new();
write_u64(&mut payload, batch_id);
write_u32(&mut payload, records.len() as u32);
for (key, bytes) in records {
write_bytes(&mut payload, key.as_bytes());
write_bytes(&mut payload, bytes);
}
write_u32(&mut payload, deletes.len() as u32);
for key in deletes {
write_bytes(&mut payload, key.as_bytes());
}
payload
}
fn frame_from_payload(payload: Vec<u8>) -> Vec<u8> {
let hash = Sha256::digest(&payload);
let mut frame = Vec::with_capacity(4 + HASH_LEN + payload.len());
write_u32(&mut frame, payload.len() as u32);
frame.extend_from_slice(&hash);
frame.extend_from_slice(&payload);
frame
}
pub fn encode_batch(batch: &FlushBatch) -> Vec<u8> {
let records: Vec<(&str, &[u8])> =
batch.records.iter().map(|record| (record.key.as_str(), record.bytes.as_slice())).collect();
let deletes: Vec<&str> = batch.deletes.iter().map(RecordKey::as_str).collect();
frame_from_payload(encode_payload(batch.id, &records, &deletes))
}
pub fn encode_snapshot(records: &BTreeMap<RecordKey, Vec<u8>>) -> Vec<u8> {
let entries: Vec<(&str, &[u8])> = records.iter().map(|(key, bytes)| (key.as_str(), bytes.as_slice())).collect();
frame_from_payload(encode_payload(0, &entries, &[]))
}
fn read_u32(bytes: &[u8], at: usize) -> Option<(u32, usize)> {
let end = at.checked_add(4)?;
let slice = bytes.get(at..end)?;
Some((u32::from_le_bytes(slice.try_into().ok()?), end))
}
fn read_u64(bytes: &[u8], at: usize) -> Option<(u64, usize)> {
let end = at.checked_add(8)?;
let slice = bytes.get(at..end)?;
Some((u64::from_le_bytes(slice.try_into().ok()?), end))
}
fn read_bytes(bytes: &[u8], at: usize) -> Option<(&[u8], usize)> {
let (len, at) = read_u32(bytes, at)?;
let end = at.checked_add(len as usize)?;
let slice = bytes.get(at..end)?;
Some((slice, end))
}
pub trait LogSink {
fn byte_len(&mut self) -> std::io::Result<u64>;
fn append_synced(&mut self, bytes: &[u8]) -> std::io::Result<()>;
fn truncate(&mut self, len: u64) -> std::io::Result<()>;
}
pub fn append_frame<S: LogSink>(sink: &mut S, frame: &[u8]) -> std::io::Result<()> {
let prev_len = sink.byte_len()?;
if sink.append_synced(frame).is_ok() {
return Ok(());
}
sink.truncate(prev_len)?;
match sink.append_synced(frame) {
Ok(()) => Ok(()),
Err(error) => {
let _ = sink.truncate(prev_len);
Err(error)
}
}
}
type DecodedFrame = (Vec<(String, Vec<u8>)>, Vec<String>);
fn decode_payload(payload: &[u8]) -> Option<DecodedFrame> {
let (_batch_id, mut at) = read_u64(payload, 0)?;
let (record_count, next) = read_u32(payload, at)?;
at = next;
let mut records = Vec::with_capacity(record_count as usize);
for _ in 0..record_count {
let (key, next) = read_bytes(payload, at)?;
let key = String::from_utf8(key.to_vec()).ok()?;
let (value, next) = read_bytes(payload, next)?;
at = next;
records.push((key, value.to_vec()));
}
let (delete_count, next) = read_u32(payload, at)?;
at = next;
let mut deletes = Vec::with_capacity(delete_count as usize);
for _ in 0..delete_count {
let (key, next) = read_bytes(payload, at)?;
let key = String::from_utf8(key.to_vec()).ok()?;
at = next;
deletes.push(key);
}
if at != payload.len() {
return None;
}
Some((records, deletes))
}
pub fn replay(bytes: &[u8]) -> (BTreeMap<RecordKey, Vec<u8>>, usize) {
let mut map = BTreeMap::new();
let mut pos = 0usize;
while let Some((payload_len, header_end)) = read_u32(bytes, pos) {
let Some(hash_end) = header_end.checked_add(HASH_LEN) else { break };
let Some(payload_end) = hash_end.checked_add(payload_len as usize) else { break };
let Some(stored_hash) = bytes.get(header_end..hash_end) else { break };
let Some(payload) = bytes.get(hash_end..payload_end) else { break };
if Sha256::digest(payload).as_slice() != stored_hash {
break;
}
let Some((records, deletes)) = decode_payload(payload) else { break };
for (key, value) in records {
map.insert(RecordKey::new(key), value);
}
for key in deletes {
map.remove(&RecordKey::new(key));
}
pos = payload_end;
}
(map, pos)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::persist::SealedRecord;
fn batch(id: u64, records: &[(&str, &[u8])], deletes: &[&str]) -> FlushBatch {
FlushBatch {
id,
records: records.iter().map(|(key, bytes)| SealedRecord { key: RecordKey::new(*key), bytes: bytes.to_vec() }).collect(),
deletes: deletes.iter().map(|key| RecordKey::new(*key)).collect(),
}
}
#[test]
fn log_replay_applies_frames_in_order_and_deletes() {
let mut log = Vec::new();
log.extend(encode_batch(&batch(0, &[("a", b"1"), ("b", b"2")], &[])));
log.extend(encode_batch(&batch(1, &[("b", b"2-updated")], &["a"])));
let (records, valid_len) = replay(&log);
assert_eq!(valid_len, log.len(), "every frame here is well-formed");
assert_eq!(records.len(), 1, "'a' was deleted by the second frame");
assert_eq!(records.get(&RecordKey::new("b")), Some(&b"2-updated".to_vec()));
assert_eq!(records.get(&RecordKey::new("a")), None);
}
#[test]
fn log_replay_stops_at_torn_tail() {
let mut log = encode_batch(&batch(0, &[("a", b"1")], &[]));
let full_len = log.len();
log.extend(encode_batch(&batch(1, &[("b", b"this frame's tail is about to be cut off")], &[])));
log.truncate(full_len + 10);
let (records, valid_len) = replay(&log);
assert_eq!(valid_len, full_len, "only the first, complete frame counts");
assert_eq!(records.len(), 1);
assert_eq!(records.get(&RecordKey::new("a")), Some(&b"1".to_vec()));
assert!(!records.contains_key(&RecordKey::new("b")), "the torn second frame never applied");
}
#[test]
fn log_replay_stops_at_a_corrupted_frame() {
let mut log = encode_batch(&batch(0, &[("a", b"1")], &[]));
let full_len = log.len();
let mut second = encode_batch(&batch(1, &[("b", b"2")], &[]));
let corrupt_at = second.len() - 1;
second[corrupt_at] ^= 0xFF;
log.extend(second);
let (records, valid_len) = replay(&log);
assert_eq!(valid_len, full_len, "the corrupted frame's hash mismatch stops replay before it");
assert_eq!(records.len(), 1);
}
#[test]
fn log_replay_of_empty_bytes_is_empty() {
let (records, valid_len) = replay(&[]);
assert!(records.is_empty());
assert_eq!(valid_len, 0);
}
struct FaultySink {
bytes: Vec<u8>,
script: std::collections::VecDeque<Option<usize>>,
fail_truncate: bool,
}
impl FaultySink {
fn new(bytes: Vec<u8>, script: &[Option<usize>]) -> Self {
Self { bytes, script: script.iter().copied().collect(), fail_truncate: false }
}
}
impl LogSink for FaultySink {
fn byte_len(&mut self) -> std::io::Result<u64> {
Ok(self.bytes.len() as u64)
}
fn append_synced(&mut self, bytes: &[u8]) -> std::io::Result<()> {
match self.script.pop_front().flatten() {
None => {
self.bytes.extend_from_slice(bytes);
Ok(())
}
Some(partial) => {
self.bytes.extend_from_slice(&bytes[..partial.min(bytes.len())]);
Err(std::io::Error::other("injected write failure"))
}
}
}
fn truncate(&mut self, len: u64) -> std::io::Result<()> {
if self.fail_truncate {
return Err(std::io::Error::other("injected truncate failure"));
}
self.bytes.truncate(len as usize);
Ok(())
}
}
#[test]
fn append_frame_cuts_a_torn_frame_and_retries_once() {
let first = encode_batch(&batch(0, &[("a", b"1")], &[]));
let second = encode_batch(&batch(1, &[("b", b"2")], &[]));
let mut sink = FaultySink::new(first.clone(), &[Some(10), None]);
append_frame(&mut sink, &second).expect("the retry succeeds");
let mut expected = first;
expected.extend_from_slice(&second);
assert_eq!(sink.bytes, expected, "no partial frame left in front of the retried one");
let (records, valid_len) = replay(&sink.bytes);
assert_eq!(valid_len, sink.bytes.len());
assert_eq!(records.len(), 2);
}
#[test]
fn append_frame_gives_up_after_a_second_failure_and_leaves_no_torn_frame() {
let first = encode_batch(&batch(0, &[("a", b"1")], &[]));
let second = encode_batch(&batch(1, &[("b", b"2")], &[]));
let mut sink = FaultySink::new(first.clone(), &[Some(10), Some(20)]);
assert!(append_frame(&mut sink, &second).is_err());
assert_eq!(sink.bytes, first, "both partial writes were cut back");
let (_, valid_len) = replay(&sink.bytes);
assert_eq!(valid_len, sink.bytes.len());
}
#[test]
fn append_frame_reports_failure_when_the_cut_itself_fails() {
let first = encode_batch(&batch(0, &[("a", b"1")], &[]));
let second = encode_batch(&batch(1, &[("b", b"2")], &[]));
let mut sink = FaultySink::new(first, &[Some(10), None]);
sink.fail_truncate = true;
assert!(append_frame(&mut sink, &second).is_err(), "a torn frame that cannot be cut must never look like success");
}
#[test]
fn append_frame_appends_a_clean_write_untouched() {
let first = encode_batch(&batch(0, &[("a", b"1")], &[]));
let mut sink = FaultySink::new(Vec::new(), &[None]);
append_frame(&mut sink, &first).expect("clean write");
assert_eq!(sink.bytes, first);
}
#[test]
fn log_snapshot_round_trips() {
let mut source = BTreeMap::new();
source.insert(RecordKey::new("messenger/DEV1/account"), b"account-bytes".to_vec());
source.insert(RecordKey::new("messenger/DEV1/sync_token"), b"s123".to_vec());
let snapshot = encode_snapshot(&source);
let (replayed, valid_len) = replay(&snapshot);
assert_eq!(valid_len, snapshot.len());
assert_eq!(replayed, source);
}
}