1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum BlockFormat {
12 Fat(FatVariant),
14 PartitionTable(PartitionTableKind),
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum FatVariant {
22 Fat12,
24 Fat16,
26 Fat32,
28 ExFat,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum PartitionTableKind {
36 Mbr,
38 Gpt,
40 Hybrid,
42}
43
44pub 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 §or[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")]
134pub mod sync {
136 use super::{BlockFormat, PartitionTableKind, detect_sector};
137 use hadris_io::sync::{Read, Seek};
138 use hadris_io::{Result, SeekFrom};
139
140 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(§or);
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")]
182pub 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 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(§or);
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(§or),
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(§or),
274 Some(BlockFormat::PartitionTable(PartitionTableKind::Mbr))
275 );
276
277 sector[446 + 4] = 0xee;
278 assert_eq!(
279 detect_sector(§or),
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}