use crate::error::NsfError;
use crate::time::Timedate;
pub const INFO2_FILE_OFFSET: usize = 520;
pub const INFO2_BYTES: usize = 124;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SuperblockSlot {
pub position_pages: u32,
pub size_bytes: u32,
}
impl SuperblockSlot {
pub fn is_empty(&self) -> bool {
self.position_pages == 0 || self.size_bytes == 0
}
pub fn byte_offset(&self) -> Option<u64> {
if self.is_empty() {
None
} else {
Some(u64::from(self.position_pages) * 256)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BdbSlot {
pub size_bytes: u32,
pub position_pages: u32,
}
impl BdbSlot {
pub fn is_empty(&self) -> bool {
self.position_pages == 0 || self.size_bytes == 0
}
pub fn byte_offset(&self) -> Option<u64> {
if self.is_empty() {
None
} else {
Some(u64::from(self.position_pages) * 256)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Information2 {
pub last_fixup_time: Timedate,
pub database_quota_limit: u32,
pub database_quota_warn_threshold: u32,
pub unknown_time1: Timedate,
pub unknown_time2: Timedate,
pub object_store_replica_identifier: Timedate,
pub superblocks: [SuperblockSlot; 4],
pub maximum_extension_granularity: u32,
pub summary_bucket_granularity: u16,
pub non_summary_bucket_granularity: u16,
pub minimum_summary_bucket_size: u32,
pub minimum_non_summary_bucket_size: u32,
pub maximum_summary_bucket_size: u32,
pub maximum_non_summary_bucket_size: u32,
pub non_summary_append_size: u16,
pub non_summary_append_factor: u16,
pub summary_bucket_fill_factor: u16,
pub non_summary_bucket_fill_factor: u16,
pub bdbs: [BdbSlot; 2],
}
impl Information2 {
pub fn parse(bytes: &[u8]) -> Result<Self, NsfError> {
if bytes.len() < INFO2_BYTES {
return Err(NsfError::TooShort {
actual: bytes.len(),
required: INFO2_BYTES,
});
}
let u16_at = |o: usize| u16::from_le_bytes([bytes[o], bytes[o + 1]]);
let u32_at = |o: usize| {
u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]])
};
let last_fixup_time = Timedate::from_bytes(&bytes[0..8])?;
let database_quota_limit = u32_at(8);
let database_quota_warn_threshold = u32_at(12);
let unknown_time1 = Timedate::from_bytes(&bytes[16..24])?;
let unknown_time2 = Timedate::from_bytes(&bytes[24..32])?;
let object_store_replica_identifier = Timedate::from_bytes(&bytes[32..40])?;
let superblocks = [
SuperblockSlot {
position_pages: u32_at(40),
size_bytes: u32_at(44),
},
SuperblockSlot {
position_pages: u32_at(48),
size_bytes: u32_at(52),
},
SuperblockSlot {
position_pages: u32_at(56),
size_bytes: u32_at(60),
},
SuperblockSlot {
position_pages: u32_at(64),
size_bytes: u32_at(68),
},
];
let maximum_extension_granularity = u32_at(72);
let summary_bucket_granularity = u16_at(76);
let non_summary_bucket_granularity = u16_at(78);
let minimum_summary_bucket_size = u32_at(80);
let minimum_non_summary_bucket_size = u32_at(84);
let maximum_summary_bucket_size = u32_at(88);
let maximum_non_summary_bucket_size = u32_at(92);
let non_summary_append_size = u16_at(96);
let non_summary_append_factor = u16_at(98);
let summary_bucket_fill_factor = u16_at(100);
let non_summary_bucket_fill_factor = u16_at(102);
let bdbs = [
BdbSlot {
size_bytes: u32_at(104),
position_pages: u32_at(108),
},
BdbSlot {
size_bytes: u32_at(112),
position_pages: u32_at(116),
},
];
Ok(Self {
last_fixup_time,
database_quota_limit,
database_quota_warn_threshold,
unknown_time1,
unknown_time2,
object_store_replica_identifier,
superblocks,
maximum_extension_granularity,
summary_bucket_granularity,
non_summary_bucket_granularity,
minimum_summary_bucket_size,
minimum_non_summary_bucket_size,
maximum_summary_bucket_size,
maximum_non_summary_bucket_size,
non_summary_append_size,
non_summary_append_factor,
summary_bucket_fill_factor,
non_summary_bucket_fill_factor,
bdbs,
})
}
pub fn populated_superblock_indices(&self) -> Vec<usize> {
self.superblocks
.iter()
.enumerate()
.filter_map(|(i, s)| if s.is_empty() { None } else { Some(i) })
.collect()
}
pub fn populated_bdb_indices(&self) -> Vec<usize> {
self.bdbs
.iter()
.enumerate()
.filter_map(|(i, s)| if s.is_empty() { None } else { Some(i) })
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic(superblock0_pages: u32, superblock0_size: u32, bdb0_pages: u32) -> Vec<u8> {
let mut buf = vec![0u8; INFO2_BYTES];
buf[40..44].copy_from_slice(&superblock0_pages.to_le_bytes());
buf[44..48].copy_from_slice(&superblock0_size.to_le_bytes());
buf[104..108].copy_from_slice(&512u32.to_le_bytes());
buf[108..112].copy_from_slice(&bdb0_pages.to_le_bytes());
buf[76..78].copy_from_slice(&8u16.to_le_bytes());
buf[78..80].copy_from_slice(&8u16.to_le_bytes());
buf
}
#[test]
fn parses_synthetic_with_one_populated_superblock_and_bdb() {
let buf = synthetic(0x100, 1024, 0x200);
let info = Information2::parse(&buf).unwrap();
assert_eq!(info.superblocks[0].position_pages, 0x100);
assert_eq!(info.superblocks[0].size_bytes, 1024);
assert_eq!(info.superblocks[0].byte_offset(), Some(0x100 * 256));
assert!(info.superblocks[1].is_empty());
assert!(info.superblocks[2].is_empty());
assert!(info.superblocks[3].is_empty());
assert_eq!(info.bdbs[0].position_pages, 0x200);
assert_eq!(info.bdbs[0].size_bytes, 512);
assert_eq!(info.bdbs[0].byte_offset(), Some(0x200 * 256));
assert!(info.bdbs[1].is_empty());
assert_eq!(info.populated_superblock_indices(), vec![0]);
assert_eq!(info.populated_bdb_indices(), vec![0]);
}
#[test]
fn rejects_short_buffer() {
let buf = vec![0u8; INFO2_BYTES - 1];
let err = Information2::parse(&buf).unwrap_err();
assert!(matches!(err, NsfError::TooShort { .. }));
}
#[test]
fn all_empty_returns_no_populated_slots() {
let buf = vec![0u8; INFO2_BYTES];
let info = Information2::parse(&buf).unwrap();
assert!(info.populated_superblock_indices().is_empty());
assert!(info.populated_bdb_indices().is_empty());
}
#[test]
fn superblock_byte_offset_scales_by_256() {
let slot = SuperblockSlot {
position_pages: 0x2AF0,
size_bytes: 256,
};
assert_eq!(slot.byte_offset(), Some(0x2AF0 * 256));
}
#[test]
fn empty_superblock_returns_none_offset() {
let slot = SuperblockSlot {
position_pages: 0,
size_bytes: 0,
};
assert_eq!(slot.byte_offset(), None);
}
#[test]
fn four_distinct_superblocks_all_parsed() {
let mut buf = vec![0u8; INFO2_BYTES];
for i in 0..4 {
let offset = 40 + i * 8;
let pos = (0x100 * (i as u32 + 1)).to_le_bytes();
let size = (1024u32).to_le_bytes();
buf[offset..offset + 4].copy_from_slice(&pos);
buf[offset + 4..offset + 8].copy_from_slice(&size);
}
let info = Information2::parse(&buf).unwrap();
for i in 0..4 {
assert_eq!(info.superblocks[i].position_pages, 0x100 * (i as u32 + 1));
assert_eq!(info.superblocks[i].size_bytes, 1024);
}
assert_eq!(info.populated_superblock_indices(), vec![0, 1, 2, 3]);
}
}