use core::mem::size_of;
use hadris_common::types::{
endian::{Endian, LittleEndian},
number::{U16, U32, U64},
};
use crate::error::{FatError, Result};
use crate::io::{Read, ReadExt, Seek, SeekFrom};
pub const BOOT_REGION_SECTORS: usize = 12;
pub const EXFAT_SIGNATURE: [u8; 8] = *b"EXFAT ";
pub const BOOT_SIGNATURE: u16 = 0xAA55;
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct RawExFatBootSector {
pub jump_boot: [u8; 3],
pub fs_name: [u8; 8],
pub must_be_zero: [u8; 53],
pub partition_offset: U64<LittleEndian>,
pub volume_length: U64<LittleEndian>,
pub fat_offset: U32<LittleEndian>,
pub fat_length: U32<LittleEndian>,
pub cluster_heap_offset: U32<LittleEndian>,
pub cluster_count: U32<LittleEndian>,
pub first_cluster_of_root: U32<LittleEndian>,
pub volume_serial_number: U32<LittleEndian>,
pub fs_revision: U16<LittleEndian>,
pub volume_flags: U16<LittleEndian>,
pub bytes_per_sector_shift: u8,
pub sectors_per_cluster_shift: u8,
pub number_of_fats: u8,
pub drive_select: u8,
pub percent_in_use: u8,
pub reserved: [u8; 7],
pub boot_code: [u8; 390],
pub boot_signature: U16<LittleEndian>,
}
unsafe impl bytemuck::NoUninit for RawExFatBootSector {}
unsafe impl bytemuck::Zeroable for RawExFatBootSector {}
unsafe impl bytemuck::AnyBitPattern for RawExFatBootSector {}
impl RawExFatBootSector {
pub fn validate(&self) -> Result<()> {
if self.fs_name != EXFAT_SIGNATURE {
return Err(FatError::ExFatInvalidSignature {
expected: EXFAT_SIGNATURE,
found: self.fs_name,
});
}
let sig = self.boot_signature.get();
if sig != BOOT_SIGNATURE {
return Err(FatError::InvalidBootSignature { found: sig });
}
if self.must_be_zero.iter().any(|&b| b != 0) {
return Err(FatError::ExFatInvalidBootSector {
reason: "must_be_zero field contains non-zero bytes",
});
}
if !(9..=12).contains(&self.bytes_per_sector_shift) {
return Err(FatError::ExFatInvalidBootSector {
reason: "invalid bytes_per_sector_shift (must be 9-12)",
});
}
let combined_shift = self.bytes_per_sector_shift + self.sectors_per_cluster_shift;
if combined_shift > 25 {
return Err(FatError::ExFatInvalidBootSector {
reason: "combined sector/cluster shift too large (max cluster size 32MB)",
});
}
if self.number_of_fats != 1 && self.number_of_fats != 2 {
return Err(FatError::ExFatInvalidBootSector {
reason: "invalid number_of_fats (must be 1 or 2)",
});
}
Ok(())
}
pub fn bytes_per_sector(&self) -> usize {
1 << self.bytes_per_sector_shift
}
pub fn sectors_per_cluster(&self) -> usize {
1 << self.sectors_per_cluster_shift
}
pub fn bytes_per_cluster(&self) -> usize {
self.bytes_per_sector() * self.sectors_per_cluster()
}
}
#[derive(Debug, Clone)]
pub struct ExFatInfo {
pub bytes_per_sector: usize,
pub sectors_per_cluster: usize,
pub bytes_per_cluster: usize,
pub fat_offset: u64,
pub fat_length: u64,
pub cluster_heap_offset: u64,
pub cluster_count: u32,
pub root_cluster: u32,
pub volume_serial: u32,
pub fat_count: u8,
}
impl ExFatInfo {
pub fn from_boot_sector(bs: &RawExFatBootSector) -> Self {
let bytes_per_sector = bs.bytes_per_sector();
let sectors_per_cluster = bs.sectors_per_cluster();
Self {
bytes_per_sector,
sectors_per_cluster,
bytes_per_cluster: bytes_per_sector * sectors_per_cluster,
fat_offset: bs.fat_offset.get() as u64 * bytes_per_sector as u64,
fat_length: bs.fat_length.get() as u64 * bytes_per_sector as u64,
cluster_heap_offset: bs.cluster_heap_offset.get() as u64 * bytes_per_sector as u64,
cluster_count: bs.cluster_count.get(),
root_cluster: bs.first_cluster_of_root.get(),
volume_serial: bs.volume_serial_number.get(),
fat_count: bs.number_of_fats,
}
}
pub fn cluster_to_offset(&self, cluster: u32) -> u64 {
self.cluster_heap_offset + (cluster as u64 - 2) * self.bytes_per_cluster as u64
}
pub fn is_valid_cluster(&self, cluster: u32) -> bool {
cluster >= 2 && cluster < self.cluster_count + 2
}
}
#[derive(Clone)]
pub struct ExFatBootSector {
raw: RawExFatBootSector,
pub info: ExFatInfo,
}
impl core::fmt::Debug for ExFatBootSector {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ExFatBootSector")
.field("info", &self.info)
.finish_non_exhaustive()
}
}
impl ExFatBootSector {
pub fn read<DATA: Read + Seek>(data: &mut DATA) -> Result<Self> {
data.seek(SeekFrom::Start(0))?;
let raw: RawExFatBootSector = data.read_struct()?;
raw.validate()?;
let info = ExFatInfo::from_boot_sector(&raw);
Ok(Self { raw, info })
}
pub fn validate_checksum<DATA: Read + Seek>(data: &mut DATA, sector_size: usize) -> Result<()> {
let mut checksum: u32 = 0;
for sector in 0..11 {
data.seek(SeekFrom::Start(sector as u64 * sector_size as u64))?;
for byte_idx in 0..sector_size {
let mut byte = [0u8; 1];
data.read_exact(&mut byte)?;
if sector == 0 && (byte_idx == 106 || byte_idx == 107 || byte_idx == 112) {
continue;
}
checksum = checksum.rotate_right(1).wrapping_add(byte[0] as u32);
}
}
data.seek(SeekFrom::Start(11 * sector_size as u64))?;
let expected_count = sector_size / size_of::<u32>();
for _ in 0..expected_count {
let mut stored = [0u8; 4];
data.read_exact(&mut stored)?;
let stored_checksum = u32::from_le_bytes(stored);
if stored_checksum != checksum {
return Err(FatError::ExFatInvalidChecksum {
expected: checksum,
found: stored_checksum,
});
}
}
Ok(())
}
pub fn raw(&self) -> &RawExFatBootSector {
&self.raw
}
pub fn info(&self) -> &ExFatInfo {
&self.info
}
}
#[cfg(test)]
mod tests {
use super::*;
use static_assertions::const_assert_eq;
const_assert_eq!(size_of::<RawExFatBootSector>(), 512);
}