use crate::cx;
use crate::error::NsfError;
const BDB_HEADER_BYTES: usize = 66;
const BDB_FOOTER_BYTES: usize = 12;
const RRV_DESCRIPTOR_BYTES: usize = 8;
const UNK_ENTRY_BYTES: usize = 10;
const UNK_TEXT_PREAMBLE: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RrvBucketKind {
Data,
NonData,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RrvBucketDescriptor {
pub kind: RrvBucketKind,
pub file_offset: u64,
pub initial_rrv_identifier: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BucketDescriptorBlock {
pub write_count: u32,
pub rrv_buckets: Vec<RrvBucketDescriptor>,
pub unk_names: Vec<String>,
pub unk_types: Vec<u8>,
pub unk_classes: Vec<u8>,
}
impl BucketDescriptorBlock {
pub fn name(&self, name_id: u16) -> Option<&str> {
self.unk_names.get(name_id as usize).map(|s| s.as_str())
}
pub fn field_kind(&self, name_id: u16) -> crate::item::FieldKind {
let i = name_id as usize;
let class = self.unk_classes.get(i).copied().unwrap_or(0xFF);
let ty = self.unk_types.get(i).copied().unwrap_or(0xFF);
if class == 0xFF {
crate::item::FieldKind::Unknown
} else {
crate::item::field_kind(class, ty)
}
}
}
impl BucketDescriptorBlock {
pub fn parse(file: &[u8], offset: u64, available_size: u32) -> Result<Self, NsfError> {
let start = offset as usize;
let header = file
.get(start..start + BDB_HEADER_BYTES)
.ok_or(NsfError::TooShort {
actual: file.len(),
required: start + BDB_HEADER_BYTES,
})?;
if header[0] != 0x01 || header[1] != 0x00 {
return Err(NsfError::BadSubrecordSignature {
kind: "bucket descriptor block",
expected: [0x01, 0x00],
observed: [header[0], header[1]],
});
}
let u16_at = |o: usize| u16::from_le_bytes([header[o], header[o + 1]]);
let u32_at = |o: usize| {
u32::from_le_bytes([header[o], header[o + 1], header[o + 2], header[o + 3]])
};
let compression_type = u16_at(4);
let uncompressed_size = u32_at(6) as usize;
let write_count = u32_at(10);
let stored_size = u32_at(14) as usize;
let number_of_unique_name_keys = u32_at(26) as usize;
let unique_name_key_text_size = u32_at(34) as usize;
let number_of_rrv_bucket_descriptors = u32_at(38) as usize;
if stored_size > available_size as usize {
return Err(NsfError::TooShort {
actual: available_size as usize,
required: stored_size,
});
}
if compression_type != 1 {
return Err(NsfError::CompressionUnsupported {
structure: "bucket descriptor block",
compression_type,
});
}
if stored_size < BDB_HEADER_BYTES + BDB_FOOTER_BYTES + 4 {
return Err(NsfError::DecompressionFailed {
detail: "bucket descriptor block size too small to hold a compressed body",
});
}
let body_len = stored_size - BDB_HEADER_BYTES - BDB_FOOTER_BYTES;
let comp_start = start + BDB_HEADER_BYTES;
let comp = file
.get(comp_start..comp_start + body_len)
.ok_or(NsfError::TooShort {
actual: file.len(),
required: comp_start + body_len,
})?;
let body = cx::decompress_chained(comp, uncompressed_size)?;
let need = number_of_rrv_bucket_descriptors * RRV_DESCRIPTOR_BYTES;
if body.len() < need {
return Err(NsfError::TooShort {
actual: body.len(),
required: need,
});
}
let mut rrv_buckets = Vec::with_capacity(number_of_rrv_bucket_descriptors);
for i in 0..number_of_rrv_bucket_descriptors {
let base = i * RRV_DESCRIPTOR_BYTES;
let raw = u32::from_le_bytes([
body[base],
body[base + 1],
body[base + 2],
body[base + 3],
]);
let initial_rrv_identifier = u32::from_le_bytes([
body[base + 4],
body[base + 5],
body[base + 6],
body[base + 7],
]);
let kind = if raw & 1 != 0 {
RrvBucketKind::NonData
} else {
RrvBucketKind::Data
};
let file_offset = u64::from(raw & 0xFFFF_FFFE) << 8;
rrv_buckets.push(RrvBucketDescriptor {
kind,
file_offset,
initial_rrv_identifier,
});
}
let unk_table_start = number_of_rrv_bucket_descriptors * RRV_DESCRIPTOR_BYTES;
let text_start = unk_table_start + number_of_unique_name_keys * UNK_ENTRY_BYTES;
let text_payload_start = text_start + UNK_TEXT_PREAMBLE;
let text_end = (text_start + unique_name_key_text_size).min(body.len());
let mut unk_names = Vec::with_capacity(number_of_unique_name_keys);
let mut unk_types = Vec::with_capacity(number_of_unique_name_keys);
let mut unk_classes = Vec::with_capacity(number_of_unique_name_keys);
let text = body.get(text_payload_start..text_end).unwrap_or(&[]);
for i in 0..number_of_unique_name_keys {
let e = unk_table_start + i * UNK_ENTRY_BYTES;
let (name, ty, class) = body
.get(e..e + UNK_ENTRY_BYTES)
.map(|d| {
let off = u32::from_le_bytes([d[0], d[1], d[2], d[3]]) as usize;
let len = u16::from_le_bytes([d[4], d[5]]) as usize;
let name = text
.get(off..off + len)
.map(|s| String::from_utf8_lossy(s).into_owned())
.unwrap_or_default();
(name, d[6], d[7])
})
.unwrap_or_default();
unk_names.push(name);
unk_types.push(ty);
unk_classes.push(class);
}
Ok(Self {
write_count,
rrv_buckets,
unk_names,
unk_types,
unk_classes,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_bad_signature() {
let mut buf = vec![0u8; 128];
buf[0] = 0xFF;
let err = BucketDescriptorBlock::parse(&buf, 0, 128).unwrap_err();
assert!(matches!(
err,
NsfError::BadSubrecordSignature {
kind: "bucket descriptor block",
..
}
));
}
#[test]
fn rejects_unsupported_compression() {
let mut buf = vec![0u8; 128];
buf[0] = 0x01;
buf[1] = 0x00;
buf[4] = 0x00;
buf[14..18].copy_from_slice(&100u32.to_le_bytes()); let err = BucketDescriptorBlock::parse(&buf, 0, 128).unwrap_err();
assert!(matches!(
err,
NsfError::CompressionUnsupported {
structure: "bucket descriptor block",
..
}
));
}
#[test]
fn rejects_stored_size_over_available() {
let mut buf = vec![0u8; 128];
buf[0] = 0x01;
buf[1] = 0x00;
buf[4] = 0x01; buf[14..18].copy_from_slice(&4096u32.to_le_bytes()); let err = BucketDescriptorBlock::parse(&buf, 0, 128).unwrap_err();
assert!(matches!(err, NsfError::TooShort { .. }));
}
}