Skip to main content

fat/
boot.rs

1//! FAT12/16/32 boot sector (BIOS Parameter Block) parse, geometry, and the
2//! cluster-count FAT-type decision (fatgen103 §3.5): `< 4085` → FAT12,
3//! `< 65525` → FAT16, else FAT32.
4
5use crate::bytes::{le_u16, le_u32, u8_at};
6use crate::error::{FatError, Result};
7
8/// The FAT-type cutoffs from Microsoft's fatgen103 specification. These bounds
9/// are the *definition* of the type, not a heuristic (spec §3.5).
10const FAT12_MAX_CLUSTERS: u32 = 4085;
11const FAT16_MAX_CLUSTERS: u32 = 65525;
12
13/// Which member of the FAT family a volume is.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum FatVariant {
16    /// 12-bit cluster indices.
17    Fat12,
18    /// 16-bit cluster indices.
19    Fat16,
20    /// 32-bit cluster indices.
21    Fat32,
22    /// exFAT (parsed by the exFAT boot path, not the BPB path).
23    ExFat,
24}
25
26/// Resolved on-disk geometry of a FAT12/16/32 volume. All byte offsets are
27/// relative to the start of the volume (sector 0).
28#[derive(Debug, Clone)]
29pub struct Geometry {
30    /// Which FAT variant.
31    pub variant: FatVariant,
32    /// Bytes per logical sector (power of two, 512–4096).
33    pub bytes_per_sector: u32,
34    /// Sectors per cluster (power of two, 1–128).
35    pub sectors_per_cluster: u32,
36    /// Cluster size in bytes.
37    pub cluster_size: u32,
38    /// Count of reserved sectors preceding the first FAT.
39    pub reserved_sectors: u32,
40    /// Number of FAT copies.
41    pub num_fats: u32,
42    /// Sectors occupied by one FAT.
43    pub fat_size_sectors: u32,
44    /// FAT12/16 fixed root directory entry count (0 on FAT32).
45    pub root_entry_count: u32,
46    /// Total sectors in the volume.
47    pub total_sectors: u64,
48    /// FAT32/exFAT first cluster of the root directory (0 on FAT12/16).
49    pub root_cluster: u32,
50    /// Count of data clusters (drives the type decision).
51    pub count_of_clusters: u32,
52    /// Byte offset of the first FAT.
53    pub fat_start: u64,
54    /// Byte offset of the FAT12/16 fixed root region (0 on FAT32).
55    pub root_dir_start: u64,
56    /// Byte size of the FAT12/16 fixed root region (0 on FAT32).
57    pub root_dir_bytes: u32,
58    /// Byte offset of cluster 2 (start of the data region).
59    pub data_start: u64,
60}
61
62/// The validated raw BPB fields, before offsets and the type decision.
63struct Fields {
64    bytes_per_sector: u32,
65    sectors_per_cluster: u32,
66    reserved_sectors: u32,
67    num_fats: u32,
68    root_entry_count: u32,
69    fat_size_sectors: u32,
70    total_sectors: u64,
71}
72
73impl Geometry {
74    /// Parse the BPB from the boot sector `boot` (at least 512 bytes).
75    ///
76    /// Fails loud, naming the offending value, on any structurally invalid
77    /// field — an invalid BPB is a bootstrap failure, never a silent default.
78    pub fn parse(boot: &[u8]) -> Result<Geometry> {
79        let f = read_fields(boot)?;
80        let bps = u64::from(f.bytes_per_sector);
81
82        // root_dir_sectors = ceil(root_entry_count * 32 / bytes_per_sector).
83        let root_dir_sectors = (u64::from(f.root_entry_count) * 32).div_ceil(bps);
84        let meta_sectors = u64::from(f.reserved_sectors)
85            + u64::from(f.num_fats) * u64::from(f.fat_size_sectors)
86            + root_dir_sectors;
87        if meta_sectors > f.total_sectors {
88            return Err(FatError::InvalidBoot(format!(
89                "reserved+FATs+root ({meta_sectors} sectors) exceeds total ({})",
90                f.total_sectors
91            )));
92        }
93
94        let data_sectors = f.total_sectors - meta_sectors;
95        let count_of_clusters =
96            u32::try_from(data_sectors / u64::from(f.sectors_per_cluster)).unwrap_or(u32::MAX);
97        let variant = if count_of_clusters < FAT12_MAX_CLUSTERS {
98            FatVariant::Fat12
99        } else if count_of_clusters < FAT16_MAX_CLUSTERS {
100            FatVariant::Fat16
101        } else {
102            FatVariant::Fat32
103        };
104        let is32 = variant == FatVariant::Fat32;
105
106        let fat_start = u64::from(f.reserved_sectors) * bps;
107        let root_region_start = (u64::from(f.reserved_sectors)
108            + u64::from(f.num_fats) * u64::from(f.fat_size_sectors))
109            * bps;
110        let (root_dir_start, root_dir_bytes) = if is32 {
111            (0, 0)
112        } else {
113            (
114                root_region_start,
115                u32::try_from(root_dir_sectors * bps).unwrap_or(u32::MAX),
116            )
117        };
118
119        Ok(Geometry {
120            variant,
121            bytes_per_sector: f.bytes_per_sector,
122            sectors_per_cluster: f.sectors_per_cluster,
123            cluster_size: f.bytes_per_sector * f.sectors_per_cluster,
124            reserved_sectors: f.reserved_sectors,
125            num_fats: f.num_fats,
126            fat_size_sectors: f.fat_size_sectors,
127            root_entry_count: if is32 { 0 } else { f.root_entry_count },
128            total_sectors: f.total_sectors,
129            root_cluster: if is32 { le_u32(boot, 44) } else { 0 },
130            count_of_clusters,
131            fat_start,
132            root_dir_start,
133            root_dir_bytes,
134            data_start: meta_sectors * bps,
135        })
136    }
137
138    /// Byte offset of the first sector of `cluster` (>= 2) in the data region.
139    pub fn cluster_offset(&self, cluster: u32) -> Option<u64> {
140        if cluster < 2 {
141            return None;
142        }
143        let rel = u64::from(cluster - 2).checked_mul(u64::from(self.cluster_size))?;
144        self.data_start.checked_add(rel)
145    }
146}
147
148/// Read and range-check the BPB fields, failing loud with the offending value.
149fn read_fields(boot: &[u8]) -> Result<Fields> {
150    let sig = le_u16(boot, 510);
151    if sig != 0xAA55 {
152        return Err(FatError::InvalidBoot(format!(
153            "boot signature at 0x1FE is {sig:#06x}, expected 0x55AA"
154        )));
155    }
156
157    let bytes_per_sector = u32::from(le_u16(boot, 11));
158    if !bytes_per_sector.is_power_of_two() || !(512..=4096).contains(&bytes_per_sector) {
159        return Err(FatError::InvalidBoot(format!(
160            "bytes-per-sector at 0x0B is {bytes_per_sector}, not a power of two in 512..=4096"
161        )));
162    }
163
164    let sectors_per_cluster = u32::from(u8_at(boot, 13));
165    if !sectors_per_cluster.is_power_of_two() || !(1..=128).contains(&sectors_per_cluster) {
166        return Err(FatError::InvalidBoot(format!(
167            "sectors-per-cluster at 0x0D is {sectors_per_cluster}, not a power of two in 1..=128"
168        )));
169    }
170
171    let reserved_sectors = u32::from(le_u16(boot, 14));
172    if reserved_sectors == 0 {
173        return Err(FatError::InvalidBoot(
174            "reserved-sector count at 0x0E is 0".into(),
175        ));
176    }
177
178    let num_fats = u32::from(u8_at(boot, 16));
179    if num_fats == 0 {
180        return Err(FatError::InvalidBoot("number of FATs at 0x10 is 0".into()));
181    }
182
183    let fat_size_16 = u32::from(le_u16(boot, 22));
184    let fat_size_sectors = if fat_size_16 != 0 {
185        fat_size_16
186    } else {
187        le_u32(boot, 36)
188    };
189    if fat_size_sectors == 0 {
190        return Err(FatError::InvalidBoot(
191            "FAT size is 0 (both 0x16 and 0x24 are zero)".into(),
192        ));
193    }
194
195    let total_sectors_16 = u32::from(le_u16(boot, 19));
196    let total_sectors = if total_sectors_16 != 0 {
197        u64::from(total_sectors_16)
198    } else {
199        u64::from(le_u32(boot, 32))
200    };
201    if total_sectors == 0 {
202        return Err(FatError::InvalidBoot(
203            "total sector count is 0 (both 0x13 and 0x20 are zero)".into(),
204        ));
205    }
206
207    Ok(Fields {
208        bytes_per_sector,
209        sectors_per_cluster,
210        reserved_sectors,
211        num_fats,
212        root_entry_count: u32::from(le_u16(boot, 17)),
213        fat_size_sectors,
214        total_sectors,
215    })
216}
217
218#[cfg(test)]
219mod tests {
220    use super::{FatVariant, Geometry};
221
222    /// A classic 1.44 MiB floppy BPB: 512 B/sector, 1 sector/cluster,
223    /// 1 reserved, 2 FATs of 9 sectors, 224 root entries, 2880 total sectors.
224    fn fat12_boot() -> Vec<u8> {
225        let mut b = vec![0u8; 512];
226        b[0] = 0xEB;
227        b[1] = 0x3C;
228        b[2] = 0x90;
229        b[11..13].copy_from_slice(&512u16.to_le_bytes()); // bytes/sector
230        b[13] = 1; // sectors/cluster
231        b[14..16].copy_from_slice(&1u16.to_le_bytes()); // reserved
232        b[16] = 2; // num FATs
233        b[17..19].copy_from_slice(&224u16.to_le_bytes()); // root entries
234        b[19..21].copy_from_slice(&2880u16.to_le_bytes()); // total sectors 16
235        b[22..24].copy_from_slice(&9u16.to_le_bytes()); // FAT size 16
236        b[510] = 0x55;
237        b[511] = 0xAA;
238        b
239    }
240
241    /// A minimal FAT32 BPB: 512 B/sector, 1 sector/cluster, 32 reserved,
242    /// 2 FATs of 512 sectors, 0 root entries, root cluster 2, 70000 sectors.
243    fn fat32_boot() -> Vec<u8> {
244        let mut b = vec![0u8; 512];
245        b[0] = 0xEB;
246        b[2] = 0x90;
247        b[11..13].copy_from_slice(&512u16.to_le_bytes());
248        b[13] = 1;
249        b[14..16].copy_from_slice(&32u16.to_le_bytes());
250        b[16] = 2;
251        b[17..19].copy_from_slice(&0u16.to_le_bytes());
252        b[19..21].copy_from_slice(&0u16.to_le_bytes()); // total16 = 0 → use total32
253        b[22..24].copy_from_slice(&0u16.to_le_bytes()); // fat16 = 0 → use fat32
254        b[32..36].copy_from_slice(&70000u32.to_le_bytes()); // total sectors 32
255        b[36..40].copy_from_slice(&512u32.to_le_bytes()); // FAT size 32
256        b[44..48].copy_from_slice(&2u32.to_le_bytes()); // root cluster
257        b[510] = 0x55;
258        b[511] = 0xAA;
259        b
260    }
261
262    #[test]
263    fn detects_fat12_geometry() {
264        let g = Geometry::parse(&fat12_boot()).unwrap();
265        assert_eq!(g.variant, FatVariant::Fat12);
266        assert_eq!(g.bytes_per_sector, 512);
267        assert_eq!(g.sectors_per_cluster, 1);
268        assert_eq!(g.cluster_size, 512);
269        // root_dir_sectors = (224*32+511)/512 = 14; data = 2880-(1+18+14)=2847
270        assert_eq!(g.count_of_clusters, 2847);
271        assert_eq!(g.fat_start, 512);
272        assert_eq!(g.root_dir_start, 19 * 512);
273        assert_eq!(g.root_dir_bytes, 14 * 512);
274        assert_eq!(g.data_start, 33 * 512);
275    }
276
277    #[test]
278    fn detects_fat32_geometry() {
279        let g = Geometry::parse(&fat32_boot()).unwrap();
280        assert_eq!(g.variant, FatVariant::Fat32);
281        assert_eq!(g.root_entry_count, 0);
282        assert_eq!(g.root_cluster, 2);
283        assert_eq!(g.root_dir_bytes, 0);
284        // data = 70000 - (32 + 2*512 + 0) = 68944 clusters (>= 65525 → FAT32)
285        assert_eq!(g.count_of_clusters, 68944);
286        assert_eq!(g.data_start, (32 + 1024) * 512);
287    }
288
289    #[test]
290    fn detects_fat16_boundary() {
291        // 5000 clusters → FAT16 (>= 4085, < 65525)
292        let mut b = fat12_boot();
293        b[17..19].copy_from_slice(&0u16.to_le_bytes()); // no root region for simplicity
294        b[19..21].copy_from_slice(&0u16.to_le_bytes());
295        b[32..36].copy_from_slice(&5033u32.to_le_bytes()); // total32
296        b[22..24].copy_from_slice(&0u16.to_le_bytes());
297        b[36..40].copy_from_slice(&16u32.to_le_bytes()); // fat32 slot as fat_size fallback
298                                                         // total = 5033, reserved 1, 2 FATs*16 = 32, root 0 → data 5000 → FAT16
299        let g = Geometry::parse(&b).unwrap();
300        assert_eq!(g.variant, FatVariant::Fat16);
301        assert_eq!(g.count_of_clusters, 5000);
302    }
303
304    #[test]
305    fn cluster_offset_maps_from_data_start() {
306        let g = Geometry::parse(&fat12_boot()).unwrap();
307        // cluster 2 is the first data cluster → data_start.
308        assert_eq!(g.cluster_offset(2), Some(g.data_start));
309        // cluster 3 is one cluster further in.
310        assert_eq!(
311            g.cluster_offset(3),
312            Some(g.data_start + u64::from(g.cluster_size))
313        );
314        // clusters 0 and 1 are reserved, not addressable.
315        assert_eq!(g.cluster_offset(0), None);
316        assert_eq!(g.cluster_offset(1), None);
317    }
318
319    #[test]
320    fn rejects_bad_signature() {
321        let mut b = fat12_boot();
322        b[510] = 0x00;
323        let e = Geometry::parse(&b).unwrap_err();
324        assert!(format!("{e}").contains("55")); // reports the offending signature
325    }
326
327    #[test]
328    fn rejects_non_power_of_two_sector() {
329        let mut b = fat12_boot();
330        b[11..13].copy_from_slice(&513u16.to_le_bytes());
331        assert!(Geometry::parse(&b).is_err());
332    }
333
334    #[test]
335    fn rejects_zero_fats() {
336        let mut b = fat12_boot();
337        b[16] = 0;
338        assert!(Geometry::parse(&b).is_err());
339    }
340
341    #[test]
342    fn rejects_non_power_of_two_cluster() {
343        let mut b = fat12_boot();
344        b[13] = 3; // sectors-per-cluster not a power of two
345        assert!(Geometry::parse(&b).is_err());
346    }
347
348    #[test]
349    fn rejects_zero_reserved() {
350        let mut b = fat12_boot();
351        b[14..16].copy_from_slice(&0u16.to_le_bytes());
352        assert!(Geometry::parse(&b).is_err());
353    }
354
355    #[test]
356    fn rejects_zero_fat_size() {
357        let mut b = fat12_boot();
358        b[22..24].copy_from_slice(&0u16.to_le_bytes()); // fat16=0
359        b[36..40].copy_from_slice(&0u32.to_le_bytes()); // fat32=0
360        assert!(Geometry::parse(&b).is_err());
361    }
362
363    #[test]
364    fn rejects_zero_total_sectors() {
365        let mut b = fat12_boot();
366        b[19..21].copy_from_slice(&0u16.to_le_bytes()); // total16=0
367        b[32..36].copy_from_slice(&0u32.to_le_bytes()); // total32=0
368        assert!(Geometry::parse(&b).is_err());
369    }
370
371    #[test]
372    fn rejects_oversize_reserved_region() {
373        // reserved region larger than the volume → data underflow, must fail loud
374        let mut b = fat12_boot();
375        b[14..16].copy_from_slice(&9000u16.to_le_bytes());
376        assert!(Geometry::parse(&b).is_err());
377    }
378}