use crate::VersionEntry;
const NULL_VALUE_LEN: u32 = u32::MAX;
pub const BLOCK_MAGIC: &[u8; 4] = b"V2BL";
pub const BLOCK_SIZE: usize = 128;
pub const BLOCK_MAX_BYTES: usize = 1024 * 1024;
pub const MAX_DECODE_BYTES: usize = 64 * 1024 * 1024;
#[must_use]
pub fn expiry_key(to_ci: u64, data_key: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + data_key.len());
out.extend_from_slice(&to_ci.to_be_bytes());
out.extend_from_slice(data_key);
out
}
#[must_use]
pub fn expiry_data_key(key: &[u8]) -> Option<&[u8]> {
key.get(8..)
}
#[must_use]
pub fn expiry_upper_bound(watermark: u64) -> Option<Vec<u8>> {
watermark
.checked_add(1)
.map(|next| next.to_be_bytes().to_vec())
}
fn encode_entries(out: &mut Vec<u8>, versions: &[VersionEntry]) {
for v in versions {
out.extend_from_slice(&v.from_ci.to_le_bytes());
out.extend_from_slice(&v.to_ci.to_le_bytes());
match &v.value {
Some(value) => {
out.extend_from_slice(
&u32::try_from(value.len())
.expect("version value length fits u32")
.to_le_bytes(),
);
out.extend_from_slice(value);
}
None => out.extend_from_slice(&NULL_VALUE_LEN.to_le_bytes()),
}
}
}
fn decode_entries(raw: &[u8], count: usize, mut pos: usize) -> Result<Vec<VersionEntry>, String> {
let mut out = Vec::new();
out.try_reserve_exact(count)
.map_err(|error| format!("invalid history block: entry allocation rejected: {error}"))?;
for _ in 0..count {
if pos + 20 > raw.len() {
return Err("invalid history block: truncated".into());
}
let from_ci = u64::from_le_bytes(
raw[pos..pos + 8]
.try_into()
.map_err(|_| "invalid history block: corrupt entry")?,
);
let to_ci = u64::from_le_bytes(
raw[pos + 8..pos + 16]
.try_into()
.map_err(|_| "invalid history block: corrupt entry")?,
);
let value_len = u32::from_le_bytes(
raw[pos + 16..pos + 20]
.try_into()
.map_err(|_| "invalid history block: corrupt entry")?,
);
pos += 20;
let value = if value_len == NULL_VALUE_LEN {
None
} else {
let end = pos + value_len as usize;
if end > raw.len() {
return Err("invalid history block: truncated value".into());
}
let value = raw[pos..end].to_vec();
pos = end;
Some(value)
};
out.push(VersionEntry {
from_ci,
to_ci,
value,
});
}
if pos != raw.len() {
return Err("invalid history block: trailing bytes".into());
}
Ok(out)
}
#[must_use]
pub fn encode_block(prev: u64, versions: &[VersionEntry]) -> Vec<u8> {
let mut out = Vec::with_capacity(16 + versions.len() * 20);
out.extend_from_slice(BLOCK_MAGIC);
out.extend_from_slice(&prev.to_le_bytes());
out.extend_from_slice(
&u32::try_from(versions.len())
.expect("version count fits u32")
.to_le_bytes(),
);
encode_entries(&mut out, versions);
out
}
pub fn decode_block(raw: &[u8]) -> Result<(u64, Vec<VersionEntry>), String> {
if raw.len() > MAX_DECODE_BYTES {
return Err("invalid history block: exceeds decode limit".into());
}
if raw.len() < 16 || &raw[0..4] != BLOCK_MAGIC {
return Err("invalid history block: bad magic".into());
}
let prev = u64::from_le_bytes(
raw[4..12]
.try_into()
.map_err(|_| "invalid history block: corrupt header")?,
);
let count = u32::from_le_bytes(
raw[12..16]
.try_into()
.map_err(|_| "invalid history block: corrupt header")?,
) as usize;
if count > (raw.len() - 16) / 20 {
return Err("invalid history block: truncated entries".into());
}
let versions = decode_entries(raw, count, 16)?;
Ok((prev, versions))
}
#[must_use]
pub fn block_key(data_key: &[u8], seq: u64) -> Vec<u8> {
let len = u64::try_from(data_key.len()).unwrap_or(u64::MAX);
let mut out = Vec::with_capacity(16 + data_key.len());
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(data_key);
out.extend_from_slice(&seq.to_be_bytes());
out
}
#[must_use]
pub fn block_prefix(data_key: &[u8]) -> Vec<u8> {
let len = u64::try_from(data_key.len()).unwrap_or(u64::MAX);
let mut out = Vec::with_capacity(8 + data_key.len());
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(data_key);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(from: u64, to: u64, value: Option<&[u8]>) -> VersionEntry {
VersionEntry {
from_ci: from,
to_ci: to,
value: value.map(<[u8]>::to_vec),
}
}
#[test]
fn expiry_keys_order_by_to_ci_then_data_key() {
let a = expiry_key(2, b"k1");
let b = expiry_key(2, b"k2");
let c = expiry_key(3, b"k0");
assert!(a < b, "same to_ci orders by data key");
assert!(b < c, "smaller to_ci orders first");
assert_eq!(expiry_data_key(&c), Some(b"k0".as_slice()));
assert_eq!(expiry_data_key(b"short"), None);
}
#[test]
fn expiry_upper_bound_excludes_unexpired_versions() {
let bound = expiry_upper_bound(5).unwrap();
assert!(expiry_key(5, b"k") < bound);
assert!(expiry_key(6, b"k") >= bound);
assert_eq!(expiry_upper_bound(u64::MAX), None);
}
#[test]
fn block_round_trips_with_prev_link() {
let versions = vec![
entry(1, 5, Some(b"a")),
entry(5, 9, None),
entry(9, 0, Some(b"longer value")),
];
let raw = encode_block(42, &versions);
let (prev, decoded) = decode_block(&raw).unwrap();
assert_eq!(prev, 42);
assert_eq!(decoded, versions);
assert!(decode_block(b"nope").is_err());
assert!(decode_block(&raw[..raw.len() - 1]).is_err());
}
#[test]
fn decode_rejects_oversized_history_before_allocating_entries() {
let raw = vec![0u8; MAX_DECODE_BYTES + 1];
assert!(decode_block(&raw).is_err());
}
#[test]
fn corrupt_entry_count_is_rejected_before_reservation() {
let mut raw = Vec::from(&BLOCK_MAGIC[..]);
raw.extend_from_slice(&0u64.to_le_bytes());
raw.extend_from_slice(&u32::MAX.to_le_bytes());
let error = decode_block(&raw).unwrap_err();
assert!(error.contains("truncated entries"), "{error}");
}
#[test]
fn block_keys_are_length_prefixed_and_unambiguous() {
assert_ne!(block_key(b"a", 1), block_key(b"ab", 1));
assert!(block_key(b"a", 1).starts_with(&block_prefix(b"a")));
assert!(!block_key(b"ab", 1).starts_with(&block_prefix(b"a")));
}
}