use crate::error::NsfError;
use crate::superblock::Superblock;
pub const SUMMARY_DESCRIPTOR_BYTES: usize = 14;
pub const NON_SUMMARY_DESCRIPTOR_BYTES: usize = 6;
const SUMMARY_PAGE_PREFIX: usize = 224;
const SUMMARY_PAGE_BYTES: usize = 8206;
const NON_SUMMARY_PAGE_PREFIX: usize = 70;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BucketDescriptorTable {
pub summary: Vec<u64>,
pub non_summary: Vec<u64>,
}
impl BucketDescriptorTable {
pub fn parse(body: &[u8], sb: &Superblock) -> Result<Self, NsfError> {
let u32_at = |buf: &[u8], o: usize| -> Option<u32> {
buf.get(o..o + 4)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
};
let mut cursor = 0usize;
let mut summary = Vec::new();
if sb.number_of_summary_bucket_descriptor_pages > 0 {
let array_start = cursor + SUMMARY_PAGE_PREFIX;
let count = sb.number_of_summary_buckets as usize;
summary.reserve(count);
for i in 0..count {
let off = array_start + i * SUMMARY_DESCRIPTOR_BYTES;
let fp = u32_at(body, off).ok_or(NsfError::TooShort {
actual: body.len(),
required: off + 4,
})?;
summary.push(u64::from(fp) << 8);
}
cursor += SUMMARY_PAGE_BYTES;
}
let mut non_summary = Vec::new();
if sb.number_of_non_summary_bucket_descriptor_pages > 0 {
let array_start = cursor + NON_SUMMARY_PAGE_PREFIX;
let count = sb.number_of_non_summary_buckets as usize;
non_summary.reserve(count);
for i in 0..count {
let off = array_start + i * NON_SUMMARY_DESCRIPTOR_BYTES;
let fp = u32_at(body, off).ok_or(NsfError::TooShort {
actual: body.len(),
required: off + 4,
})?;
non_summary.push(u64::from(fp) << 8);
}
}
Ok(Self {
summary,
non_summary,
})
}
pub fn summary_bucket_offset(&self, bucket_index: u32) -> Result<u64, NsfError> {
Self::lookup(&self.summary, bucket_index)
}
pub fn non_summary_bucket_offset(&self, bucket_index: u32) -> Result<u64, NsfError> {
Self::lookup(&self.non_summary, bucket_index)
}
fn lookup(map: &[u64], bucket_index: u32) -> Result<u64, NsfError> {
if bucket_index == 0 {
return Err(NsfError::BucketIndexOutOfRange {
requested: 0,
available: map.len(),
});
}
let ordinal = (bucket_index - 1) as usize;
map.get(ordinal)
.copied()
.ok_or(NsfError::BucketIndexOutOfRange {
requested: bucket_index,
available: map.len(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::superblock::{Superblock, SUPERBLOCK_HEADER_BYTES, SUPERBLOCK_SIGNATURE};
fn superblock_with_counts(
summary_pages: u32,
summary_buckets: u32,
non_summary_pages: u32,
non_summary_buckets: u32,
) -> Superblock {
let mut buf = vec![0u8; SUPERBLOCK_HEADER_BYTES];
buf[0..2].copy_from_slice(&SUPERBLOCK_SIGNATURE);
buf[14..18].copy_from_slice(&summary_buckets.to_le_bytes());
buf[18..22].copy_from_slice(&non_summary_buckets.to_le_bytes());
buf[70..74].copy_from_slice(&summary_pages.to_le_bytes());
buf[74..78].copy_from_slice(&non_summary_pages.to_le_bytes());
Superblock::parse(&buf).unwrap()
}
fn synthetic_body(summary_buckets: u32, non_summary_buckets: u32) -> Vec<u8> {
let mut body = Vec::new();
if summary_buckets > 0 {
let mut page = vec![0u8; SUMMARY_PAGE_BYTES];
for i in 0..summary_buckets as usize {
let off = SUMMARY_PAGE_PREFIX + i * SUMMARY_DESCRIPTOR_BYTES;
let fp = 0x100u32 + i as u32;
page[off..off + 4].copy_from_slice(&fp.to_le_bytes());
}
body.extend_from_slice(&page);
}
if non_summary_buckets > 0 {
let mut page = vec![
0u8;
NON_SUMMARY_PAGE_PREFIX
+ non_summary_buckets as usize * NON_SUMMARY_DESCRIPTOR_BYTES
];
for i in 0..non_summary_buckets as usize {
let off = NON_SUMMARY_PAGE_PREFIX + i * NON_SUMMARY_DESCRIPTOR_BYTES;
let fp = 0x900u32 + i as u32;
page[off..off + 4].copy_from_slice(&fp.to_le_bytes());
}
body.extend_from_slice(&page);
}
body
}
#[test]
fn parses_summary_descriptor_array() {
let sb = superblock_with_counts(1, 3, 0, 0);
let body = synthetic_body(3, 0);
let bdt = BucketDescriptorTable::parse(&body, &sb).unwrap();
assert_eq!(bdt.summary.len(), 3);
assert_eq!(bdt.summary[0], 0x100u64 << 8);
assert_eq!(bdt.summary[1], 0x101u64 << 8);
assert_eq!(bdt.summary[2], 0x102u64 << 8);
assert!(bdt.non_summary.is_empty());
}
#[test]
fn parses_both_pages_with_correct_offsets() {
let sb = superblock_with_counts(1, 2, 1, 2);
let body = synthetic_body(2, 2);
let bdt = BucketDescriptorTable::parse(&body, &sb).unwrap();
assert_eq!(bdt.summary.len(), 2);
assert_eq!(bdt.non_summary.len(), 2);
assert_eq!(bdt.non_summary[0], 0x900u64 << 8);
assert_eq!(bdt.non_summary[1], 0x901u64 << 8);
}
#[test]
fn summary_offset_is_one_based() {
let sb = superblock_with_counts(1, 3, 0, 0);
let body = synthetic_body(3, 0);
let bdt = BucketDescriptorTable::parse(&body, &sb).unwrap();
assert_eq!(bdt.summary_bucket_offset(1).unwrap(), 0x100u64 << 8);
assert_eq!(bdt.summary_bucket_offset(3).unwrap(), 0x102u64 << 8);
}
#[test]
fn bucket_index_zero_is_rejected() {
let sb = superblock_with_counts(1, 1, 0, 0);
let body = synthetic_body(1, 0);
let bdt = BucketDescriptorTable::parse(&body, &sb).unwrap();
assert!(matches!(
bdt.summary_bucket_offset(0),
Err(NsfError::BucketIndexOutOfRange { requested: 0, .. })
));
}
#[test]
fn bucket_index_past_end_is_rejected() {
let sb = superblock_with_counts(1, 2, 0, 0);
let body = synthetic_body(2, 0);
let bdt = BucketDescriptorTable::parse(&body, &sb).unwrap();
assert!(matches!(
bdt.summary_bucket_offset(3),
Err(NsfError::BucketIndexOutOfRange {
requested: 3,
available: 2
})
));
}
#[test]
fn no_descriptor_pages_yields_empty_maps() {
let sb = superblock_with_counts(0, 0, 0, 0);
let bdt = BucketDescriptorTable::parse(&[], &sb).unwrap();
assert!(bdt.summary.is_empty());
assert!(bdt.non_summary.is_empty());
}
#[test]
fn non_summary_only_starts_at_body_offset_zero() {
let sb = superblock_with_counts(0, 0, 1, 2);
let body = synthetic_body(0, 2);
let bdt = BucketDescriptorTable::parse(&body, &sb).unwrap();
assert!(bdt.summary.is_empty());
assert_eq!(bdt.non_summary.len(), 2);
assert_eq!(bdt.non_summary[0], 0x900u64 << 8);
assert_eq!(bdt.non_summary[1], 0x901u64 << 8);
}
#[test]
fn truncated_body_errors_not_panics() {
let sb = superblock_with_counts(1, 3, 0, 0);
let body = vec![0u8; SUMMARY_PAGE_PREFIX + SUMMARY_DESCRIPTOR_BYTES];
assert!(matches!(
BucketDescriptorTable::parse(&body, &sb),
Err(NsfError::TooShort { .. })
));
}
}