use std::collections::{BTreeMap, BTreeSet};
pub const JOURNAL_FRAME_MAGIC: [u8; 8] = *b"ASRQJRN1";
pub const JOURNAL_FRAME_VERSION: u16 = 1;
pub const JOURNAL_FLAG_CHECKPOINT_BOUNDARY: u16 = 0b0000_0001;
pub const JOURNAL_FRAME_HEADER_LEN: usize = 44;
const CRC_LEN: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JournalFrameError {
Truncated,
BadMagic,
UnsupportedVersion(u16),
PayloadTruncated,
HeaderChecksumMismatch,
PayloadChecksumMismatch,
}
impl core::fmt::Display for JournalFrameError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Truncated => f.write_str("journal frame truncated before a full header"),
Self::BadMagic => f.write_str("journal frame magic mismatch"),
Self::UnsupportedVersion(v) => write!(f, "unsupported journal frame version {v}"),
Self::PayloadTruncated => f.write_str("journal frame payload truncated (torn tail)"),
Self::HeaderChecksumMismatch => f.write_str("journal frame header CRC mismatch"),
Self::PayloadChecksumMismatch => f.write_str("journal frame payload CRC mismatch"),
}
}
}
impl std::error::Error for JournalFrameError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JournalFrameHeader {
pub epoch: u64,
pub source_block_number: u32,
pub encoding_symbol_id: u32,
pub source_symbol_count: u32,
pub symbol_size: u32,
pub payload_len: u32,
pub flags: u16,
}
impl JournalFrameHeader {
#[must_use]
pub const fn is_checkpoint_boundary(&self) -> bool {
self.flags & JOURNAL_FLAG_CHECKPOINT_BOUNDARY != 0
}
#[must_use]
fn encode(&self) -> [u8; JOURNAL_FRAME_HEADER_LEN] {
let mut out = [0u8; JOURNAL_FRAME_HEADER_LEN];
out[0..8].copy_from_slice(&JOURNAL_FRAME_MAGIC);
out[8..10].copy_from_slice(&JOURNAL_FRAME_VERSION.to_be_bytes());
out[10..12].copy_from_slice(&self.flags.to_be_bytes());
out[12..20].copy_from_slice(&self.epoch.to_be_bytes());
out[20..24].copy_from_slice(&self.source_block_number.to_be_bytes());
out[24..28].copy_from_slice(&self.encoding_symbol_id.to_be_bytes());
out[28..32].copy_from_slice(&self.source_symbol_count.to_be_bytes());
out[32..36].copy_from_slice(&self.symbol_size.to_be_bytes());
out[36..40].copy_from_slice(&self.payload_len.to_be_bytes());
let header_crc = crc32(&out[0..40]);
out[40..44].copy_from_slice(&header_crc.to_be_bytes());
out
}
fn decode(bytes: &[u8]) -> Result<Self, JournalFrameError> {
if bytes.len() < JOURNAL_FRAME_HEADER_LEN {
return Err(JournalFrameError::Truncated);
}
if bytes[0..8] != JOURNAL_FRAME_MAGIC {
return Err(JournalFrameError::BadMagic);
}
let version = u16::from_be_bytes([bytes[8], bytes[9]]);
if version > JOURNAL_FRAME_VERSION {
return Err(JournalFrameError::UnsupportedVersion(version));
}
let stored_crc = u32::from_be_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]);
if stored_crc != crc32(&bytes[0..40]) {
return Err(JournalFrameError::HeaderChecksumMismatch);
}
Ok(Self {
flags: u16::from_be_bytes([bytes[10], bytes[11]]),
epoch: u64::from_be_bytes(read8(bytes, 12)),
source_block_number: u32::from_be_bytes(read4(bytes, 20)),
encoding_symbol_id: u32::from_be_bytes(read4(bytes, 24)),
source_symbol_count: u32::from_be_bytes(read4(bytes, 28)),
symbol_size: u32::from_be_bytes(read4(bytes, 32)),
payload_len: u32::from_be_bytes(read4(bytes, 36)),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JournalFrame {
pub header: JournalFrameHeader,
pub payload: Vec<u8>,
}
impl JournalFrame {
#[must_use]
pub fn new(
epoch: u64,
source_block_number: u32,
encoding_symbol_id: u32,
source_symbol_count: u32,
symbol_size: u32,
flags: u16,
payload: Vec<u8>,
) -> Self {
let payload_len = u32::try_from(payload.len()).unwrap_or(u32::MAX);
Self {
header: JournalFrameHeader {
epoch,
source_block_number,
encoding_symbol_id,
source_symbol_count,
symbol_size,
payload_len,
flags,
},
payload,
}
}
#[must_use]
pub fn encoded_len(&self) -> usize {
JOURNAL_FRAME_HEADER_LEN + self.payload.len() + CRC_LEN
}
pub fn encode_into(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.header.encode());
out.extend_from_slice(&self.payload);
out.extend_from_slice(&crc32(&self.payload).to_be_bytes());
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.encoded_len());
self.encode_into(&mut out);
out
}
pub fn decode(bytes: &[u8]) -> Result<(Self, usize), JournalFrameError> {
let header = JournalFrameHeader::decode(bytes)?;
let payload_len = header.payload_len as usize;
let payload_start = JOURNAL_FRAME_HEADER_LEN;
let payload_end = payload_start
.checked_add(payload_len)
.ok_or(JournalFrameError::PayloadTruncated)?;
let crc_end = payload_end
.checked_add(CRC_LEN)
.ok_or(JournalFrameError::PayloadTruncated)?;
if bytes.len() < crc_end {
return Err(JournalFrameError::PayloadTruncated);
}
let payload = &bytes[payload_start..payload_end];
let stored_crc = u32::from_be_bytes(read4(bytes, payload_end));
if stored_crc != crc32(payload) {
return Err(JournalFrameError::PayloadChecksumMismatch);
}
Ok((
Self {
header,
payload: payload.to_vec(),
},
crc_end,
))
}
}
#[must_use]
pub fn scan_frames(bytes: &[u8]) -> (Vec<JournalFrame>, usize) {
let mut frames = Vec::new();
let mut offset = 0;
while offset < bytes.len() {
match JournalFrame::decode(&bytes[offset..]) {
Ok((frame, consumed)) => {
frames.push(frame);
offset += consumed;
}
Err(_) => break,
}
}
(frames, offset)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BlockKey {
pub epoch: u64,
pub source_block_number: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockRecovery {
pub key: BlockKey,
pub distinct_symbols: usize,
pub source_symbol_count: u32,
}
impl BlockRecovery {
#[must_use]
pub fn is_decodable(&self) -> bool {
self.distinct_symbols as u64 >= u64::from(self.source_symbol_count)
}
}
#[must_use]
pub fn summarize_blocks(frames: &[JournalFrame]) -> Vec<BlockRecovery> {
let mut blocks: BTreeMap<BlockKey, (BTreeSet<u32>, u32)> = BTreeMap::new();
for frame in frames {
let key = BlockKey {
epoch: frame.header.epoch,
source_block_number: frame.header.source_block_number,
};
let entry = blocks.entry(key).or_insert_with(|| (BTreeSet::new(), 0));
entry.0.insert(frame.header.encoding_symbol_id);
entry.1 = entry.1.max(frame.header.source_symbol_count);
}
blocks
.into_iter()
.map(|(key, (symbols, source_symbol_count))| BlockRecovery {
key,
distinct_symbols: symbols.len(),
source_symbol_count,
})
.collect()
}
#[must_use]
pub fn decodable_blocks(frames: &[JournalFrame]) -> Vec<BlockKey> {
summarize_blocks(frames)
.into_iter()
.filter(BlockRecovery::is_decodable)
.map(|block| block.key)
.collect()
}
#[must_use]
pub fn latest_recoverable_epoch(frames: &[JournalFrame]) -> Option<u64> {
let mut epoch_complete: BTreeMap<u64, bool> = BTreeMap::new();
for block in summarize_blocks(frames) {
let complete = epoch_complete.entry(block.key.epoch).or_insert(true);
*complete = *complete && block.is_decodable();
}
epoch_complete
.into_iter()
.filter(|&(_, complete)| complete)
.map(|(epoch, _)| epoch)
.next_back()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EpochManifest {
pub epoch: u64,
pub source_block_count: u32,
}
pub const EPOCH_MANIFEST_MAGIC: [u8; 4] = *b"ASRM";
pub const EPOCH_MANIFEST_LEN: usize = 4 + 2 + 8 + 4 + 4;
impl EpochManifest {
#[must_use]
pub fn encode(&self) -> [u8; EPOCH_MANIFEST_LEN] {
let mut out = [0u8; EPOCH_MANIFEST_LEN];
out[0..4].copy_from_slice(&EPOCH_MANIFEST_MAGIC);
out[4..6].copy_from_slice(&JOURNAL_FRAME_VERSION.to_be_bytes());
out[6..14].copy_from_slice(&self.epoch.to_be_bytes());
out[14..18].copy_from_slice(&self.source_block_count.to_be_bytes());
let crc = crc32(&out[0..18]);
out[18..22].copy_from_slice(&crc.to_be_bytes());
out
}
pub fn decode(bytes: &[u8]) -> Result<Self, JournalFrameError> {
if bytes.len() < EPOCH_MANIFEST_LEN {
return Err(JournalFrameError::Truncated);
}
if bytes[0..4] != EPOCH_MANIFEST_MAGIC {
return Err(JournalFrameError::BadMagic);
}
let version = u16::from_be_bytes([bytes[4], bytes[5]]);
if version > JOURNAL_FRAME_VERSION {
return Err(JournalFrameError::UnsupportedVersion(version));
}
let stored_crc = u32::from_be_bytes(read4(bytes, 18));
if stored_crc != crc32(&bytes[0..18]) {
return Err(JournalFrameError::HeaderChecksumMismatch);
}
Ok(Self {
epoch: u64::from_be_bytes(read8(bytes, 6)),
source_block_count: u32::from_be_bytes(read4(bytes, 14)),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObjectParamsRecord {
pub epoch: u64,
pub object_size: u64,
pub symbol_size: u16,
pub max_block_size: u32,
}
pub const OBJECT_PARAMS_MAGIC: [u8; 4] = *b"ASRP";
pub const OBJECT_PARAMS_RECORD_LEN: usize = 4 + 2 + 8 + 8 + 2 + 4 + 4;
impl ObjectParamsRecord {
#[must_use]
pub fn encode(&self) -> [u8; OBJECT_PARAMS_RECORD_LEN] {
let mut out = [0u8; OBJECT_PARAMS_RECORD_LEN];
out[0..4].copy_from_slice(&OBJECT_PARAMS_MAGIC);
out[4..6].copy_from_slice(&JOURNAL_FRAME_VERSION.to_be_bytes());
out[6..14].copy_from_slice(&self.epoch.to_be_bytes());
out[14..22].copy_from_slice(&self.object_size.to_be_bytes());
out[22..24].copy_from_slice(&self.symbol_size.to_be_bytes());
out[24..28].copy_from_slice(&self.max_block_size.to_be_bytes());
let crc = crc32(&out[0..28]);
out[28..32].copy_from_slice(&crc.to_be_bytes());
out
}
pub fn decode(bytes: &[u8]) -> Result<Self, JournalFrameError> {
if bytes.len() < OBJECT_PARAMS_RECORD_LEN {
return Err(JournalFrameError::Truncated);
}
if bytes[0..4] != OBJECT_PARAMS_MAGIC {
return Err(JournalFrameError::BadMagic);
}
let version = u16::from_be_bytes([bytes[4], bytes[5]]);
if version > JOURNAL_FRAME_VERSION {
return Err(JournalFrameError::UnsupportedVersion(version));
}
let stored_crc = u32::from_be_bytes(read4(bytes, 28));
if stored_crc != crc32(&bytes[0..28]) {
return Err(JournalFrameError::HeaderChecksumMismatch);
}
Ok(Self {
epoch: u64::from_be_bytes(read8(bytes, 6)),
object_size: u64::from_be_bytes(read8(bytes, 14)),
symbol_size: u16::from_be_bytes([bytes[22], bytes[23]]),
max_block_size: u32::from_be_bytes(read4(bytes, 24)),
})
}
#[must_use]
pub fn block_layout(&self) -> (u16, u16) {
let object_size = self.object_size;
let symbol_size = u64::from(self.symbol_size);
let max_block_size = u64::from(self.max_block_size);
if object_size == 0 || symbol_size == 0 || max_block_size == 0 {
return (0, 0);
}
let mut blocks: u16 = 0;
let mut max_k: u64 = 0;
let mut offset: u64 = 0;
while offset < object_size {
let len = max_block_size.min(object_size - offset);
let k = len.div_ceil(symbol_size);
max_k = max_k.max(k);
offset += len;
blocks = blocks.saturating_add(1);
}
(blocks, u16::try_from(max_k).unwrap_or(u16::MAX))
}
}
#[must_use]
pub fn epoch_is_complete(frames: &[JournalFrame], manifest: EpochManifest) -> bool {
let decodable: BTreeSet<u32> = summarize_blocks(frames)
.into_iter()
.filter(|block| block.key.epoch == manifest.epoch && block.is_decodable())
.map(|block| block.key.source_block_number)
.collect();
(0..manifest.source_block_count).all(|sbn| decodable.contains(&sbn))
}
#[must_use]
pub fn latest_complete_epoch(frames: &[JournalFrame], manifests: &[EpochManifest]) -> Option<u64> {
manifests
.iter()
.filter(|manifest| epoch_is_complete(frames, **manifest))
.map(|manifest| manifest.epoch)
.max()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StripePlan {
symbol_count: usize,
stripe_count: usize,
}
impl StripePlan {
#[must_use]
pub const fn new(symbol_count: usize, stripe_count: usize) -> Option<Self> {
if stripe_count == 0 {
None
} else {
Some(Self {
symbol_count,
stripe_count,
})
}
}
#[must_use]
pub const fn stripe_count(&self) -> usize {
self.stripe_count
}
#[must_use]
pub const fn stripe_of(&self, index: usize) -> usize {
index % self.stripe_count
}
#[must_use]
pub const fn symbols_on_stripe(&self, stripe: usize) -> usize {
if stripe >= self.stripe_count {
return 0;
}
if stripe >= self.symbol_count {
0
} else {
(self.symbol_count - stripe + self.stripe_count - 1) / self.stripe_count
}
}
#[must_use]
pub const fn symbols_surviving_loss(&self, lost: usize) -> usize {
if lost >= self.stripe_count {
return 0;
}
let base = self.symbol_count / self.stripe_count; let remainder = self.symbol_count % self.stripe_count; let ceil_lost = if lost < remainder { lost } else { remainder };
let floor_lost = lost - ceil_lost;
let lost_symbols = ceil_lost * (base + 1) + floor_lost * base;
self.symbol_count - lost_symbols
}
#[must_use]
pub const fn survives_stripe_loss(&self, source_symbols: usize, lost: usize) -> bool {
self.symbols_surviving_loss(lost) >= source_symbols
}
}
#[must_use]
pub fn serialize_striped(
epoch: u64,
source_block_number: u32,
source_symbol_count: u32,
symbol_size: u32,
flags: u16,
symbols: &[(u32, Vec<u8>)],
plan: &StripePlan,
) -> Vec<Vec<u8>> {
let mut stripes = vec![Vec::new(); plan.stripe_count()];
for (index, (encoding_symbol_id, payload)) in symbols.iter().enumerate() {
let frame = JournalFrame::new(
epoch,
source_block_number,
*encoding_symbol_id,
source_symbol_count,
symbol_size,
flags,
payload.clone(),
);
frame.encode_into(&mut stripes[plan.stripe_of(index)]);
}
stripes
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockSymbols {
pub source_block_number: u32,
pub source_symbol_count: u32,
pub symbol_size: u32,
pub symbols: Vec<(u32, Vec<u8>)>,
}
#[must_use]
pub fn serialize_epoch(
epoch: u64,
stripe_count: usize,
flags: u16,
blocks: &[BlockSymbols],
) -> Option<(Vec<Vec<u8>>, EpochManifest)> {
if stripe_count == 0 {
return None;
}
let mut stripes = vec![Vec::new(); stripe_count];
for block in blocks {
let plan = StripePlan::new(block.symbols.len(), stripe_count)?;
let per_block = serialize_striped(
epoch,
block.source_block_number,
block.source_symbol_count,
block.symbol_size,
flags,
&block.symbols,
&plan,
);
for (stripe, bytes) in per_block.into_iter().enumerate() {
stripes[stripe].extend_from_slice(&bytes);
}
}
let manifest = EpochManifest {
epoch,
source_block_count: u32::try_from(blocks.len()).unwrap_or(u32::MAX),
};
Some((stripes, manifest))
}
#[inline]
fn read4(bytes: &[u8], at: usize) -> [u8; 4] {
[bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]
}
#[inline]
fn read8(bytes: &[u8], at: usize) -> [u8; 8] {
[
bytes[at],
bytes[at + 1],
bytes[at + 2],
bytes[at + 3],
bytes[at + 4],
bytes[at + 5],
bytes[at + 6],
bytes[at + 7],
]
}
#[must_use]
pub fn crc32(bytes: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in bytes {
crc ^= u32::from(byte);
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}