use forensicnomicon::ntfs::{boot_offsets as off, OEM_ID};
use crate::error::{NtfsError, Result};
const MIN_LEN: usize = 0x50;
const MIN_RECORD_SIZE: u64 = 256;
const MAX_RECORD_SIZE: u64 = 1 << 20;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BootSector {
pub bytes_per_sector: u16,
pub sectors_per_cluster: u8,
pub total_sectors: u64,
pub mft_lcn: u64,
pub mftmirr_lcn: u64,
pub mft_record_size: u64,
pub index_record_size: u64,
pub volume_serial: u64,
}
impl BootSector {
#[must_use]
pub fn cluster_size(&self) -> u64 {
u64::from(self.bytes_per_sector) * u64::from(self.sectors_per_cluster)
}
#[must_use]
pub fn mft_byte_offset(&self) -> u64 {
self.mft_lcn.saturating_mul(self.cluster_size())
}
#[must_use]
pub fn mftmirr_byte_offset(&self) -> u64 {
self.mftmirr_lcn.saturating_mul(self.cluster_size())
}
pub fn parse(sector: &[u8]) -> Result<BootSector> {
if sector.len() < MIN_LEN {
return Err(NtfsError::TooShort {
what: "boot sector",
need: MIN_LEN,
got: sector.len(),
});
}
let oem: [u8; 8] = sector[off::OEM_ID..off::OEM_ID + 8].try_into().unwrap();
if oem != OEM_ID {
return Err(NtfsError::BadOemId(oem));
}
let bytes_per_sector = u16::from_le_bytes(
sector[off::BYTES_PER_SECTOR..off::BYTES_PER_SECTOR + 2]
.try_into()
.unwrap(),
);
if !(256..=4096).contains(&bytes_per_sector) || !bytes_per_sector.is_power_of_two() {
return Err(NtfsError::BadBytesPerSector(bytes_per_sector));
}
let sectors_per_cluster = sector[off::SECTORS_PER_CLUSTER];
if sectors_per_cluster == 0 || !sectors_per_cluster.is_power_of_two() {
return Err(NtfsError::BadSectorsPerCluster(sectors_per_cluster));
}
let cluster_size = u64::from(bytes_per_sector) * u64::from(sectors_per_cluster);
let total_sectors = u64::from_le_bytes(
sector[off::TOTAL_SECTORS..off::TOTAL_SECTORS + 8]
.try_into()
.unwrap(),
);
let mft_lcn =
u64::from_le_bytes(sector[off::MFT_LCN..off::MFT_LCN + 8].try_into().unwrap());
let mftmirr_lcn = u64::from_le_bytes(
sector[off::MFTMIRR_LCN..off::MFTMIRR_LCN + 8]
.try_into()
.unwrap(),
);
let cpr = sector[off::CLUSTERS_PER_RECORD];
let mft_record_size =
record_size(cpr, cluster_size).ok_or(NtfsError::BadRecordSize(cpr))?;
let cpi = sector[off::CLUSTERS_PER_INDEX];
let index_record_size =
record_size(cpi, cluster_size).ok_or(NtfsError::BadIndexRecordSize(cpi))?;
let volume_serial = u64::from_le_bytes(
sector[off::VOLUME_SERIAL..off::VOLUME_SERIAL + 8]
.try_into()
.unwrap(),
);
Ok(BootSector {
bytes_per_sector,
sectors_per_cluster,
total_sectors,
mft_lcn,
mftmirr_lcn,
mft_record_size,
index_record_size,
volume_serial,
})
}
}
fn record_size(raw: u8, cluster_size: u64) -> Option<u64> {
let v = raw as i8;
let size = if v > 0 {
u64::from(v.unsigned_abs()).checked_mul(cluster_size)?
} else {
1u64.checked_shl(u32::from(v.unsigned_abs()))?
};
(MIN_RECORD_SIZE..=MAX_RECORD_SIZE)
.contains(&size)
.then_some(size)
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::too_many_arguments)]
fn make_boot(
bytes_per_sector: u16,
sectors_per_cluster: u8,
total_sectors: u64,
mft_lcn: u64,
mftmirr_lcn: u64,
clusters_per_record: u8,
clusters_per_index: u8,
volume_serial: u64,
) -> [u8; 512] {
let mut b = [0u8; 512];
b[0..3].copy_from_slice(&[0xEB, 0x52, 0x90]); b[3..11].copy_from_slice(b"NTFS ");
b[0x0B..0x0D].copy_from_slice(&bytes_per_sector.to_le_bytes());
b[0x0D] = sectors_per_cluster;
b[0x15] = 0xF8; b[0x28..0x30].copy_from_slice(&total_sectors.to_le_bytes());
b[0x30..0x38].copy_from_slice(&mft_lcn.to_le_bytes());
b[0x38..0x40].copy_from_slice(&mftmirr_lcn.to_le_bytes());
b[0x40] = clusters_per_record;
b[0x44] = clusters_per_index;
b[0x48..0x50].copy_from_slice(&volume_serial.to_le_bytes());
b[510] = 0x55;
b[511] = 0xAA;
b
}
#[test]
fn parses_standard_boot_sector() {
let b = make_boot(
512,
8,
0x0010_0000,
0x0004_0000,
0x02,
0xF6,
0x01,
0xDEAD_BEEF_CAFE_F00D,
);
let bs = BootSector::parse(&b).expect("valid NTFS boot sector");
assert_eq!(bs.bytes_per_sector, 512);
assert_eq!(bs.sectors_per_cluster, 8);
assert_eq!(bs.cluster_size(), 4096);
assert_eq!(bs.total_sectors, 0x0010_0000);
assert_eq!(bs.mft_lcn, 0x0004_0000);
assert_eq!(bs.mftmirr_lcn, 0x02);
assert_eq!(bs.mft_record_size, 1024);
assert_eq!(bs.index_record_size, 4096);
assert_eq!(bs.volume_serial, 0xDEAD_BEEF_CAFE_F00D);
assert_eq!(bs.mft_byte_offset(), 0x0004_0000 * 4096);
assert_eq!(bs.mftmirr_byte_offset(), 0x02 * 4096);
}
#[test]
fn positive_clusters_per_record_multiplies_cluster_size() {
let b = make_boot(512, 8, 1000, 100, 2, 0x01, 0x01, 0);
let bs = BootSector::parse(&b).unwrap();
assert_eq!(bs.mft_record_size, 4096);
}
#[test]
fn rejects_non_ntfs_oem_id() {
let mut b = make_boot(512, 8, 1000, 100, 2, 0xF6, 0x01, 0);
b[3..11].copy_from_slice(b"MSDOS5.0");
assert!(matches!(BootSector::parse(&b), Err(NtfsError::BadOemId(_))));
}
#[test]
fn too_short_returns_error() {
let short = [0u8; 16];
assert!(matches!(
BootSector::parse(&short),
Err(NtfsError::TooShort { .. })
));
}
#[test]
fn rejects_bad_bytes_per_sector() {
let b = make_boot(513, 8, 1000, 100, 2, 0xF6, 0x01, 0);
assert!(matches!(
BootSector::parse(&b),
Err(NtfsError::BadBytesPerSector(513))
));
}
#[test]
fn rejects_zero_sectors_per_cluster() {
let b = make_boot(512, 0, 1000, 100, 2, 0xF6, 0x01, 0);
assert!(matches!(
BootSector::parse(&b),
Err(NtfsError::BadSectorsPerCluster(0))
));
}
#[test]
fn record_size_encoding_min_i8_does_not_panic() {
let b = make_boot(512, 8, 1000, 100, 2, 0x80, 0x01, 0);
assert!(matches!(
BootSector::parse(&b),
Err(NtfsError::BadRecordSize(0x80))
));
}
#[test]
fn rejects_bad_index_record_size() {
let b = make_boot(512, 8, 1000, 100, 2, 0xF6, 0x80, 0);
assert!(matches!(
BootSector::parse(&b),
Err(NtfsError::BadIndexRecordSize(0x80))
));
}
}