use crate::cursor::ManifestCursor;
use crate::error::CoreError;
use limnifs_format::{SlabId, SLAB_MAGIC};
pub const DEFAULT_SLAB_MAX_BYTES: u64 = 64 * 1024 * 1024;
pub const SLAB_FORMAT_VERSION: u16 = 1;
pub const SLAB_HEADER_LEN: usize = 56;
pub const EC_DESCRIPTOR_EXTENDED: u8 = 0xFF;
pub const CRYPTO_HINT_EXTENDED: u8 = 0xFF;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct SlabHeader {
pub format_version: u16,
pub slab_id: SlabId,
pub total_length: u64,
pub ec_descriptor: u8,
pub crypto_hint: u8,
}
impl SlabHeader {
#[must_use]
pub const fn has_erasure_coding(self) -> bool {
self.ec_descriptor != 0x00 && self.ec_descriptor != EC_DESCRIPTOR_EXTENDED
}
#[must_use]
pub const fn is_sealed(self) -> bool {
self.crypto_hint != 0x00 && self.crypto_hint != CRYPTO_HINT_EXTENDED
}
}
pub fn parse_slab_header(cursor: &mut ManifestCursor<'_>) -> Result<SlabHeader, CoreError> {
parse_slab_header_with_ceiling(cursor, DEFAULT_SLAB_MAX_BYTES)
}
pub fn parse_slab_header_with_ceiling(
cursor: &mut ManifestCursor<'_>,
max_total_length: u64,
) -> Result<SlabHeader, CoreError> {
let magic = cursor.read_magic()?;
if magic != SLAB_MAGIC {
return Err(CoreError::Corrupt {
reason: format!(
"bad slab magic: expected LIM1 ({:x?}), found {:x?}",
SLAB_MAGIC, magic
),
});
}
let format_version = cursor.read_u16_le()?;
if format_version != SLAB_FORMAT_VERSION {
return Err(CoreError::UnsupportedFeature {
feature: format!(
"slab format_version {format_version} (supported: {SLAB_FORMAT_VERSION})"
),
});
}
let ordinal = cursor.read_u64_le()?;
let hash = cursor.read_n(32)?;
let mut hash_array = [0u8; 32];
hash_array.copy_from_slice(hash);
let slab_id = SlabId::new(ordinal, hash_array);
let total_length = cursor.read_u64_le()?;
if total_length < u64::try_from(SLAB_HEADER_LEN).unwrap_or(u64::MAX) {
return Err(CoreError::Corrupt {
reason: format!(
"slab total_length {total_length} is less than header width {SLAB_HEADER_LEN}"
),
});
}
if total_length > max_total_length {
return Err(CoreError::Corrupt {
reason: format!(
"slab total_length {total_length} exceeds configured ceiling {max_total_length}"
),
});
}
let ec_descriptor = cursor.read_u8()?;
if ec_descriptor == EC_DESCRIPTOR_EXTENDED {
return Err(CoreError::UnsupportedFeature {
feature: "slab ec_descriptor 0xFF (extended descriptor, post-v1)".into(),
});
}
let crypto_hint = cursor.read_u8()?;
if crypto_hint == CRYPTO_HINT_EXTENDED {
return Err(CoreError::UnsupportedFeature {
feature: "slab crypto_hint 0xFF (extended hint, post-v1)".into(),
});
}
Ok(SlabHeader {
format_version,
slab_id,
total_length,
ec_descriptor,
crypto_hint,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_slab_header_bytes(
format_version: u16,
slab_id: SlabId,
total_length: u64,
ec_descriptor: u8,
crypto_hint: u8,
) -> [u8; SLAB_HEADER_LEN] {
let mut bytes = [0u8; SLAB_HEADER_LEN];
bytes[..4].copy_from_slice(&SLAB_MAGIC);
bytes[4..6].copy_from_slice(&format_version.to_le_bytes());
let slab_id_bytes = slab_id.to_bytes();
bytes[6..46].copy_from_slice(&slab_id_bytes);
bytes[46..54].copy_from_slice(&total_length.to_le_bytes());
bytes[54] = ec_descriptor;
bytes[55] = crypto_hint;
bytes
}
fn sample_slab_id() -> SlabId {
SlabId::new(7, [0xAA; 32])
}
#[test]
fn parses_current_plaintext_header() {
let bytes = make_slab_header_bytes(1, sample_slab_id(), 4096, 0x00, 0x00);
let mut cursor = ManifestCursor::new(&bytes);
let header = parse_slab_header(&mut cursor).expect("current header parses");
assert_eq!(header.format_version, 1);
assert_eq!(header.slab_id, sample_slab_id());
assert_eq!(header.total_length, 4096);
assert_eq!(header.ec_descriptor, 0x00);
assert_eq!(header.crypto_hint, 0x00);
assert!(!header.has_erasure_coding());
assert!(!header.is_sealed());
assert_eq!(cursor.position(), SLAB_HEADER_LEN);
}
#[test]
fn parses_ec_enabled_sealed_header() {
let bytes = make_slab_header_bytes(1, sample_slab_id(), 16_384, 0x01, 0x01);
let mut cursor = ManifestCursor::new(&bytes);
let header = parse_slab_header(&mut cursor).expect("EC + sealed parses");
assert!(header.has_erasure_coding());
assert!(header.is_sealed());
}
#[test]
fn rejects_bad_magic() {
let mut bytes = make_slab_header_bytes(1, sample_slab_id(), 4096, 0, 0);
bytes[0] = b'X';
let mut cursor = ManifestCursor::new(&bytes);
match parse_slab_header(&mut cursor) {
Err(CoreError::Corrupt { reason }) => {
assert!(reason.contains("bad slab magic"), "got {reason}");
}
other => panic!("expected Corrupt, got {other:?}"),
}
}
#[test]
fn rejects_unknown_format_version() {
let bytes = make_slab_header_bytes(7, sample_slab_id(), 4096, 0, 0);
let mut cursor = ManifestCursor::new(&bytes);
match parse_slab_header(&mut cursor) {
Err(CoreError::UnsupportedFeature { feature }) => {
assert!(feature.contains("format_version 7"), "got: {feature}");
}
other => panic!("expected UnsupportedFeature, got {other:?}"),
}
}
#[test]
fn rejects_total_length_below_header_width() {
let bytes = make_slab_header_bytes(1, sample_slab_id(), 32, 0, 0);
let mut cursor = ManifestCursor::new(&bytes);
match parse_slab_header(&mut cursor) {
Err(CoreError::Corrupt { reason }) => {
assert!(reason.contains("header width"), "got: {reason}");
}
other => panic!("expected Corrupt, got {other:?}"),
}
}
#[test]
fn rejects_total_length_above_default_ceiling() {
let bytes = make_slab_header_bytes(1, sample_slab_id(), DEFAULT_SLAB_MAX_BYTES + 1, 0, 0);
let mut cursor = ManifestCursor::new(&bytes);
match parse_slab_header(&mut cursor) {
Err(CoreError::Corrupt { reason }) => {
assert!(reason.contains("ceiling"), "got: {reason}");
}
other => panic!("expected Corrupt, got {other:?}"),
}
}
#[test]
fn custom_ceiling_accepts_oversized_slab() {
let bytes = make_slab_header_bytes(1, sample_slab_id(), DEFAULT_SLAB_MAX_BYTES + 1, 0, 0);
let mut cursor = ManifestCursor::new(&bytes);
let header = parse_slab_header_with_ceiling(&mut cursor, DEFAULT_SLAB_MAX_BYTES * 2)
.expect("custom ceiling accepts");
assert_eq!(header.total_length, DEFAULT_SLAB_MAX_BYTES + 1);
}
#[test]
fn rejects_extended_ec_descriptor() {
let bytes = make_slab_header_bytes(1, sample_slab_id(), 4096, 0xFF, 0);
let mut cursor = ManifestCursor::new(&bytes);
match parse_slab_header(&mut cursor) {
Err(CoreError::UnsupportedFeature { feature }) => {
assert!(feature.contains("ec_descriptor"), "got: {feature}");
}
other => panic!("expected UnsupportedFeature, got {other:?}"),
}
}
#[test]
fn rejects_extended_crypto_hint() {
let bytes = make_slab_header_bytes(1, sample_slab_id(), 4096, 0, 0xFF);
let mut cursor = ManifestCursor::new(&bytes);
match parse_slab_header(&mut cursor) {
Err(CoreError::UnsupportedFeature { feature }) => {
assert!(feature.contains("crypto_hint"), "got: {feature}");
}
other => panic!("expected UnsupportedFeature, got {other:?}"),
}
}
#[test]
fn rejects_truncated_header() {
let mut bytes = [0u8; 50];
bytes[..4].copy_from_slice(&SLAB_MAGIC);
bytes[4..6].copy_from_slice(&SLAB_FORMAT_VERSION.to_le_bytes());
let mut cursor = ManifestCursor::new(&bytes);
match parse_slab_header(&mut cursor) {
Err(CoreError::TooShort { .. }) => {}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn round_trip_via_constructor() {
let header = SlabHeader {
format_version: SLAB_FORMAT_VERSION,
slab_id: sample_slab_id(),
total_length: 8192,
ec_descriptor: 0x01,
crypto_hint: 0x01,
};
let bytes = make_slab_header_bytes(
header.format_version,
header.slab_id,
header.total_length,
header.ec_descriptor,
header.crypto_hint,
);
let mut cursor = ManifestCursor::new(&bytes);
let reparsed = parse_slab_header(&mut cursor).expect("roundtrip");
assert_eq!(reparsed, header);
}
}