use crate::core::{LuciError, Result};
use crate::storage::block::{BLOCK_SIZE, BlockId, HEADER_SIZE};
pub const MAGIC: [u8; 8] = *b"LUCI\x00\x00\x00\x01";
pub const FORMAT_VERSION: u32 = 3;
const OFF_MAGIC: usize = 0;
const OFF_VERSION: usize = 8;
const OFF_BLOCK_SIZE: usize = 12;
const OFF_ROOT_A_BLOCK: usize = 16;
const OFF_ROOT_A_CHECKSUM: usize = 24;
const OFF_ROOT_B_BLOCK: usize = 32;
const OFF_ROOT_B_CHECKSUM: usize = 40;
const OFF_ACTIVE_ROOT: usize = 48;
const EMPTY_ROOT_BLOCK: u64 = u64::MAX;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RootPointer {
pub block_id: Option<BlockId>,
pub checksum: u64,
}
impl RootPointer {
pub const EMPTY: Self = Self {
block_id: None,
checksum: 0,
};
pub const fn new(block_id: BlockId, checksum: u64) -> Self {
Self {
block_id: Some(block_id),
checksum,
}
}
pub const fn is_populated(&self) -> bool {
self.block_id.is_some()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ActiveRoot {
A = 0,
B = 1,
}
impl ActiveRoot {
pub const fn inactive(self) -> Self {
match self {
Self::A => Self::B,
Self::B => Self::A,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileHeader {
pub format_version: u32,
pub block_size: u32,
pub root_a: RootPointer,
pub root_b: RootPointer,
pub active_root: ActiveRoot,
}
impl FileHeader {
pub fn new() -> Self {
Self {
format_version: FORMAT_VERSION,
block_size: BLOCK_SIZE,
root_a: RootPointer::EMPTY,
root_b: RootPointer::EMPTY,
active_root: ActiveRoot::A,
}
}
#[cfg(test)]
pub fn with_format_version(version: u32) -> Self {
Self {
format_version: version,
..Self::new()
}
}
pub fn active_root_pointer(&self) -> &RootPointer {
match self.active_root {
ActiveRoot::A => &self.root_a,
ActiveRoot::B => &self.root_b,
}
}
pub fn inactive_root_pointer(&self) -> &RootPointer {
match self.active_root {
ActiveRoot::A => &self.root_b,
ActiveRoot::B => &self.root_a,
}
}
pub fn commit(&mut self, metadata_block: BlockId, checksum: u64) {
let new_root = RootPointer::new(metadata_block, checksum);
match self.active_root {
ActiveRoot::A => self.root_b = new_root,
ActiveRoot::B => self.root_a = new_root,
}
self.active_root = self.active_root.inactive();
self.format_version = FORMAT_VERSION;
}
pub fn to_bytes(&self) -> [u8; HEADER_SIZE as usize] {
let mut buf = [0u8; HEADER_SIZE as usize];
buf[OFF_MAGIC..OFF_MAGIC + 8].copy_from_slice(&MAGIC);
buf[OFF_VERSION..OFF_VERSION + 4].copy_from_slice(&self.format_version.to_le_bytes());
buf[OFF_BLOCK_SIZE..OFF_BLOCK_SIZE + 4].copy_from_slice(&self.block_size.to_le_bytes());
let root_a_block = self
.root_a
.block_id
.map_or(EMPTY_ROOT_BLOCK, |b| b.as_u64());
buf[OFF_ROOT_A_BLOCK..OFF_ROOT_A_BLOCK + 8].copy_from_slice(&root_a_block.to_le_bytes());
buf[OFF_ROOT_A_CHECKSUM..OFF_ROOT_A_CHECKSUM + 8]
.copy_from_slice(&self.root_a.checksum.to_le_bytes());
let root_b_block = self
.root_b
.block_id
.map_or(EMPTY_ROOT_BLOCK, |b| b.as_u64());
buf[OFF_ROOT_B_BLOCK..OFF_ROOT_B_BLOCK + 8].copy_from_slice(&root_b_block.to_le_bytes());
buf[OFF_ROOT_B_CHECKSUM..OFF_ROOT_B_CHECKSUM + 8]
.copy_from_slice(&self.root_b.checksum.to_le_bytes());
buf[OFF_ACTIVE_ROOT] = self.active_root as u8;
buf
}
pub fn from_bytes(buf: &[u8; HEADER_SIZE as usize]) -> Result<Self> {
if buf[OFF_MAGIC..OFF_MAGIC + 8] != MAGIC {
return Err(LuciError::IndexCorrupted(
"invalid magic bytes — not a Luci index file".into(),
));
}
let format_version =
u32::from_le_bytes(buf[OFF_VERSION..OFF_VERSION + 4].try_into().unwrap());
if format_version > FORMAT_VERSION {
return Err(LuciError::IndexCorrupted(format!(
"unsupported format version {format_version} (max supported: {FORMAT_VERSION})"
)));
}
let block_size =
u32::from_le_bytes(buf[OFF_BLOCK_SIZE..OFF_BLOCK_SIZE + 4].try_into().unwrap());
if block_size != BLOCK_SIZE {
return Err(LuciError::IndexCorrupted(format!(
"unexpected block size {block_size} (expected {BLOCK_SIZE})"
)));
}
let root_a = read_root_pointer(buf, OFF_ROOT_A_BLOCK, OFF_ROOT_A_CHECKSUM);
let root_b = read_root_pointer(buf, OFF_ROOT_B_BLOCK, OFF_ROOT_B_CHECKSUM);
let active_root = match buf[OFF_ACTIVE_ROOT] {
0 => ActiveRoot::A,
1 => ActiveRoot::B,
other => {
return Err(LuciError::IndexCorrupted(format!(
"invalid active root flag: {other}"
)));
}
};
Ok(Self {
format_version,
block_size,
root_a,
root_b,
active_root,
})
}
}
pub fn xxh3_checksum(data: &[u8]) -> u64 {
xxhash_rust::xxh3::xxh3_64(data)
}
fn read_root_pointer(buf: &[u8], block_off: usize, checksum_off: usize) -> RootPointer {
let raw_block = u64::from_le_bytes(buf[block_off..block_off + 8].try_into().unwrap());
let checksum = u64::from_le_bytes(buf[checksum_off..checksum_off + 8].try_into().unwrap());
if raw_block == EMPTY_ROOT_BLOCK {
RootPointer::EMPTY
} else {
RootPointer::new(BlockId(raw_block), checksum)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_header_has_empty_roots() {
let h = FileHeader::new();
assert_eq!(h.format_version, FORMAT_VERSION);
assert_eq!(h.block_size, BLOCK_SIZE);
assert_eq!(h.root_a, RootPointer::EMPTY);
assert_eq!(h.root_b, RootPointer::EMPTY);
assert_eq!(h.active_root, ActiveRoot::A);
}
#[test]
fn round_trip_fresh_header() {
let h = FileHeader::new();
let bytes = h.to_bytes();
let h2 = FileHeader::from_bytes(&bytes).unwrap();
assert_eq!(h, h2);
}
#[test]
fn round_trip_with_populated_roots() {
let mut h = FileHeader::new();
h.root_a = RootPointer::new(BlockId(42), 0xDEAD_BEEF);
h.root_b = RootPointer::new(BlockId(99), 0xCAFE_BABE);
h.active_root = ActiveRoot::B;
let bytes = h.to_bytes();
let h2 = FileHeader::from_bytes(&bytes).unwrap();
assert_eq!(h, h2);
}
#[test]
fn commit_flips_active_root() {
let mut h = FileHeader::new();
assert_eq!(h.active_root, ActiveRoot::A);
h.commit(BlockId(5), 0x1111);
assert_eq!(h.active_root, ActiveRoot::B);
assert_eq!(h.root_b, RootPointer::new(BlockId(5), 0x1111));
assert_eq!(h.root_a, RootPointer::EMPTY);
h.commit(BlockId(10), 0x2222);
assert_eq!(h.active_root, ActiveRoot::A);
assert_eq!(h.root_a, RootPointer::new(BlockId(10), 0x2222));
assert_eq!(h.root_b, RootPointer::new(BlockId(5), 0x1111));
}
#[test]
fn active_and_inactive_root_pointers() {
let mut h = FileHeader::new();
h.commit(BlockId(5), 0x1111);
assert_eq!(
*h.active_root_pointer(),
RootPointer::new(BlockId(5), 0x1111)
);
assert_eq!(*h.inactive_root_pointer(), RootPointer::EMPTY);
}
#[test]
fn bad_magic_is_rejected() {
let mut bytes = FileHeader::new().to_bytes();
bytes[0] = b'X';
let err = FileHeader::from_bytes(&bytes).unwrap_err();
assert!(format!("{err}").contains("magic"));
}
#[test]
fn future_version_is_rejected() {
let mut h = FileHeader::new();
h.format_version = FORMAT_VERSION + 1;
let bytes = h.to_bytes();
let err = FileHeader::from_bytes(&bytes).unwrap_err();
assert!(format!("{err}").contains("version"));
}
#[test]
fn wrong_block_size_is_rejected() {
let mut bytes = FileHeader::new().to_bytes();
bytes[OFF_BLOCK_SIZE..OFF_BLOCK_SIZE + 4].copy_from_slice(&(128 * 1024u32).to_le_bytes());
let err = FileHeader::from_bytes(&bytes).unwrap_err();
assert!(format!("{err}").contains("block size"));
}
#[test]
fn invalid_active_root_flag_is_rejected() {
let mut bytes = FileHeader::new().to_bytes();
bytes[OFF_ACTIVE_ROOT] = 2;
let err = FileHeader::from_bytes(&bytes).unwrap_err();
assert!(format!("{err}").contains("active root"));
}
#[test]
fn header_is_exactly_4kb() {
let bytes = FileHeader::new().to_bytes();
assert_eq!(bytes.len(), 4096);
}
#[test]
fn active_root_inactive_is_inverse() {
assert_eq!(ActiveRoot::A.inactive(), ActiveRoot::B);
assert_eq!(ActiveRoot::B.inactive(), ActiveRoot::A);
}
#[test]
fn root_pointer_empty_is_not_populated() {
assert!(!RootPointer::EMPTY.is_populated());
}
#[test]
fn root_pointer_with_block_is_populated() {
let rp = RootPointer::new(BlockId(0), 0);
assert!(rp.is_populated());
}
#[test]
fn xxh3_checksum_is_deterministic() {
let data = b"hello luci";
let c1 = xxh3_checksum(data);
let c2 = xxh3_checksum(data);
assert_eq!(c1, c2);
assert_ne!(c1, 0); }
#[test]
fn xxh3_checksum_differs_for_different_data() {
let c1 = xxh3_checksum(b"block A");
let c2 = xxh3_checksum(b"block B");
assert_ne!(c1, c2);
}
}