pub fn fnv1a(bytes: &[u8]) -> u64 {
const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
const PRIME: u64 = 1_099_511_628_211;
let mut hash = OFFSET_BASIS;
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(PRIME);
}
hash
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum JournalEntryKind {
Put,
Delete,
Pin,
Unpin,
Compact,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct JournalEntry {
pub sequence: u64,
pub kind: JournalEntryKind,
pub cid: String,
pub size_bytes: u64,
pub timestamp_secs: u64,
pub checksum: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct JournalCursor {
pub last_sequence: u64,
}
impl JournalCursor {
pub fn is_at_start(&self) -> bool {
self.last_sequence == 0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct JournalStats {
pub total_entries: usize,
pub put_count: usize,
pub delete_count: usize,
pub total_bytes_journaled: u64,
pub oldest_sequence: Option<u64>,
pub newest_sequence: Option<u64>,
}
pub struct StorageWriteJournal {
pub entries: Vec<JournalEntry>,
pub next_sequence: u64,
pub max_entries: usize,
}
impl StorageWriteJournal {
pub fn new(max_entries: usize) -> Self {
Self {
entries: Vec::new(),
next_sequence: 1,
max_entries,
}
}
pub fn append(
&mut self,
kind: JournalEntryKind,
cid: &str,
size_bytes: u64,
timestamp_secs: u64,
) -> u64 {
let sequence = self.next_sequence;
let checksum = fnv1a(cid.as_bytes()) ^ sequence;
let entry = JournalEntry {
sequence,
kind,
cid: cid.to_owned(),
size_bytes,
timestamp_secs,
checksum,
};
self.entries.push(entry);
if self.entries.len() > self.max_entries {
self.entries.remove(0);
}
self.next_sequence += 1;
sequence
}
pub fn entries_since<'a>(&'a self, cursor: &JournalCursor) -> Vec<&'a JournalEntry> {
self.entries
.iter()
.filter(|e| e.sequence > cursor.last_sequence)
.collect()
}
pub fn cursor(&self) -> JournalCursor {
let last_sequence = self.entries.last().map(|e| e.sequence).unwrap_or(0);
JournalCursor { last_sequence }
}
pub fn verify_checksum(&self, entry: &JournalEntry) -> bool {
let expected = fnv1a(entry.cid.as_bytes()) ^ entry.sequence;
expected == entry.checksum
}
pub fn stats(&self) -> JournalStats {
let mut put_count = 0usize;
let mut delete_count = 0usize;
let mut total_bytes_journaled = 0u64;
for entry in &self.entries {
match entry.kind {
JournalEntryKind::Put => {
put_count += 1;
total_bytes_journaled = total_bytes_journaled.saturating_add(entry.size_bytes);
}
JournalEntryKind::Delete => {
delete_count += 1;
}
_ => {}
}
}
let oldest_sequence = self.entries.first().map(|e| e.sequence);
let newest_sequence = self.entries.last().map(|e| e.sequence);
JournalStats {
total_entries: self.entries.len(),
put_count,
delete_count,
total_bytes_journaled,
oldest_sequence,
newest_sequence,
}
}
pub fn truncate_before(&mut self, sequence: u64) {
self.entries.retain(|e| e.sequence >= sequence);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fnv1a_empty_is_offset_basis() {
assert_eq!(fnv1a(b""), 14_695_981_039_346_656_037);
}
#[test]
fn test_fnv1a_deterministic() {
assert_eq!(fnv1a(b"hello"), fnv1a(b"hello"));
}
#[test]
fn test_fnv1a_different_inputs_differ() {
assert_ne!(fnv1a(b"foo"), fnv1a(b"bar"));
}
#[test]
fn test_new_starts_empty() {
let journal = StorageWriteJournal::new(100);
assert!(journal.entries.is_empty());
}
#[test]
fn test_new_next_sequence_is_one() {
let journal = StorageWriteJournal::new(100);
assert_eq!(journal.next_sequence, 1);
}
#[test]
fn test_append_returns_correct_sequence() {
let mut journal = StorageWriteJournal::new(100);
let seq = journal.append(JournalEntryKind::Put, "bafyabc", 512, 1_000);
assert_eq!(seq, 1);
}
#[test]
fn test_append_sequence_monotonically_increasing() {
let mut journal = StorageWriteJournal::new(100);
let s1 = journal.append(JournalEntryKind::Put, "cid1", 100, 1);
let s2 = journal.append(JournalEntryKind::Delete, "cid2", 0, 2);
let s3 = journal.append(JournalEntryKind::Pin, "cid3", 0, 3);
assert!(s1 < s2 && s2 < s3);
assert_eq!(s1, 1);
assert_eq!(s2, 2);
assert_eq!(s3, 3);
}
#[test]
fn test_append_stores_entry_correctly() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "bafytest", 1024, 999);
let entry = &journal.entries[0];
assert_eq!(entry.sequence, 1);
assert_eq!(entry.kind, JournalEntryKind::Put);
assert_eq!(entry.cid, "bafytest");
assert_eq!(entry.size_bytes, 1024);
assert_eq!(entry.timestamp_secs, 999);
}
#[test]
fn test_append_checksum_is_fnv1a_xor_sequence() {
let mut journal = StorageWriteJournal::new(100);
let seq = journal.append(JournalEntryKind::Put, "bafychecksum", 0, 0);
let entry = &journal.entries[0];
let expected = fnv1a(b"bafychecksum") ^ seq;
assert_eq!(entry.checksum, expected);
}
#[test]
fn test_verify_checksum_true_for_valid_entry() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "bafyvalid", 256, 42);
let entry = &journal.entries[0];
assert!(journal.verify_checksum(entry));
}
#[test]
fn test_verify_checksum_false_for_tampered_cid() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "bafyoriginal", 256, 42);
let mut tampered = journal.entries[0].clone();
tampered.cid = "bafytampered".to_owned();
assert!(!journal.verify_checksum(&tampered));
}
#[test]
fn test_verify_checksum_false_for_tampered_checksum() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "bafyoriginal2", 256, 42);
let mut tampered = journal.entries[0].clone();
tampered.checksum ^= 0xDEAD_BEEF;
assert!(!journal.verify_checksum(&tampered));
}
#[test]
fn test_entries_since_zero_cursor_returns_all() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "a", 1, 0);
journal.append(JournalEntryKind::Put, "b", 2, 0);
journal.append(JournalEntryKind::Delete, "c", 0, 0);
let cursor = JournalCursor { last_sequence: 0 };
let result = journal.entries_since(&cursor);
assert_eq!(result.len(), 3);
}
#[test]
fn test_entries_since_cursor_returns_only_newer() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "a", 1, 0);
journal.append(JournalEntryKind::Put, "b", 2, 0);
journal.append(JournalEntryKind::Delete, "c", 0, 0);
let cursor = JournalCursor { last_sequence: 1 };
let result = journal.entries_since(&cursor);
assert_eq!(result.len(), 2);
assert_eq!(result[0].sequence, 2);
assert_eq!(result[1].sequence, 3);
}
#[test]
fn test_entries_since_preserves_order() {
let mut journal = StorageWriteJournal::new(100);
for i in 0..5u64 {
journal.append(JournalEntryKind::Put, &format!("cid{i}"), i * 10, i);
}
let cursor = JournalCursor { last_sequence: 0 };
let result = journal.entries_since(&cursor);
let seqs: Vec<u64> = result.iter().map(|e| e.sequence).collect();
let mut sorted = seqs.clone();
sorted.sort_unstable();
assert_eq!(seqs, sorted);
}
#[test]
fn test_cursor_returns_zero_when_empty() {
let journal = StorageWriteJournal::new(100);
assert_eq!(journal.cursor().last_sequence, 0);
}
#[test]
fn test_cursor_returns_newest_sequence() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "x", 0, 0);
journal.append(JournalEntryKind::Put, "y", 0, 0);
journal.append(JournalEntryKind::Put, "z", 0, 0);
assert_eq!(journal.cursor().last_sequence, 3);
}
#[test]
fn test_is_at_start_true_when_last_sequence_zero() {
let cursor = JournalCursor { last_sequence: 0 };
assert!(cursor.is_at_start());
}
#[test]
fn test_is_at_start_false_when_last_sequence_nonzero() {
let cursor = JournalCursor { last_sequence: 5 };
assert!(!cursor.is_at_start());
}
#[test]
fn test_max_entries_evicts_oldest() {
let mut journal = StorageWriteJournal::new(3);
journal.append(JournalEntryKind::Put, "a", 0, 0); journal.append(JournalEntryKind::Put, "b", 0, 0); journal.append(JournalEntryKind::Put, "c", 0, 0); journal.append(JournalEntryKind::Put, "d", 0, 0); assert_eq!(journal.entries.len(), 3);
assert_eq!(journal.entries[0].sequence, 2);
assert_eq!(journal.entries[2].sequence, 4);
}
#[test]
fn test_append_after_max_entries_has_correct_sequences() {
let mut journal = StorageWriteJournal::new(2);
for i in 0..5u64 {
journal.append(JournalEntryKind::Put, &format!("cid{i}"), 0, 0);
}
assert_eq!(journal.entries.len(), 2);
assert_eq!(journal.entries[0].sequence, 4);
assert_eq!(journal.entries[1].sequence, 5);
}
#[test]
fn test_truncate_before_removes_correct_entries() {
let mut journal = StorageWriteJournal::new(100);
for i in 0..5u64 {
journal.append(JournalEntryKind::Put, &format!("cid{i}"), 0, 0);
}
journal.truncate_before(3);
assert_eq!(journal.entries.len(), 3);
assert_eq!(journal.entries[0].sequence, 3);
}
#[test]
fn test_stats_total_entries_correct() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "a", 10, 0);
journal.append(JournalEntryKind::Delete, "b", 0, 0);
journal.append(JournalEntryKind::Pin, "c", 0, 0);
assert_eq!(journal.stats().total_entries, 3);
}
#[test]
fn test_stats_put_count_correct() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "a", 10, 0);
journal.append(JournalEntryKind::Put, "b", 20, 0);
journal.append(JournalEntryKind::Delete, "c", 0, 0);
assert_eq!(journal.stats().put_count, 2);
}
#[test]
fn test_stats_delete_count_correct() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Delete, "a", 0, 0);
journal.append(JournalEntryKind::Put, "b", 50, 0);
journal.append(JournalEntryKind::Delete, "c", 0, 0);
assert_eq!(journal.stats().delete_count, 2);
}
#[test]
fn test_stats_total_bytes_journaled_sums_put_entries_only() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "a", 100, 0);
journal.append(JournalEntryKind::Delete, "b", 999, 0); journal.append(JournalEntryKind::Put, "c", 200, 0);
assert_eq!(journal.stats().total_bytes_journaled, 300);
}
#[test]
fn test_stats_oldest_newest_sequence() {
let mut journal = StorageWriteJournal::new(100);
journal.append(JournalEntryKind::Put, "a", 0, 0);
journal.append(JournalEntryKind::Put, "b", 0, 0);
journal.append(JournalEntryKind::Put, "c", 0, 0);
let stats = journal.stats();
assert_eq!(stats.oldest_sequence, Some(1));
assert_eq!(stats.newest_sequence, Some(3));
}
#[test]
fn test_stats_none_when_empty() {
let journal = StorageWriteJournal::new(100);
let stats = journal.stats();
assert_eq!(stats.oldest_sequence, None);
assert_eq!(stats.newest_sequence, None);
}
#[test]
fn test_compact_entry_appended_correctly() {
let mut journal = StorageWriteJournal::new(100);
let seq = journal.append(JournalEntryKind::Compact, "", 0, 77);
assert_eq!(seq, 1);
let entry = &journal.entries[0];
assert_eq!(entry.kind, JournalEntryKind::Compact);
assert_eq!(entry.cid, "");
assert_eq!(entry.size_bytes, 0);
assert_eq!(entry.timestamp_secs, 77);
assert!(journal.verify_checksum(entry));
}
}