Skip to main content

hadris_block/
detect.rs

1//! Lightweight block-format detection.
2//!
3//! Detection examines boot metadata and restores the stream's original
4//! position. It does not validate an entire filesystem or partition table;
5//! callers should open the corresponding concrete crate to perform full
6//! validation.
7
8/// A recognized block-storage layout.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum BlockFormat {
12    /// A FAT filesystem occupying the probed device or bounded partition.
13    Fat(FatVariant),
14    /// A disk partition table.
15    PartitionTable(PartitionTableKind),
16}
17
18/// FAT family identified from its BIOS parameter block.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum FatVariant {
22    /// A FAT12 filesystem.
23    Fat12,
24    /// A FAT16 filesystem.
25    Fat16,
26    /// A FAT32 filesystem.
27    Fat32,
28    /// exFAT was recognized; the stable unified opener does not open it.
29    ExFat,
30}
31
32/// Partition-table family identified from sector-zero and GPT metadata.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum PartitionTableKind {
36    /// A legacy Master Boot Record partition table.
37    Mbr,
38    /// A GUID Partition Table, including its protective MBR.
39    Gpt,
40    /// A GPT with both protective and ordinary MBR entries.
41    Hybrid,
42}
43
44/// Probe a 512-byte logical sector without performing I/O.
45///
46/// A protective MBR is reported as GPT based on its partition entries. The
47/// stream-based detectors additionally check for the GPT header signature.
48pub fn detect_sector(sector: &[u8; 512]) -> Option<BlockFormat> {
49    if let Some(kind) = partition_kind(sector) {
50        return Some(BlockFormat::PartitionTable(kind));
51    }
52    fat_variant(sector).map(BlockFormat::Fat)
53}
54
55fn partition_kind(sector: &[u8; 512]) -> Option<PartitionTableKind> {
56    if sector[510..512] != [0x55, 0xaa] {
57        return None;
58    }
59
60    let mut used = 0u8;
61    let mut protective = false;
62    let mut ordinary = false;
63    for entry in sector[446..510].chunks_exact(16) {
64        if !matches!(entry[0], 0x00 | 0x80) {
65            return None;
66        }
67        let ty = entry[4];
68        let sectors = u32::from_le_bytes([entry[12], entry[13], entry[14], entry[15]]);
69        if ty == 0 || sectors == 0 {
70            continue;
71        }
72        used += 1;
73        protective |= ty == 0xee;
74        ordinary |= ty != 0xee;
75    }
76
77    if used == 0 {
78        None
79    } else if protective && ordinary {
80        Some(PartitionTableKind::Hybrid)
81    } else if protective {
82        Some(PartitionTableKind::Gpt)
83    } else {
84        Some(PartitionTableKind::Mbr)
85    }
86}
87
88fn fat_variant(sector: &[u8; 512]) -> Option<FatVariant> {
89    if sector[510..512] != [0x55, 0xaa] {
90        return None;
91    }
92    if &sector[3..11] == b"EXFAT   " {
93        return Some(FatVariant::ExFat);
94    }
95
96    let bytes_per_sector = u16::from_le_bytes([sector[11], sector[12]]) as u32;
97    let sectors_per_cluster = sector[13] as u32;
98    let reserved = u16::from_le_bytes([sector[14], sector[15]]) as u32;
99    let fats = sector[16] as u32;
100    let root_entries = u16::from_le_bytes([sector[17], sector[18]]) as u32;
101    let total16 = u16::from_le_bytes([sector[19], sector[20]]) as u32;
102    let total32 = u32::from_le_bytes([sector[32], sector[33], sector[34], sector[35]]);
103    let fat16 = u16::from_le_bytes([sector[22], sector[23]]) as u32;
104    let fat32 = u32::from_le_bytes([sector[36], sector[37], sector[38], sector[39]]);
105
106    if !matches!(bytes_per_sector, 512 | 1024 | 2048 | 4096)
107        || sectors_per_cluster == 0
108        || !sectors_per_cluster.is_power_of_two()
109        || reserved == 0
110        || fats == 0
111    {
112        return None;
113    }
114
115    let total = if total16 != 0 { total16 } else { total32 };
116    let fat_size = if fat16 != 0 { fat16 } else { fat32 };
117    let root_sectors = (root_entries * 32).div_ceil(bytes_per_sector);
118    let metadata = reserved
119        .checked_add(fats.checked_mul(fat_size)?)?
120        .checked_add(root_sectors)?;
121    let data_sectors = total.checked_sub(metadata)?;
122    let clusters = data_sectors / sectors_per_cluster;
123
124    Some(if clusters < 4_085 {
125        FatVariant::Fat12
126    } else if clusters < 65_525 {
127        FatVariant::Fat16
128    } else {
129        FatVariant::Fat32
130    })
131}
132
133#[cfg(feature = "sync")]
134/// Synchronous block-format detection.
135pub mod sync {
136    use super::{BlockFormat, PartitionTableKind, detect_sector};
137    use hadris_io::sync::{Read, Seek};
138    use hadris_io::{Result, SeekFrom};
139
140    /// Detect a layout and restore the reader's original position.
141    pub fn detect<R>(reader: &mut R, logical_block_size: u32) -> Result<Option<BlockFormat>>
142    where
143        R: Read + Seek<Error = <R as Read>::Error>,
144    {
145        let original = reader.stream_position().map_err(|error| error.erase())?;
146        let result = detect_at_start(reader, logical_block_size);
147        reader
148            .seek(SeekFrom::Start(original))
149            .map_err(|error| error.erase())?;
150        result
151    }
152
153    fn detect_at_start<R>(reader: &mut R, logical_block_size: u32) -> Result<Option<BlockFormat>>
154    where
155        R: Read + Seek<Error = <R as Read>::Error>,
156    {
157        reader
158            .seek(SeekFrom::Start(0))
159            .map_err(|error| error.erase())?;
160        let mut sector = [0u8; 512];
161        reader.read_exact(&mut sector)?;
162        let detected = detect_sector(&sector);
163        if matches!(
164            detected,
165            Some(BlockFormat::PartitionTable(PartitionTableKind::Gpt))
166        ) && logical_block_size >= 512
167        {
168            reader
169                .seek(SeekFrom::Start(logical_block_size as u64))
170                .map_err(|error| error.erase())?;
171            let mut signature = [0u8; 8];
172            reader.read_exact(&mut signature)?;
173            if &signature != b"EFI PART" {
174                return Ok(None);
175            }
176        }
177        Ok(detected)
178    }
179}
180
181#[cfg(feature = "async")]
182/// Asynchronous block-format detection.
183pub mod r#async {
184    use super::{BlockFormat, PartitionTableKind, detect_sector};
185    use hadris_io::r#async::{Read, Seek};
186    use hadris_io::{Result, SeekFrom};
187
188    /// Detect a layout asynchronously and restore the reader's original position.
189    pub async fn detect<R>(reader: &mut R, logical_block_size: u32) -> Result<Option<BlockFormat>>
190    where
191        R: Read + Seek<Error = <R as Read>::Error>,
192    {
193        let original = reader
194            .stream_position()
195            .await
196            .map_err(|error| error.erase())?;
197        let result = detect_at_start(reader, logical_block_size).await;
198        reader
199            .seek(SeekFrom::Start(original))
200            .await
201            .map_err(|error| error.erase())?;
202        result
203    }
204
205    async fn detect_at_start<R>(
206        reader: &mut R,
207        logical_block_size: u32,
208    ) -> Result<Option<BlockFormat>>
209    where
210        R: Read + Seek<Error = <R as Read>::Error>,
211    {
212        reader
213            .seek(SeekFrom::Start(0))
214            .await
215            .map_err(|error| error.erase())?;
216        let mut sector = [0u8; 512];
217        reader.read_exact(&mut sector).await?;
218        let detected = detect_sector(&sector);
219        if matches!(
220            detected,
221            Some(BlockFormat::PartitionTable(PartitionTableKind::Gpt))
222        ) && logical_block_size >= 512
223        {
224            reader
225                .seek(SeekFrom::Start(logical_block_size as u64))
226                .await
227                .map_err(|error| error.erase())?;
228            let mut signature = [0u8; 8];
229            reader.read_exact(&mut signature).await?;
230            if &signature != b"EFI PART" {
231                return Ok(None);
232            }
233        }
234        Ok(detected)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    fn fat_sector(total: u32, fat_size: u16, sectors_per_cluster: u8) -> [u8; 512] {
243        let mut sector = [0u8; 512];
244        sector[0] = 0xeb;
245        sector[3..11].copy_from_slice(b"HADRIS  ");
246        sector[11..13].copy_from_slice(&512u16.to_le_bytes());
247        sector[13] = sectors_per_cluster;
248        sector[14..16].copy_from_slice(&1u16.to_le_bytes());
249        sector[16] = 2;
250        sector[17..19].copy_from_slice(&512u16.to_le_bytes());
251        sector[19..21].copy_from_slice(&(total as u16).to_le_bytes());
252        sector[22..24].copy_from_slice(&fat_size.to_le_bytes());
253        sector[510..512].copy_from_slice(&[0x55, 0xaa]);
254        sector
255    }
256
257    #[test]
258    fn recognizes_fat_without_mistaking_boot_signature_for_mbr() {
259        let sector = fat_sector(4_000, 12, 1);
260        assert_eq!(
261            detect_sector(&sector),
262            Some(BlockFormat::Fat(FatVariant::Fat12))
263        );
264    }
265
266    #[test]
267    fn recognizes_mbr_and_gpt_partition_entries() {
268        let mut sector = [0u8; 512];
269        sector[446 + 4] = 0x83;
270        sector[446 + 12..446 + 16].copy_from_slice(&100u32.to_le_bytes());
271        sector[510..512].copy_from_slice(&[0x55, 0xaa]);
272        assert_eq!(
273            detect_sector(&sector),
274            Some(BlockFormat::PartitionTable(PartitionTableKind::Mbr))
275        );
276
277        sector[446 + 4] = 0xee;
278        assert_eq!(
279            detect_sector(&sector),
280            Some(BlockFormat::PartitionTable(PartitionTableKind::Gpt))
281        );
282    }
283
284    #[cfg(feature = "sync")]
285    #[test]
286    fn stream_probe_validates_gpt_signature_and_restores_position() {
287        use hadris_io::SeekFrom;
288        use hadris_io::sync::Seek;
289
290        let mut image = [0u8; 1024];
291        image[446 + 4] = 0xee;
292        image[446 + 12..446 + 16].copy_from_slice(&100u32.to_le_bytes());
293        image[510..512].copy_from_slice(&[0x55, 0xaa]);
294        image[512..520].copy_from_slice(b"EFI PART");
295
296        let mut cursor = hadris_io::Cursor::new(&image);
297        cursor.seek(SeekFrom::Start(17)).unwrap();
298        assert_eq!(
299            sync::detect(&mut cursor, 512).unwrap(),
300            Some(BlockFormat::PartitionTable(PartitionTableKind::Gpt))
301        );
302        assert_eq!(cursor.stream_position().unwrap(), 17);
303
304        image[512..520].fill(0);
305        let mut cursor = hadris_io::Cursor::new(&image);
306        assert_eq!(sync::detect(&mut cursor, 512).unwrap(), None);
307    }
308
309    #[cfg(all(feature = "std", feature = "sync", feature = "write", feature = "fat"))]
310    #[test]
311    fn recognizes_volume_created_by_fat_formatter() {
312        use hadris_fat::format::{FatFormatOptions, FatTypeSelection, FatVolumeFormatter};
313
314        let mut image = std::vec![0u8; 2 * 1024 * 1024];
315        let options = FatFormatOptions::new(image.len() as u64).fat_type(FatTypeSelection::Fat12);
316        FatVolumeFormatter::format(std::io::Cursor::new(&mut image[..]), options).unwrap();
317
318        let mut cursor = std::io::Cursor::new(image);
319        assert_eq!(
320            sync::detect(&mut cursor, 512).unwrap(),
321            Some(BlockFormat::Fat(FatVariant::Fat12))
322        );
323    }
324}