use std::mem::{offset_of, size_of};
pub const PAGE_SIZE: u32 = 0x80000;
pub const HEADER_SIZE: u32 = 0x1000;
pub const MAX_SLOTS: u32 = 0x2800;
pub const SLOT_TABLE_SIZE: u32 = MAX_SLOTS * 4;
pub const DATA_AREA_START: u32 = HEADER_SIZE + SLOT_TABLE_SIZE;
const _: () = assert!(DATA_AREA_START == 0xB000);
pub type BlobGuid = [u8; 16];
pub(crate) const ROOT_BLOB_GUID: BlobGuid = [0; 16];
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct BlobHeader {
_pad_0: [u8; 0x50],
pub field_50: u16,
pub field_52: u16,
pub num_slots: u16,
pub root_slot: u16,
pub space_used: u32,
pub num_ext_blobs: u16,
pub field_5e: u16,
pub compact_times: u32,
pub dead_bytes: u32,
pub gap_space: u32,
pub tombstone_leaf_cnt: u32,
pub free_list_head: [u16; 8],
pub created_epoch: u64,
pub epoch_high_water: u64,
_pad_90: [u8; 0x10],
pub blob_guid: BlobGuid,
pub routing_off: u32,
pub routing_len: u32,
pub leaf_region_start: u32,
pub routing_unfit: u32,
pub bloom_off: u32,
pub bloom_len: u32,
pub bloom_bits_per_key: u32,
_pad_cc: [u8; (HEADER_SIZE as usize) - 0xcc],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RoutingRegion {
pub off: u32,
pub leaf_region_start: u32,
}
impl BlobHeader {
#[must_use]
pub fn routing_region(&self) -> Option<RoutingRegion> {
if self.routing_len == 0 {
return None;
}
let off = self.routing_off;
let lrs = self.leaf_region_start;
let routing_end = off.checked_add(self.routing_len)?;
if off < DATA_AREA_START || off >= lrs || lrs > PAGE_SIZE || routing_end > lrs {
return None;
}
Some(RoutingRegion {
off,
leaf_region_start: lrs,
})
}
#[must_use]
pub fn bloom_region(&self) -> Option<(u32, u32, u8)> {
if self.bloom_len == 0 {
return None;
}
let off = self.bloom_off;
let end = off.checked_add(self.bloom_len)?;
if off < DATA_AREA_START || end > self.leaf_region_start {
return None;
}
let bpk = u8::try_from(self.bloom_bits_per_key).ok()?;
if bpk == 0 {
return None;
}
Some((off, self.bloom_len, bpk))
}
}
const _: () = assert!(size_of::<BlobHeader>() == HEADER_SIZE as usize);
const _: () = assert!(offset_of!(BlobHeader, field_50) == 0x50);
const _: () = assert!(offset_of!(BlobHeader, num_slots) == 0x54);
const _: () = assert!(offset_of!(BlobHeader, root_slot) == 0x56);
const _: () = assert!(offset_of!(BlobHeader, space_used) == 0x58);
const _: () = assert!(offset_of!(BlobHeader, num_ext_blobs) == 0x5c);
const _: () = assert!(offset_of!(BlobHeader, compact_times) == 0x60);
const _: () = assert!(offset_of!(BlobHeader, dead_bytes) == 0x64);
const _: () = assert!(offset_of!(BlobHeader, gap_space) == 0x68);
const _: () = assert!(offset_of!(BlobHeader, tombstone_leaf_cnt) == 0x6c);
const _: () = assert!(offset_of!(BlobHeader, free_list_head) == 0x70);
const _: () = assert!(offset_of!(BlobHeader, created_epoch) == 0x80);
const _: () = assert!(offset_of!(BlobHeader, epoch_high_water) == 0x88);
const _: () = assert!(offset_of!(BlobHeader, blob_guid) == 0xa0);
const _: () = assert!(offset_of!(BlobHeader, routing_off) == 0xb0);
const _: () = assert!(offset_of!(BlobHeader, routing_len) == 0xb4);
const _: () = assert!(offset_of!(BlobHeader, leaf_region_start) == 0xb8);
const _: () = assert!(offset_of!(BlobHeader, routing_unfit) == 0xbc);
const _: () = assert!(offset_of!(BlobHeader, bloom_off) == 0xc0);
const _: () = assert!(offset_of!(BlobHeader, bloom_len) == 0xc4);
const _: () = assert!(offset_of!(BlobHeader, bloom_bits_per_key) == 0xc8);
pub const CREATED_EPOCH_OFFSET: usize = offset_of!(BlobHeader, created_epoch);
#[inline]
pub fn set_frame_created_epoch(buf: &mut [u8], epoch: u64) {
buf[CREATED_EPOCH_OFFSET..CREATED_EPOCH_OFFSET + size_of::<u64>()]
.copy_from_slice(&epoch.to_ne_bytes());
}
#[inline]
#[must_use]
pub fn frame_created_epoch(buf: &[u8]) -> u64 {
u64::from_ne_bytes(
buf[CREATED_EPOCH_OFFSET..CREATED_EPOCH_OFFSET + size_of::<u64>()]
.try_into()
.expect("frame buffer is at least HEADER_SIZE bytes"),
)
}
pub const EPOCH_HIGH_WATER_OFFSET: usize = offset_of!(BlobHeader, epoch_high_water);
#[inline]
pub fn set_frame_epoch_high_water(buf: &mut [u8], epoch: u64) {
buf[EPOCH_HIGH_WATER_OFFSET..EPOCH_HIGH_WATER_OFFSET + size_of::<u64>()]
.copy_from_slice(&epoch.to_ne_bytes());
}
#[inline]
#[must_use]
pub fn frame_epoch_high_water(buf: &[u8]) -> u64 {
u64::from_ne_bytes(
buf[EPOCH_HIGH_WATER_OFFSET..EPOCH_HIGH_WATER_OFFSET + size_of::<u64>()]
.try_into()
.expect("frame buffer is at least HEADER_SIZE bytes"),
)
}
pub const BLOB_GUID_OFFSET: usize = offset_of!(BlobHeader, blob_guid);
#[inline]
pub fn set_frame_blob_guid(buf: &mut [u8], guid: BlobGuid) {
buf[BLOB_GUID_OFFSET..BLOB_GUID_OFFSET + size_of::<BlobGuid>()]
.copy_from_slice(guid.as_slice());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_size_and_offsets() {
assert_eq!(size_of::<BlobHeader>(), 4096);
assert_eq!(offset_of!(BlobHeader, num_slots), 0x54);
assert_eq!(offset_of!(BlobHeader, root_slot), 0x56);
assert_eq!(offset_of!(BlobHeader, space_used), 0x58);
assert_eq!(offset_of!(BlobHeader, compact_times), 0x60);
assert_eq!(offset_of!(BlobHeader, gap_space), 0x68);
assert_eq!(offset_of!(BlobHeader, tombstone_leaf_cnt), 0x6c);
assert_eq!(offset_of!(BlobHeader, free_list_head), 0x70);
assert_eq!(offset_of!(BlobHeader, created_epoch), 0x80);
assert_eq!(offset_of!(BlobHeader, epoch_high_water), 0x88);
assert_eq!(offset_of!(BlobHeader, blob_guid), 0xa0);
}
#[test]
fn created_epoch_round_trips_through_buffer() {
let mut buf = vec![0u8; PAGE_SIZE as usize];
let span = CREATED_EPOCH_OFFSET..CREATED_EPOCH_OFFSET + 8;
assert_eq!(&buf[span.clone()], &[0u8; 8]);
set_frame_created_epoch(&mut buf, 0x1234_5678_9abc_def0);
assert_eq!(&buf[span], &0x1234_5678_9abc_def0_u64.to_ne_bytes());
assert_eq!(&buf[0xa0..0xb0], &[0u8; 16]);
}
#[test]
fn constants_consistent() {
assert_eq!(PAGE_SIZE, 524_288);
assert_eq!(HEADER_SIZE, 4096);
assert_eq!(MAX_SLOTS, 10_240);
assert_eq!(DATA_AREA_START, 0xB000);
}
#[test]
fn zeroed_header_is_legacy_layout() {
let header: BlobHeader = unsafe { core::mem::zeroed() };
assert_eq!(header.routing_off, 0);
assert_eq!(header.routing_len, 0);
assert_eq!(header.leaf_region_start, 0);
assert_eq!(header.routing_region(), None);
let routed = BlobHeader {
routing_len: 0x40,
routing_off: DATA_AREA_START,
leaf_region_start: DATA_AREA_START + 0x40,
..header
};
assert_eq!(
routed.routing_region(),
Some(RoutingRegion {
off: DATA_AREA_START,
leaf_region_start: DATA_AREA_START + 0x40,
})
);
}
#[test]
fn corrupt_routing_bounds_report_legacy() {
let base = BlobHeader {
routing_len: 0x40,
routing_off: DATA_AREA_START,
leaf_region_start: DATA_AREA_START + 0x40,
..unsafe { core::mem::zeroed::<BlobHeader>() }
};
let assert_legacy = |h: Box<BlobHeader>, label: &str| {
assert_eq!(h.routing_region(), None, "{label} must be legacy");
};
assert_legacy(
Box::new(BlobHeader {
routing_off: DATA_AREA_START - 8,
..base
}),
"routing_off before data area",
);
assert_legacy(
Box::new(BlobHeader {
leaf_region_start: PAGE_SIZE + 8,
..base
}),
"leaf_region_start past frame",
);
assert_legacy(
Box::new(BlobHeader {
leaf_region_start: DATA_AREA_START,
..base
}),
"leaf_region_start <= routing_off",
);
assert_legacy(
Box::new(BlobHeader {
routing_len: 0x80,
leaf_region_start: DATA_AREA_START + 0x40,
..base
}),
"routing_len past leaf region",
);
assert_legacy(
Box::new(BlobHeader {
routing_off: u32::MAX - 4,
routing_len: 0x40,
..base
}),
"routing_off + routing_len overflow",
);
}
}