lamexfat 0.1.0

no_std read-only exFAT reader for UEFI bootloaders (removable media)
Documentation
// SPDX-License-Identifier: Apache-2.0
//! Boot-sector (VBR) parse + validated geometry, and the Main-Boot-Region
//! checksum.
//!
//! Adapted from exfat-slim @2ffd2c2 `boot_sector.rs` (Apache-2.0) — field
//! offsets are the on-disk `exFAT` layout (ECMA-406 / the Microsoft exFAT spec);
//! the geometry caching, the range/sanity rejects, and the boot-region checksum
//! are `lamexfat`.

use crate::{
    block_read::u32_at,
    error::{Error, Result, VbrReason},
};

/// Parsed, validated exFAT geometry — everything the read path needs, cached
/// once at [`crate::ExFat::open`].
#[derive(Debug, Clone, Copy)]
pub(crate) struct Geometry {
    pub bytes_per_sector: u32,
    pub sectors_per_cluster: u32,
    pub fat_offset: u32,
    pub cluster_heap_offset: u32,
    pub cluster_count: u32,
    pub root_cluster: u32,
    pub volume_serial: u32,
}

impl Geometry {
    /// Cluster size in bytes (`bytes_per_sector * sectors_per_cluster`).
    pub(crate) fn cluster_size(&self) -> u64 {
        u64::from(self.bytes_per_sector) * u64::from(self.sectors_per_cluster)
    }

    /// Absolute byte offset of cluster `cluster_id`'s data. Clusters are numbered
    /// from 2 (cluster 2 is the first cluster of the heap). `None` if `cluster_id`
    /// is below 2 or past the heap.
    pub(crate) fn cluster_byte(&self, cluster_id: u32) -> Option<u64> {
        let rel = cluster_id.checked_sub(2)?;
        if rel >= self.cluster_count {
            return None;
        }
        let sector = u64::from(self.cluster_heap_offset)
            + u64::from(rel) * u64::from(self.sectors_per_cluster);
        Some(sector * u64::from(self.bytes_per_sector))
    }

    /// Absolute byte offset of the FAT entry (`u32`) for `cluster_id`.
    pub(crate) fn fat_entry_byte(&self, cluster_id: u32) -> u64 {
        u64::from(self.fat_offset) * u64::from(self.bytes_per_sector) + u64::from(cluster_id) * 4
    }
}

/// Parse + validate the boot sector (sector 0). `buf` must hold at least the
/// 512-byte boot sector (every geometry field lives in the first 113 bytes and
/// the boot signature at 510).
pub(crate) fn parse(buf: &[u8]) -> Result<Geometry> {
    let bad = Error::BadVbr;
    if buf.get(3..11) != Some(b"EXFAT   ".as_slice()) {
        return Err(bad(VbrReason::BadMagic));
    }
    if buf.get(510..512) != Some([0x55, 0xAA].as_slice()) {
        return Err(bad(VbrReason::BadBootSignature));
    }

    let bps_shift = *buf.get(108).ok_or(bad(VbrReason::BadGeometry))?;
    let spc_shift = *buf.get(109).ok_or(bad(VbrReason::BadGeometry))?;
    // exFAT permits 512..=4096 B sectors (shift 9..=12) and a cluster up to
    // 32 MiB (`bps_shift + spc_shift <= 25`). Reject anything outside that BEFORE
    // any shift drives an allocation.
    if !(9..=12).contains(&bps_shift) || u16::from(bps_shift) + u16::from(spc_shift) > 25 {
        return Err(bad(VbrReason::BadGeometry));
    }
    let bytes_per_sector = 1u32 << bps_shift;
    let sectors_per_cluster = 1u32 << spc_shift;

    let fat_offset = u32_at(buf, 80).ok_or(bad(VbrReason::BadGeometry))?;
    let fat_length = u32_at(buf, 84).ok_or(bad(VbrReason::BadGeometry))?;
    let cluster_heap_offset = u32_at(buf, 88).ok_or(bad(VbrReason::BadGeometry))?;
    let cluster_count = u32_at(buf, 92).ok_or(bad(VbrReason::BadGeometry))?;
    let root_cluster = u32_at(buf, 96).ok_or(bad(VbrReason::BadGeometry))?;
    let volume_serial = u32_at(buf, 100).ok_or(bad(VbrReason::BadGeometry))?;
    let number_of_fats = *buf.get(110).ok_or(bad(VbrReason::BadGeometry))?;

    // Layout sanity: the FAT must precede the heap, the heap must hold the root,
    // and a single FAT (2 only for the rare TexFAT, which we don't read).
    if !(1..=2).contains(&number_of_fats)
        || cluster_count == 0
        || root_cluster < 2
        || root_cluster - 2 >= cluster_count
        || fat_offset < 24
        || cluster_heap_offset < fat_offset.saturating_add(fat_length)
    {
        return Err(bad(VbrReason::BadGeometry));
    }

    Ok(Geometry {
        bytes_per_sector,
        sectors_per_cluster,
        fat_offset,
        cluster_heap_offset,
        cluster_count,
        root_cluster,
        volume_serial,
    })
}

/// Verify the Main-Boot-Region checksum. `region` must hold the first 12 sectors
/// (sectors 0..=10 are checksummed; sector 11 stores the result, the `u32`
/// repeated). Bytes 106/107 (VolumeFlags) and 112 (PercentInUse) of sector 0 are
/// excluded — they mutate at runtime without invalidating the checksum.
pub(crate) fn verify_boot_checksum(region: &[u8], bytes_per_sector: usize) -> Result<()> {
    let err = || Error::BadVbr(VbrReason::BadBootChecksum);
    let covered = bytes_per_sector
        .checked_mul(11)
        .and_then(|n| region.get(..n))
        .ok_or_else(err)?;
    let mut sum: u32 = 0;
    for (i, &b) in covered.iter().enumerate() {
        if i == 106 || i == 107 || i == 112 {
            continue;
        }
        sum = sum.rotate_right(1).wrapping_add(u32::from(b));
    }
    let stored = u32_at(region, bytes_per_sector * 11).ok_or_else(err)?;
    if sum != stored {
        return Err(err());
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    extern crate alloc;
    use alloc::{vec, vec::Vec};

    use super::*;

    fn valid_vbr() -> [u8; 512] {
        let mut b = [0u8; 512];
        b[3..11].copy_from_slice(b"EXFAT   ");
        b[80..84].copy_from_slice(&24u32.to_le_bytes()); // fat_offset
        b[84..88].copy_from_slice(&8u32.to_le_bytes()); // fat_length
        b[88..92].copy_from_slice(&32u32.to_le_bytes()); // cluster_heap_offset
        b[92..96].copy_from_slice(&100u32.to_le_bytes()); // cluster_count
        b[96..100].copy_from_slice(&2u32.to_le_bytes()); // root_cluster
        b[100..104].copy_from_slice(&0x1234_5678u32.to_le_bytes());
        b[108] = 9; // bps_shift -> 512
        b[109] = 3; // spc_shift -> 8 sectors/cluster
        b[110] = 1; // number_of_fats
        b[510] = 0x55;
        b[511] = 0xAA;
        b
    }

    #[test]
    fn accepts_valid_and_caches_geometry() {
        let g = parse(&valid_vbr()).expect("valid");
        assert_eq!(g.bytes_per_sector, 512);
        assert_eq!(g.sectors_per_cluster, 8);
        assert_eq!(g.cluster_count, 100);
        assert_eq!(g.root_cluster, 2);
        assert_eq!(g.volume_serial, 0x1234_5678);
        assert_eq!(g.cluster_size(), 512 * 8);
        // Cluster 2 is the first heap cluster: heap_offset(32 sectors) * bps(512).
        assert_eq!(g.cluster_byte(2), Some(32 * 512));
        // Out-of-range cluster ids yield None (no panic, no wrap).
        assert_eq!(g.cluster_byte(1), None);
        assert_eq!(g.cluster_byte(102), None);
    }

    #[test]
    fn rejects_each_corruption_with_its_reason() {
        let reason = |mutate: &dyn Fn(&mut [u8; 512])| {
            let mut b = valid_vbr();
            mutate(&mut b);
            match parse(&b) {
                Err(Error::BadVbr(r)) => r,
                other => panic!("expected BadVbr, got {other:?}"),
            }
        };
        assert_eq!(reason(&|b| b[5] = b'X'), VbrReason::BadMagic);
        assert_eq!(reason(&|b| b[510] = 0), VbrReason::BadBootSignature);
        assert_eq!(reason(&|b| b[108] = 8), VbrReason::BadGeometry); // bps shift < 9
        assert_eq!(reason(&|b| b[108] = 13), VbrReason::BadGeometry); // bps shift > 12
        assert_eq!(reason(&|b| b[109] = 20), VbrReason::BadGeometry); // bps+spc > 25
        assert_eq!(
            reason(&|b| b[96..100].copy_from_slice(&1u32.to_le_bytes())),
            VbrReason::BadGeometry // root_cluster < 2
        );
        assert_eq!(reason(&|b| b[110] = 0), VbrReason::BadGeometry); // number_of_fats == 0
        assert_eq!(
            reason(&|b| b[88..92].copy_from_slice(&31u32.to_le_bytes())),
            VbrReason::BadGeometry // heap overlaps the FAT (24 + 8 = 32)
        );
    }

    #[test]
    fn boot_checksum_excludes_volatile_bytes() {
        let bps = 512usize;
        let mut region = vec![0u8; bps * 12];
        for (i, b) in region.iter_mut().enumerate() {
            *b = (i % 251) as u8;
        }
        // Store the correct checksum into sector 11.
        let mut sum: u32 = 0;
        for (i, &b) in region[..bps * 11].iter().enumerate() {
            if i == 106 || i == 107 || i == 112 {
                continue;
            }
            sum = sum.rotate_right(1).wrapping_add(u32::from(b));
        }
        region[bps * 11..bps * 11 + 4].copy_from_slice(&sum.to_le_bytes());
        assert!(verify_boot_checksum(&region, bps).is_ok());

        // Mutating an excluded byte (VolumeFlags / PercentInUse) keeps it valid.
        let mut volatile: Vec<u8> = region.clone();
        volatile[106] ^= 0xFF;
        volatile[112] ^= 0xFF;
        assert!(verify_boot_checksum(&volatile, bps).is_ok());

        // Mutating a covered byte invalidates it.
        let mut tampered = region;
        tampered[200] ^= 0xFF;
        assert_eq!(
            verify_boot_checksum(&tampered, bps).unwrap_err(),
            Error::BadVbr(VbrReason::BadBootChecksum)
        );
    }
}