use crate::detect::{identify_file_strict, FileKind};
use crate::error::NsfError;
use crate::ods::Ods;
use crate::time::Timedate;
const DBINFO_START: usize = 6;
const DBINFO_CORE_MIN: usize = 128;
pub mod flags {
pub const DBFLAG_TEMPLATE: u16 = 0x0010;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DbHeader {
pub db_header_size: u32,
pub ods: Ods,
pub database_id: Timedate,
pub app_version: u16,
pub non_data_rrv_bucket_position: u32,
pub available_non_data_rrv_identifier: u32,
pub number_of_available_non_data_rrvs: u16,
pub activity_log_offset: u32,
pub bucket_modification: Timedate,
pub database_class: u16,
pub database_flags: u16,
pub bucket_descriptor_block_size: u32,
pub bucket_descriptor_block_position: u32,
pub bdt_size: u16,
pub bdt_position: u32,
pub bdt_bitmaps: u16,
pub data_rrv_bucket_position: u32,
pub first_data_rrv_identifier: u32,
pub available_data_rrv_identifier: u32,
pub number_of_available_data_rrvs: u16,
pub rrv_bucket_size: u16,
pub summary_bucket_size: u16,
pub bitmap_size: u16,
pub allocation_granularity: u16,
pub extention_granularity: u32,
pub file_size_pages: u32,
}
impl DbHeader {
pub fn parse(bytes: &[u8]) -> Result<Self, NsfError> {
let file_kind = identify_file_strict(bytes)?;
let db_header_size = match file_kind {
FileKind::Nsf { db_header_size } => db_header_size,
FileKind::NotNsf { reason } => {
let _ = reason;
return Err(NsfError::BadFileSignature { observed: [0, 0] });
}
};
let required = DBINFO_START + DBINFO_CORE_MIN;
if bytes.len() < required {
return Err(NsfError::TooShort {
actual: bytes.len(),
required,
});
}
let d = &bytes[DBINFO_START..DBINFO_START + DBINFO_CORE_MIN];
let u16_at = |o: usize| u16::from_le_bytes([d[o], d[o + 1]]);
let u32_at = |o: usize| u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]]);
let ods_raw = u32_at(0);
let database_id = Timedate::from_bytes(&d[4..12])?;
let app_version = u16_at(12);
let non_data_rrv_bucket_position = u32_at(14);
let available_non_data_rrv_identifier = u32_at(18);
let number_of_available_non_data_rrvs = u16_at(22);
let activity_log_offset = u32_at(24);
let bucket_modification = Timedate::from_bytes(&d[28..36])?;
let database_class = u16_at(36);
let database_flags = u16_at(38);
let bucket_descriptor_block_size = u32_at(40);
let bucket_descriptor_block_position = u32_at(44);
let bdt_size = u16_at(48);
let bdt_position = u32_at(50);
let bdt_bitmaps = u16_at(54);
let data_rrv_bucket_position = u32_at(56);
let first_data_rrv_identifier = u32_at(60);
let available_data_rrv_identifier = u32_at(64);
let number_of_available_data_rrvs = u16_at(68);
let rrv_bucket_size = u16_at(70);
let summary_bucket_size = u16_at(72);
let bitmap_size = u16_at(74);
let allocation_granularity = u16_at(76);
let extention_granularity = u32_at(78);
let file_size_pages = u32_at(82);
Ok(Self {
db_header_size,
ods: Ods::new(ods_raw),
database_id,
app_version,
non_data_rrv_bucket_position,
available_non_data_rrv_identifier,
number_of_available_non_data_rrvs,
activity_log_offset,
bucket_modification,
database_class,
database_flags,
bucket_descriptor_block_size,
bucket_descriptor_block_position,
bdt_size,
bdt_position,
bdt_bitmaps,
data_rrv_bucket_position,
first_data_rrv_identifier,
available_data_rrv_identifier,
number_of_available_data_rrvs,
rrv_bucket_size,
summary_bucket_size,
bitmap_size,
allocation_granularity,
extention_granularity,
file_size_pages,
})
}
pub fn is_template(&self) -> bool {
self.database_flags & flags::DBFLAG_TEMPLATE != 0
}
pub fn is_database_encrypted(&self) -> Option<bool> {
None
}
pub fn file_size_from_header_bytes(&self) -> u64 {
(self.file_size_pages as u64) * 256
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_header(ods: u32, flags: u16) -> Vec<u8> {
let mut buf = vec![0u8; 256];
buf[0] = 0x1A;
buf[1] = 0x00;
buf[2..6].copy_from_slice(&1024u32.to_le_bytes());
buf[6..10].copy_from_slice(&ods.to_le_bytes());
buf[44..46].copy_from_slice(&flags.to_le_bytes());
buf[50..54].copy_from_slice(&0x0000_4000u32.to_le_bytes());
buf[62..66].copy_from_slice(&0x0000_2af0u32.to_le_bytes());
buf[88..92].copy_from_slice(&5000u32.to_le_bytes());
buf
}
#[test]
fn parses_synthetic_ods_53_unencrypted() {
let buf = synthetic_header(53, 0);
let h = DbHeader::parse(&buf).unwrap();
assert_eq!(h.db_header_size, 1024);
assert_eq!(h.ods.raw, 53);
assert!(!h.is_template());
assert!(h.is_database_encrypted().is_none(), "encryption detection deferred");
assert_eq!(h.bucket_descriptor_block_position, 0x0000_4000);
assert_eq!(h.data_rrv_bucket_position, 0x0000_2af0);
assert_eq!(h.file_size_pages, 5000);
assert_eq!(h.file_size_from_header_bytes(), 5000 * 256);
}
#[test]
fn flags_template_decodes_correctly() {
let buf = synthetic_header(53, flags::DBFLAG_TEMPLATE);
let h = DbHeader::parse(&buf).unwrap();
assert!(h.is_template());
}
#[test]
fn rejects_bad_magic() {
let mut buf = synthetic_header(53, 0);
buf[0] = 0xDE;
buf[1] = 0xAD;
let err = DbHeader::parse(&buf).unwrap_err();
assert!(matches!(err, NsfError::BadFileSignature { .. }));
}
#[test]
fn rejects_too_short_for_dbinfo() {
let buf: Vec<u8> = vec![0x1A, 0x00, 0x00, 0x04, 0x00, 0x00];
let err = DbHeader::parse(&buf).unwrap_err();
assert!(matches!(err, NsfError::TooShort { .. }));
}
#[test]
fn ods_supported_check_works_via_header() {
let buf_modern = synthetic_header(53, 0);
let h_modern = DbHeader::parse(&buf_modern).unwrap();
assert!(h_modern.ods.is_supported_for_enumeration());
let buf_legacy = synthetic_header(17, 0);
let h_legacy = DbHeader::parse(&buf_legacy).unwrap();
assert!(!h_legacy.ods.is_supported_for_enumeration());
}
#[test]
fn parses_canonical_comparedbs_ntf_header_bytes() {
#[rustfmt::skip]
let bytes: &[u8] = &[
0x1a, 0x00, 0x00, 0x04, 0x00, 0x00, 0x34, 0x00,
0x00, 0x00, 0xa9, 0xf4, 0x61, 0x00, 0x0c, 0x88,
0x25, 0x85, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00,
0xf6, 0x03, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00,
0x00, 0x00, 0x3f, 0x08, 0x62, 0x00, 0x0c, 0x88,
0x25, 0x00, 0x04, 0xff, 0x50, 0x42, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xf0, 0x2a,
0x00, 0x00, 0xf6, 0x08, 0x00, 0x00, 0x5a, 0x09,
0x00, 0x00, 0xe3, 0x01, 0x00, 0x10, 0x00, 0x20,
0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,
0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let mut buf = bytes.to_vec();
buf.resize(256, 0);
let h = DbHeader::parse(&buf).unwrap();
assert_eq!(h.ods.raw, 52);
assert!(h.is_template(), "comparedbs.ntf flags = 0x{:04X}", h.database_flags);
assert_eq!(h.bucket_descriptor_block_position, 0);
assert_eq!(h.data_rrv_bucket_position, 0x2af0);
assert_eq!(h.file_size_pages, 0x3000);
assert_eq!(h.rrv_bucket_size, 0x1000);
}
}