use crate::error::NsfError;
pub const RRV_BUCKET_SIGNATURE: u8 = 0x06;
pub const RRV_BUCKET_HEADER_BYTES: usize = 32;
pub const RRV_ENTRY_BYTES: usize = 8;
const FILE_POSITION_EMPTY_ALT: u32 = 0x7FFF_FFFF;
const BUCKET_INDEX_EMPTY_ALT: u32 = 0x00FF_FFFF;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RrvBucketHeader {
pub header_size: u8,
pub initial_rrv_identifier: u32,
pub checksum: u32,
}
impl RrvBucketHeader {
pub fn parse(bytes: &[u8]) -> Result<Self, NsfError> {
if bytes.len() < RRV_BUCKET_HEADER_BYTES {
return Err(NsfError::TooShort {
actual: bytes.len(),
required: RRV_BUCKET_HEADER_BYTES,
});
}
if bytes[0] != RRV_BUCKET_SIGNATURE {
return Err(NsfError::BadFileSignature {
observed: [bytes[0], 0],
});
}
if bytes[1] != 0x20 {
return Err(NsfError::BadHeaderSize {
size: bytes[1] as u32,
});
}
let initial_rrv_identifier =
u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]);
let checksum = u32::from_le_bytes([bytes[18], bytes[19], bytes[20], bytes[21]]);
Ok(Self {
header_size: bytes[1],
initial_rrv_identifier,
checksum,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RrvLocation {
BucketSlot {
bucket_index: u32,
slot_index: u16,
nonsum: u32,
},
FilePosition {
file_position_pages: u32,
},
}
impl RrvLocation {
pub fn file_byte_offset(&self) -> Option<u64> {
match self {
Self::FilePosition {
file_position_pages,
} => Some(u64::from(*file_position_pages) * 256),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RrvEntry {
pub rrv_identifier: u32,
pub location: RrvLocation,
}
pub struct RrvIter<'a> {
next_rrv_identifier: u32,
remaining: &'a [u8],
}
impl<'a> RrvIter<'a> {
pub fn new(bucket: &'a [u8]) -> Result<(RrvBucketHeader, Self), NsfError> {
let header = RrvBucketHeader::parse(bucket)?;
let entry_data = &bucket[RRV_BUCKET_HEADER_BYTES..];
Ok((
header,
Self {
next_rrv_identifier: header.initial_rrv_identifier,
remaining: entry_data,
},
))
}
}
impl<'a> Iterator for RrvIter<'a> {
type Item = RrvEntry;
fn next(&mut self) -> Option<Self::Item> {
while self.remaining.len() >= RRV_ENTRY_BYTES {
let rrv_entry = u32::from_le_bytes([
self.remaining[0],
self.remaining[1],
self.remaining[2],
self.remaining[3],
]);
let rrv_entry_bsid = u32::from_le_bytes([
self.remaining[4],
self.remaining[5],
self.remaining[6],
self.remaining[7],
]);
self.remaining = &self.remaining[RRV_ENTRY_BYTES..];
let identifier = self.next_rrv_identifier;
self.next_rrv_identifier = self.next_rrv_identifier.wrapping_add(4);
if (rrv_entry & 0x8000_0000) == 0 {
if rrv_entry == 0 || rrv_entry == FILE_POSITION_EMPTY_ALT {
continue;
}
return Some(RrvEntry {
rrv_identifier: identifier,
location: RrvLocation::FilePosition {
file_position_pages: rrv_entry,
},
});
} else {
let bucket_index = rrv_entry & 0x00FF_FFFF;
if bucket_index == 0 || bucket_index == BUCKET_INDEX_EMPTY_ALT {
continue;
}
let nonsum_high = (rrv_entry >> 7) & 0x00E0_0000;
let slot_index = (rrv_entry_bsid & 0x0000_07FF) as u16;
let nonsum_low = rrv_entry_bsid >> 11;
let nonsum = nonsum_high | nonsum_low;
return Some(RrvEntry {
rrv_identifier: identifier,
location: RrvLocation::BucketSlot {
bucket_index,
slot_index,
nonsum,
},
});
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_bucket_with_entries(entries: &[(u32, u32)]) -> Vec<u8> {
let mut buf = vec![0u8; RRV_BUCKET_HEADER_BYTES + entries.len() * RRV_ENTRY_BYTES];
buf[0] = RRV_BUCKET_SIGNATURE;
buf[1] = 0x20;
buf[6..10].copy_from_slice(&100u32.to_le_bytes());
for (i, (a, b)) in entries.iter().enumerate() {
let off = RRV_BUCKET_HEADER_BYTES + i * RRV_ENTRY_BYTES;
buf[off..off + 4].copy_from_slice(&a.to_le_bytes());
buf[off + 4..off + 8].copy_from_slice(&b.to_le_bytes());
}
buf
}
#[test]
fn parses_header_signature() {
let buf = synthetic_bucket_with_entries(&[]);
let h = RrvBucketHeader::parse(&buf).unwrap();
assert_eq!(h.header_size, 0x20);
assert_eq!(h.initial_rrv_identifier, 100);
}
#[test]
fn skips_zero_and_alternate_empty_markers_in_file_position_variant() {
let buf = synthetic_bucket_with_entries(&[
(0, 0),
(FILE_POSITION_EMPTY_ALT, 0),
(0x0000_2AF0, 0),
]);
let (_, iter) = RrvIter::new(&buf).unwrap();
let entries: Vec<_> = iter.collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].rrv_identifier, 108);
assert!(matches!(
entries[0].location,
RrvLocation::FilePosition {
file_position_pages: 0x0000_2AF0
}
));
assert_eq!(
entries[0].location.file_byte_offset(),
Some(0x0000_2AF0 * 256)
);
}
#[test]
fn parses_bucket_slot_variant_bit_layout() {
let bucket_index: u32 = 0x0012_3456;
let slot_index: u32 = 0x01AB;
let rrv_entry: u32 = 0x8000_0000 | bucket_index;
let nonsum_low: u32 = 0x0001_2345; let rrv_entry_bsid: u32 = (nonsum_low << 11) | slot_index;
let buf = synthetic_bucket_with_entries(&[(rrv_entry, rrv_entry_bsid)]);
let (_, iter) = RrvIter::new(&buf).unwrap();
let entries: Vec<_> = iter.collect();
assert_eq!(entries.len(), 1);
match entries[0].location {
RrvLocation::BucketSlot {
bucket_index: b,
slot_index: s,
nonsum,
} => {
assert_eq!(b, bucket_index);
assert_eq!(s, slot_index as u16);
assert_eq!(nonsum, nonsum_low);
}
other => panic!("expected BucketSlot, got {other:?}"),
}
}
#[test]
fn rejects_bad_rrv_signature() {
let mut buf = synthetic_bucket_with_entries(&[]);
buf[0] = 0x55;
assert!(RrvBucketHeader::parse(&buf).is_err());
}
#[test]
fn rejects_bad_rrv_header_size() {
let mut buf = synthetic_bucket_with_entries(&[]);
buf[1] = 0x30;
assert!(RrvBucketHeader::parse(&buf).is_err());
}
}