use crate::error::NsfError;
use crate::time::Timedate;
pub const SUPERBLOCK_SIGNATURE: [u8; 2] = [0x0E, 0x00];
pub const SUPERBLOCK_HEADER_BYTES: usize = 100;
pub const SUPERBLOCK_FOOTER_BYTES: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Superblock {
pub modification_time: Timedate,
pub uncompressed_size: u32,
pub number_of_summary_buckets: u32,
pub number_of_non_summary_buckets: u32,
pub number_of_bitmaps: u32,
pub rrv_bucket_size: u32,
pub data_rrv_bucket_position: u32,
pub rrv_identifier_low: u32,
pub rrv_identifier_high: u32,
pub bitmap_size: u32,
pub data_note_identifier_table_size: u32,
pub modified_note_log_size: u32,
pub folder_directory_object_size: u32,
pub flags: u16,
pub write_count: u32,
pub size: u32,
pub compression_type: u16,
pub number_of_summary_bucket_descriptor_pages: u32,
pub number_of_non_summary_bucket_descriptor_pages: u32,
pub number_of_soft_deleted_note_entries: u32,
pub shared_template_information_size: u16,
pub number_of_form_names: u16,
pub form_bitmap_size: u32,
}
impl Superblock {
pub fn parse(bytes: &[u8]) -> Result<Self, NsfError> {
if bytes.len() < SUPERBLOCK_HEADER_BYTES {
return Err(NsfError::TooShort {
actual: bytes.len(),
required: SUPERBLOCK_HEADER_BYTES,
});
}
if bytes[0] != SUPERBLOCK_SIGNATURE[0] || bytes[1] != SUPERBLOCK_SIGNATURE[1] {
return Err(NsfError::BadSubrecordSignature {
kind: "superblock",
expected: SUPERBLOCK_SIGNATURE,
observed: [bytes[0], bytes[1]],
});
}
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]])
};
Ok(Self {
modification_time: Timedate::from_bytes(&bytes[2..10])?,
uncompressed_size: u32_at(10),
number_of_summary_buckets: u32_at(14),
number_of_non_summary_buckets: u32_at(18),
number_of_bitmaps: u32_at(22),
rrv_bucket_size: u32_at(26),
data_rrv_bucket_position: u32_at(30),
rrv_identifier_low: u32_at(34),
rrv_identifier_high: u32_at(38),
bitmap_size: u32_at(42),
data_note_identifier_table_size: u32_at(46),
modified_note_log_size: u32_at(50),
folder_directory_object_size: u32_at(54),
flags: u16_at(58),
write_count: u32_at(60),
size: u32_at(64),
compression_type: u16_at(68),
number_of_summary_bucket_descriptor_pages: u32_at(70),
number_of_non_summary_bucket_descriptor_pages: u32_at(74),
number_of_soft_deleted_note_entries: u32_at(78),
shared_template_information_size: u16_at(82),
number_of_form_names: u16_at(86),
form_bitmap_size: u32_at(88),
})
}
pub fn modification_sort_key(&self) -> (u32, u32) {
let julian = self.modification_time.innards1 & 0x00FF_FFFF;
(julian, self.modification_time.innards0)
}
}
pub fn select_freshest(superblocks: &[(usize, Superblock)]) -> Option<(usize, Superblock)> {
superblocks
.iter()
.copied()
.max_by_key(|(_, sb)| sb.modification_sort_key())
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic(julian: u32, centi: u32) -> Vec<u8> {
let mut buf = vec![0u8; SUPERBLOCK_HEADER_BYTES];
buf[0..2].copy_from_slice(&SUPERBLOCK_SIGNATURE);
buf[2..6].copy_from_slice(¢i.to_le_bytes());
buf[6..10].copy_from_slice(&julian.to_le_bytes());
buf[26..30].copy_from_slice(&0x1000u32.to_le_bytes());
buf[30..34].copy_from_slice(&0x2af0u32.to_le_bytes());
buf[64..68].copy_from_slice(&4096u32.to_le_bytes());
buf[70..74].copy_from_slice(&3u32.to_le_bytes());
buf[74..78].copy_from_slice(&5u32.to_le_bytes());
buf
}
#[test]
fn parses_synthetic_superblock() {
let buf = synthetic(2_450_428, 0x006C_DCC0);
let sb = Superblock::parse(&buf).unwrap();
assert_eq!(sb.rrv_bucket_size, 0x1000);
assert_eq!(sb.data_rrv_bucket_position, 0x2af0);
assert_eq!(sb.size, 4096);
assert_eq!(sb.number_of_summary_bucket_descriptor_pages, 3);
assert_eq!(sb.number_of_non_summary_bucket_descriptor_pages, 5);
assert_eq!(sb.modification_sort_key(), (2_450_428, 0x006C_DCC0));
}
#[test]
fn rejects_bad_signature() {
let mut buf = synthetic(2_450_428, 0);
buf[0] = 0xFF;
let err = Superblock::parse(&buf).unwrap_err();
assert!(matches!(
err,
NsfError::BadSubrecordSignature {
kind: "superblock",
..
}
));
let msg = err.to_string();
assert!(msg.contains("superblock"), "got: {msg}");
assert!(msg.contains("0E 00"), "got: {msg}");
}
#[test]
fn rejects_short_buffer() {
let buf = vec![0u8; SUPERBLOCK_HEADER_BYTES - 1];
let err = Superblock::parse(&buf).unwrap_err();
assert!(matches!(err, NsfError::TooShort { .. }));
}
#[test]
fn select_freshest_picks_highest_julian_day() {
let sb_old = Superblock::parse(&synthetic(2_450_000, 0)).unwrap();
let sb_new = Superblock::parse(&synthetic(2_500_000, 0)).unwrap();
let sb_mid = Superblock::parse(&synthetic(2_460_000, 0)).unwrap();
let result = select_freshest(&[(0, sb_old), (1, sb_new), (2, sb_mid)]);
assert_eq!(result.unwrap().0, 1);
assert_eq!(result.unwrap().1.modification_sort_key().0, 2_500_000);
}
#[test]
fn select_freshest_breaks_ties_by_centiseconds() {
let sb_morning = Superblock::parse(&synthetic(2_500_000, 1_000_000)).unwrap();
let sb_evening = Superblock::parse(&synthetic(2_500_000, 8_000_000)).unwrap();
let sb_noon = Superblock::parse(&synthetic(2_500_000, 4_320_000)).unwrap();
let result = select_freshest(&[(0, sb_morning), (1, sb_evening), (2, sb_noon)]);
assert_eq!(result.unwrap().0, 1);
}
#[test]
fn select_freshest_empty_returns_none() {
let v: Vec<(usize, Superblock)> = vec![];
assert!(select_freshest(&v).is_none());
}
#[test]
fn select_freshest_single_returns_that_one() {
let sb = Superblock::parse(&synthetic(2_450_428, 0)).unwrap();
let result = select_freshest(&[(2, sb)]);
assert_eq!(result.unwrap().0, 2);
}
}