use crate::error::NsfError;
use crate::time::Timedate;
pub const BUCKET_SIGNATURE: u8 = 0x02;
pub const BUCKET_HEADER_BYTES: usize = 66;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BucketHeader {
pub header_size: u8,
pub size: u32,
pub modification_time: Timedate,
pub checksum: u32,
pub number_of_slots: u32,
pub footer_size: u32,
}
impl BucketHeader {
pub fn parse(bytes: &[u8]) -> Result<Self, NsfError> {
if bytes.len() < BUCKET_HEADER_BYTES {
return Err(NsfError::TooShort {
actual: bytes.len(),
required: BUCKET_HEADER_BYTES,
});
}
if bytes[0] != BUCKET_SIGNATURE {
return Err(NsfError::BadFileSignature {
observed: [bytes[0], 0],
});
}
let header_size = bytes[1];
let size = u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]);
let modification_time = Timedate::from_bytes(&bytes[10..18])?;
let checksum = u32::from_le_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]);
let number_of_slots =
u32::from_le_bytes([bytes[44], bytes[45], bytes[46], bytes[47]]);
let footer_size =
u32::from_le_bytes([bytes[50], bytes[51], bytes[52], bytes[53]]);
Ok(Self {
header_size,
size,
modification_time,
checksum,
number_of_slots,
footer_size,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BucketSlot {
pub offset: u16,
pub size: u16,
}
#[derive(Debug)]
pub struct Bucket<'a> {
header: BucketHeader,
bytes: &'a [u8],
}
impl<'a> Bucket<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, NsfError> {
let header = BucketHeader::parse(bytes)?;
let declared = header.size as usize;
let end = declared.min(bytes.len());
Ok(Self {
header,
bytes: &bytes[..end],
})
}
pub fn header(&self) -> &BucketHeader {
&self.header
}
pub fn slot_count(&self) -> u32 {
self.header.number_of_slots
}
pub fn slot_entry(&self, slot_index: u16) -> Result<BucketSlot, NsfError> {
let count = self.header.number_of_slots;
if slot_index == 0 || u32::from(slot_index) > count {
return Err(NsfError::SlotIndexOutOfRange {
requested: slot_index,
available: count,
});
}
let table_end = (self.bytes.len()).saturating_sub(self.header.footer_size as usize);
let ordinal = (slot_index - 1) as usize;
let entry_base = match table_end.checked_sub(4 * (ordinal + 1)) {
Some(b) => b,
None => {
return Err(NsfError::TooShort {
actual: self.bytes.len(),
required: 4 * (ordinal + 1),
})
}
};
let entry = self.bytes.get(entry_base..entry_base + 4).ok_or(NsfError::TooShort {
actual: self.bytes.len(),
required: entry_base + 4,
})?;
let offset = u16::from_le_bytes([entry[0], entry[1]]);
let size = u16::from_le_bytes([entry[2], entry[3]]);
Ok(BucketSlot { offset, size })
}
pub fn slot(&self, slot_index: u16) -> Result<&'a [u8], NsfError> {
let BucketSlot { offset, size } = self.slot_entry(slot_index)?;
let start = offset as usize;
let end = start + size as usize;
self.bytes.get(start..end).ok_or(NsfError::TooShort {
actual: self.bytes.len(),
required: end,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_bucket() -> Vec<u8> {
let mut buf = vec![0u8; 128];
buf[0] = BUCKET_SIGNATURE;
buf[1] = 0x42;
buf[6..10].copy_from_slice(&4096u32.to_le_bytes());
buf[44..48].copy_from_slice(&100u32.to_le_bytes());
buf[50..54].copy_from_slice(&12u32.to_le_bytes());
buf
}
#[test]
fn parses_synthetic_bucket() {
let buf = synthetic_bucket();
let h = BucketHeader::parse(&buf).unwrap();
assert_eq!(h.header_size, 0x42);
assert_eq!(h.size, 4096);
assert_eq!(h.number_of_slots, 100);
assert_eq!(h.footer_size, 12);
}
#[test]
fn rejects_bad_signature() {
let mut buf = synthetic_bucket();
buf[0] = 0xFF;
assert!(BucketHeader::parse(&buf).is_err());
}
fn synthetic_bucket_with_slots() -> Vec<u8> {
let mut buf = vec![0u8; 100];
buf[0] = BUCKET_SIGNATURE;
buf[1] = 0x42;
buf[6..10].copy_from_slice(&100u32.to_le_bytes()); buf[44..48].copy_from_slice(&2u32.to_le_bytes()); buf[50..54].copy_from_slice(&12u32.to_le_bytes()); buf[66..70].copy_from_slice(&[0xAA; 4]);
buf[70..73].copy_from_slice(&[0xBB; 3]);
buf[84..86].copy_from_slice(&66u16.to_le_bytes());
buf[86..88].copy_from_slice(&4u16.to_le_bytes());
buf[80..82].copy_from_slice(&70u16.to_le_bytes());
buf[82..84].copy_from_slice(&3u16.to_le_bytes());
buf
}
#[test]
fn bucket_resolves_one_based_slots() {
let buf = synthetic_bucket_with_slots();
let bucket = Bucket::parse(&buf).unwrap();
assert_eq!(bucket.slot_count(), 2);
assert_eq!(bucket.slot(1).unwrap(), &[0xAA; 4]);
assert_eq!(bucket.slot(2).unwrap(), &[0xBB; 3]);
}
#[test]
fn bucket_rejects_slot_index_zero() {
let buf = synthetic_bucket_with_slots();
let bucket = Bucket::parse(&buf).unwrap();
let err = bucket.slot(0).unwrap_err();
assert!(matches!(
err,
NsfError::SlotIndexOutOfRange {
requested: 0,
available: 2
}
));
}
#[test]
fn bucket_rejects_slot_index_past_end() {
let buf = synthetic_bucket_with_slots();
let bucket = Bucket::parse(&buf).unwrap();
let err = bucket.slot(3).unwrap_err();
assert!(matches!(
err,
NsfError::SlotIndexOutOfRange {
requested: 3,
available: 2
}
));
}
#[test]
fn bucket_clamps_view_to_declared_size() {
let mut buf = synthetic_bucket_with_slots();
buf.extend_from_slice(&[0x99; 64]);
let bucket = Bucket::parse(&buf).unwrap();
assert_eq!(bucket.slot(2).unwrap(), &[0xBB; 3]);
}
#[test]
fn bucket_slot_with_corrupt_offset_errors_not_panics() {
let mut buf = synthetic_bucket_with_slots();
buf[84..86].copy_from_slice(&250u16.to_le_bytes());
let bucket = Bucket::parse(&buf).unwrap();
assert!(matches!(bucket.slot(1), Err(NsfError::TooShort { .. })));
}
#[test]
fn bucket_slot_with_corrupt_size_overflow_errors_not_panics() {
let mut buf = synthetic_bucket_with_slots();
buf[86..88].copy_from_slice(&250u16.to_le_bytes());
let bucket = Bucket::parse(&buf).unwrap();
assert!(matches!(bucket.slot(1), Err(NsfError::TooShort { .. })));
}
#[test]
fn zero_slot_bucket_reports_no_slots() {
let mut buf = synthetic_bucket_with_slots();
buf[44..48].copy_from_slice(&0u32.to_le_bytes());
let bucket = Bucket::parse(&buf).unwrap();
assert_eq!(bucket.slot_count(), 0);
assert!(matches!(
bucket.slot(1),
Err(NsfError::SlotIndexOutOfRange {
requested: 1,
available: 0
})
));
}
}