use std::ops::Range;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use thiserror::Error;
use crate::Position;
pub const INDEX_HEADER_SIZE: usize = 64;
pub const TYPE_COLUMN_OFFSET: u32 = INDEX_HEADER_SIZE as u32;
const SZ_MAGIC: usize = 4;
const SZ_VERSION: usize = 2;
const SZ_CREATED_AT: usize = 8;
const SZ_BASE_POSITION: usize = 8;
const SZ_EVENT_COUNT: usize = 8;
const SZ_SECTION: usize = 4;
const SZ_CRC: usize = 4;
const OFF_MAGIC: usize = 0;
const OFF_VERSION: usize = OFF_MAGIC + SZ_MAGIC;
const OFF_CREATED_AT: usize = OFF_VERSION + SZ_VERSION;
const OFF_BASE_POSITION: usize = OFF_CREATED_AT + SZ_CREATED_AT;
const OFF_EVENT_COUNT: usize = OFF_BASE_POSITION + SZ_BASE_POSITION;
const OFF_TYPECOL_OFF: usize = OFF_EVENT_COUNT + SZ_EVENT_COUNT;
const OFF_TYPEDICT_OFF: usize = OFF_TYPECOL_OFF + SZ_SECTION;
const OFF_POSTINGS_OFF: usize = OFF_TYPEDICT_OFF + SZ_SECTION;
const OFF_FST_OFF: usize = OFF_POSTINGS_OFF + SZ_SECTION;
const OFF_FST_LEN: usize = OFF_FST_OFF + SZ_SECTION;
const OFF_BODY_CRC: usize = OFF_FST_LEN + SZ_SECTION;
const OFF_PADDING: usize = OFF_BODY_CRC + SZ_SECTION;
const OFF_HEADER_CRC: usize = INDEX_HEADER_SIZE - SZ_CRC;
const _: () = assert!(OFF_PADDING <= OFF_HEADER_CRC);
const _: () = assert!(OFF_HEADER_CRC + SZ_CRC == INDEX_HEADER_SIZE);
const _: () = assert!(TYPE_COLUMN_OFFSET as usize == INDEX_HEADER_SIZE);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IndexSegmentHeader {
pub version: u16,
pub created_at_nanos: u64,
pub base_position: Position,
pub event_count: u64,
pub typedict_off: u32,
pub postings_off: u32,
pub fst_off: u32,
pub fst_len: u32,
pub body_crc: u32,
}
impl IndexSegmentHeader {
pub const MAGIC_BYTES: u32 = u32::from_le_bytes(*b"EVIX");
pub const VERSION: u16 = 0;
pub fn created_at(&self) -> SystemTime {
UNIX_EPOCH + Duration::from_nanos(self.created_at_nanos)
}
pub fn max_position(&self) -> Option<Position> {
(self.event_count > 0)
.then(|| Position::new(self.base_position.get() + self.event_count - 1))
}
pub fn type_column_range(&self) -> Range<usize> {
INDEX_HEADER_SIZE..self.typedict_off as usize
}
pub fn type_dict_range(&self) -> Range<usize> {
self.typedict_off as usize..self.postings_off as usize
}
pub fn postings_range(&self) -> Range<usize> {
self.postings_off as usize..self.fst_off as usize
}
pub fn fst_range(&self) -> Range<usize> {
self.fst_off as usize..self.fst_off as usize + self.fst_len as usize
}
pub fn segment_len(&self) -> usize {
self.fst_off as usize + self.fst_len as usize
}
pub fn to_bytes(&self) -> [u8; INDEX_HEADER_SIZE] {
let mut buf = [0u8; INDEX_HEADER_SIZE];
buf[OFF_MAGIC..OFF_MAGIC + SZ_MAGIC].copy_from_slice(&Self::MAGIC_BYTES.to_le_bytes());
buf[OFF_VERSION..OFF_VERSION + SZ_VERSION].copy_from_slice(&self.version.to_le_bytes());
buf[OFF_CREATED_AT..OFF_CREATED_AT + SZ_CREATED_AT]
.copy_from_slice(&self.created_at_nanos.to_le_bytes());
buf[OFF_BASE_POSITION..OFF_BASE_POSITION + SZ_BASE_POSITION]
.copy_from_slice(&self.base_position.get().to_le_bytes());
buf[OFF_EVENT_COUNT..OFF_EVENT_COUNT + SZ_EVENT_COUNT]
.copy_from_slice(&self.event_count.to_le_bytes());
buf[OFF_TYPECOL_OFF..OFF_TYPECOL_OFF + SZ_SECTION]
.copy_from_slice(&TYPE_COLUMN_OFFSET.to_le_bytes());
buf[OFF_TYPEDICT_OFF..OFF_TYPEDICT_OFF + SZ_SECTION]
.copy_from_slice(&self.typedict_off.to_le_bytes());
buf[OFF_POSTINGS_OFF..OFF_POSTINGS_OFF + SZ_SECTION]
.copy_from_slice(&self.postings_off.to_le_bytes());
buf[OFF_FST_OFF..OFF_FST_OFF + SZ_SECTION].copy_from_slice(&self.fst_off.to_le_bytes());
buf[OFF_FST_LEN..OFF_FST_LEN + SZ_SECTION].copy_from_slice(&self.fst_len.to_le_bytes());
buf[OFF_BODY_CRC..OFF_BODY_CRC + SZ_SECTION].copy_from_slice(&self.body_crc.to_le_bytes());
let crc = crc32fast::hash(&buf[..OFF_HEADER_CRC]);
buf[OFF_HEADER_CRC..].copy_from_slice(&crc.to_le_bytes());
buf
}
pub fn from_bytes(buf: &[u8; INDEX_HEADER_SIZE]) -> Result<Self, IndexHeaderError> {
if buf.iter().all(|&b| b == 0) {
return Err(IndexHeaderError::Unwritten);
}
let expected = u32::from_le_bytes(buf[OFF_HEADER_CRC..].try_into().unwrap());
let computed = crc32fast::hash(&buf[..OFF_HEADER_CRC]);
if expected != computed {
return Err(IndexHeaderError::ChecksumMismatch { expected, computed });
}
let magic = u32::from_le_bytes(buf[OFF_MAGIC..OFF_MAGIC + SZ_MAGIC].try_into().unwrap());
if magic != Self::MAGIC_BYTES {
return Err(IndexHeaderError::BadMagic {
expected: Self::MAGIC_BYTES,
found: magic,
});
}
let version = u16::from_le_bytes(
buf[OFF_VERSION..OFF_VERSION + SZ_VERSION]
.try_into()
.unwrap(),
);
if version > Self::VERSION {
return Err(IndexHeaderError::UnsupportedVersion {
found: version,
supported: Self::VERSION,
});
}
if buf[OFF_PADDING..OFF_HEADER_CRC].iter().any(|&b| b != 0) {
return Err(IndexHeaderError::DirtyPadding);
}
let created_at_nanos = read_u64(buf, OFF_CREATED_AT);
let base_position = Position::new(read_u64(buf, OFF_BASE_POSITION));
let event_count = read_u64(buf, OFF_EVENT_COUNT);
let typecol_off = read_u32(buf, OFF_TYPECOL_OFF);
let typedict_off = read_u32(buf, OFF_TYPEDICT_OFF);
let postings_off = read_u32(buf, OFF_POSTINGS_OFF);
let fst_off = read_u32(buf, OFF_FST_OFF);
let fst_len = read_u32(buf, OFF_FST_LEN);
let body_crc = read_u32(buf, OFF_BODY_CRC);
if typecol_off != TYPE_COLUMN_OFFSET {
return Err(IndexHeaderError::BadSectionLayout {
detail: "type column must begin at offset 64",
});
}
let expected_typedict = event_count
.checked_mul(2)
.and_then(|cols| cols.checked_add(INDEX_HEADER_SIZE as u64));
if expected_typedict != Some(u64::from(typedict_off)) {
return Err(IndexHeaderError::BadSectionLayout {
detail: "type dictionary offset does not match event_count",
});
}
if !(typedict_off <= postings_off && postings_off <= fst_off) {
return Err(IndexHeaderError::BadSectionLayout {
detail: "section offsets are not monotonically non-decreasing",
});
}
if fst_off.checked_add(fst_len).is_none() {
return Err(IndexHeaderError::BadSectionLayout {
detail: "fst_off + fst_len overflows",
});
}
Ok(IndexSegmentHeader {
version,
created_at_nanos,
base_position,
event_count,
typedict_off,
postings_off,
fst_off,
fst_len,
body_crc,
})
}
}
fn read_u64(buf: &[u8; INDEX_HEADER_SIZE], off: usize) -> u64 {
u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
}
fn read_u32(buf: &[u8; INDEX_HEADER_SIZE], off: usize) -> u32 {
u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
}
#[derive(Debug, Error)]
pub enum IndexHeaderError {
#[error("index header is unwritten (all zero)")]
Unwritten,
#[error("bad magic bytes: expected {expected:#010x}, found {found:#010x}")]
BadMagic { expected: u32, found: u32 },
#[error("unsupported index version {found}, this build supports up to {supported}")]
UnsupportedVersion { found: u16, supported: u16 },
#[error("index header checksum mismatch: expected {expected:#010x}, computed {computed:#010x}")]
ChecksumMismatch { expected: u32, computed: u32 },
#[error("non-zero bytes in index header padding")]
DirtyPadding,
#[error("index header section layout is invalid: {detail}")]
BadSectionLayout { detail: &'static str },
}
#[cfg(test)]
mod tests {
use super::*;
const GOLDEN: [u8; INDEX_HEADER_SIZE] = [
0x45, 0x56, 0x49, 0x58, 0x00, 0x00, 0x00, 0x00, 0x61, 0xAA, 0x78, 0xA6, 0xD1, 0x12, 0x15, 0xCD, 0x5B, 0x07, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x5A, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAF, 0xC8, 0xE3, 0x9F, ];
fn golden_header() -> IndexSegmentHeader {
IndexSegmentHeader {
version: 0,
created_at_nanos: 60 * 60 * 24 * 15695 * 1_000_000_000,
base_position: Position::new(123456789),
event_count: 3,
typedict_off: 70,
postings_off: 90,
fst_off: 100,
fst_len: 50,
body_crc: 0,
}
}
fn mutated(f: impl FnOnce(&mut [u8; INDEX_HEADER_SIZE])) -> [u8; INDEX_HEADER_SIZE] {
let mut buf = GOLDEN;
f(&mut buf);
let crc = crc32fast::hash(&buf[..OFF_HEADER_CRC]);
buf[OFF_HEADER_CRC..].copy_from_slice(&crc.to_le_bytes());
buf
}
#[test]
fn encode() {
assert_eq!(golden_header().to_bytes(), GOLDEN);
}
#[test]
fn decode() {
assert_eq!(
IndexSegmentHeader::from_bytes(&GOLDEN).unwrap(),
golden_header()
);
}
#[test]
fn round_trip() {
for h in [
golden_header(),
IndexSegmentHeader {
version: 0,
created_at_nanos: 0,
base_position: Position::new(1),
event_count: 0,
typedict_off: 64,
postings_off: 64,
fst_off: 64,
fst_len: 0,
body_crc: 0,
},
IndexSegmentHeader {
version: 0,
created_at_nanos: u64::MAX,
base_position: Position::new(1),
event_count: 10,
typedict_off: 84,
postings_off: 84,
fst_off: 200,
fst_len: 1000,
body_crc: 0xDEAD_BEEF,
},
] {
assert_eq!(IndexSegmentHeader::from_bytes(&h.to_bytes()).unwrap(), h);
}
}
#[test]
fn magic_reads_as_evix_on_disk() {
assert_eq!(&GOLDEN[OFF_MAGIC..OFF_MAGIC + SZ_MAGIC], b"EVIX");
}
#[test]
fn derived_positions_and_ranges() {
let h = golden_header();
assert_eq!(h.base_position, Position::new(123456789));
assert_eq!(h.max_position(), Some(Position::new(123456789 + 3 - 1)));
assert_eq!(h.type_column_range(), 64..70);
assert_eq!(h.type_dict_range(), 70..90);
assert_eq!(h.postings_range(), 90..100);
assert_eq!(h.fst_range(), 100..150);
assert_eq!(h.segment_len(), 150);
}
#[test]
fn empty_segment_has_no_max_position() {
let h = IndexSegmentHeader {
version: 0,
created_at_nanos: 0,
base_position: Position::new(1),
event_count: 0,
typedict_off: 64,
postings_off: 64,
fst_off: 64,
fst_len: 0,
body_crc: 0,
};
assert_eq!(h.max_position(), None);
assert_eq!(h.type_column_range(), 64..64);
}
#[test]
fn unwritten_header_is_not_corruption() {
let buf = [0u8; INDEX_HEADER_SIZE];
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::Unwritten)
));
}
#[test]
fn all_ones_is_checksum_mismatch() {
let buf = [0xFFu8; INDEX_HEADER_SIZE];
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::ChecksumMismatch { .. })
));
}
#[test]
fn bad_magic_rejected() {
let buf = mutated(|b| b[OFF_MAGIC] = b'X');
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::BadMagic { .. })
));
}
#[test]
fn future_version_rejected() {
let buf = mutated(|b| {
b[OFF_VERSION..OFF_VERSION + SZ_VERSION]
.copy_from_slice(&(IndexSegmentHeader::VERSION + 1).to_le_bytes())
});
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::UnsupportedVersion { found, supported })
if found == IndexSegmentHeader::VERSION + 1
&& supported == IndexSegmentHeader::VERSION
));
}
#[test]
fn dirty_padding_rejected() {
let buf = mutated(|b| b[OFF_PADDING] = 0x01);
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::DirtyPadding)
));
}
#[test]
fn type_column_offset_must_be_64() {
let buf = mutated(|b| {
b[OFF_TYPECOL_OFF..OFF_TYPECOL_OFF + SZ_SECTION].copy_from_slice(&12u32.to_le_bytes())
});
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::BadSectionLayout { .. })
));
}
#[test]
fn typedict_offset_must_match_event_count() {
let buf = mutated(|b| {
b[OFF_TYPEDICT_OFF..OFF_TYPEDICT_OFF + SZ_SECTION].copy_from_slice(&68u32.to_le_bytes())
});
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::BadSectionLayout { .. })
));
}
#[test]
fn non_monotonic_offsets_rejected() {
let buf = mutated(|b| {
b[OFF_POSTINGS_OFF..OFF_POSTINGS_OFF + SZ_SECTION].copy_from_slice(&65u32.to_le_bytes())
});
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::BadSectionLayout { .. })
));
}
#[test]
fn checksum_is_checked_before_fields() {
let mut buf = GOLDEN;
buf[OFF_MAGIC] = b'X';
assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::ChecksumMismatch { .. })
));
}
#[test]
fn checksum_covers_all_bytes_before_it() {
let mut buf = GOLDEN;
buf[OFF_HEADER_CRC - 1] ^= 0xFF; assert!(matches!(
IndexSegmentHeader::from_bytes(&buf),
Err(IndexHeaderError::ChecksumMismatch { .. })
));
}
#[test]
fn every_single_byte_flip_is_detected() {
for i in 0..INDEX_HEADER_SIZE {
for bit in 0..8 {
let mut buf = GOLDEN;
buf[i] ^= 1 << bit;
assert!(
IndexSegmentHeader::from_bytes(&buf).is_err(),
"flip at byte {i} bit {bit} was accepted"
);
}
}
}
#[test]
fn layout_offsets_are_sane() {
const {
assert!(OFF_MAGIC == 0);
assert!(OFF_EVENT_COUNT == 22);
assert!(OFF_BODY_CRC == 50);
assert!(OFF_PADDING <= OFF_HEADER_CRC);
assert!(OFF_HEADER_CRC + SZ_CRC == INDEX_HEADER_SIZE);
}
}
}