1use crate::boot::{FatVariant, Geometry};
11use crate::bytes::{le_u16, le_u32, le_u64, u8_at};
12use crate::error::{FatError, Result};
13use crate::time::{decode as decode_time, FatTimestamp};
14
15const ATTR_DIRECTORY: u16 = 0x0010;
17
18const TYPE_FILE: u8 = 0x05;
20const TYPE_STREAM_EXT: u8 = 0x40;
21const TYPE_FILE_NAME: u8 = 0x41;
22
23#[derive(Debug, Clone)]
25pub struct ExfatDirEntry {
26 pub name: String,
28 pub attributes: u16,
30 pub is_dir: bool,
32 pub deleted: bool,
34 pub first_cluster: u32,
36 pub size: u64,
38 pub contiguous: bool,
40 pub index: u16,
42 pub created: Option<FatTimestamp>,
44 pub modified: Option<FatTimestamp>,
46 pub accessed: Option<FatTimestamp>,
48}
49
50fn ts_decode(raw: u32, tenths: u8) -> Option<FatTimestamp> {
53 decode_time((raw >> 16) as u16, (raw & 0xFFFF) as u16, tenths)
54}
55
56pub fn parse_directory(data: &[u8]) -> Vec<ExfatDirEntry> {
61 let slots: Vec<&[u8]> = data.chunks_exact(32).collect();
62 let mut out = Vec::new();
63 let mut i = 0;
64 while i < slots.len() {
65 let e = slots[i];
66 let ty = e[0];
67 if ty == 0x00 {
68 break;
69 }
70 if ty & 0x7F != TYPE_FILE {
71 i += 1;
72 continue;
73 }
74 let secondary_count = usize::from(e[1]);
75 let entry = decode_set(&slots, i, secondary_count, e);
76 out.push(entry);
77 i += 1 + secondary_count;
78 }
79 out
80}
81
82fn decode_set(slots: &[&[u8]], i: usize, secondary_count: usize, file: &[u8]) -> ExfatDirEntry {
84 let deleted = file[0] & 0x80 == 0;
85 let attributes = le_u16(file, 4);
86
87 let mut first_cluster = 0u32;
88 let mut size = 0u64;
89 let mut contiguous = false;
90 let mut name_len = 0usize;
91 let mut units: Vec<u16> = Vec::new();
92
93 for j in 1..=secondary_count {
94 let Some(s) = slots.get(i + j) else {
95 break;
96 };
97 match s[0] & 0x7F {
98 TYPE_STREAM_EXT => {
99 contiguous = s[1] & 0x02 != 0;
100 name_len = usize::from(s[3]);
101 first_cluster = le_u32(s, 20);
102 size = le_u64(s, 24);
103 }
104 TYPE_FILE_NAME => {
105 for k in 0..15 {
107 units.push(le_u16(s, 2 + k * 2));
108 }
109 }
110 _ => {}
111 }
112 }
113 units.truncate(name_len);
114
115 ExfatDirEntry {
116 name: String::from_utf16_lossy(&units),
117 attributes,
118 is_dir: attributes & ATTR_DIRECTORY != 0,
119 deleted,
120 first_cluster,
121 size,
122 contiguous,
123 index: u16::try_from(i).unwrap_or(u16::MAX),
124 created: ts_decode(le_u32(file, 8), u8_at(file, 20)),
125 modified: ts_decode(le_u32(file, 12), u8_at(file, 21)),
126 accessed: ts_decode(le_u32(file, 16), 0),
127 }
128}
129
130pub fn parse_boot(boot: &[u8]) -> Result<Geometry> {
136 if boot.get(3..11) != Some(b"EXFAT ") {
137 return Err(FatError::NotFat(format!(
138 "exFAT signature at 0x03 is {:02X?}, expected \"EXFAT \"",
139 boot.get(3..11).unwrap_or(&[])
140 )));
141 }
142 let sig = crate::bytes::le_u16(boot, 510);
143 if sig != 0xAA55 {
144 return Err(FatError::InvalidBoot(format!(
145 "boot signature at 0x1FE is {sig:#06x}, expected 0x55AA"
146 )));
147 }
148
149 let bps_shift = u8_at(boot, 108);
150 if !(9..=12).contains(&bps_shift) {
151 return Err(FatError::InvalidBoot(format!(
152 "BytesPerSectorShift at 0x6C is {bps_shift}, not in 9..=12 (512..4096)"
153 )));
154 }
155 let spc_shift = u8_at(boot, 109);
156 if u32::from(bps_shift) + u32::from(spc_shift) > 25 {
158 return Err(FatError::InvalidBoot(format!(
159 "SectorsPerClusterShift at 0x6D is {spc_shift}; cluster size exceeds 32 MiB"
160 )));
161 }
162 let bytes_per_sector = 1u32 << bps_shift;
163 let sectors_per_cluster = 1u32 << spc_shift;
164
165 let num_fats = u8_at(boot, 110);
166 if num_fats == 0 || num_fats > 2 {
167 return Err(FatError::InvalidBoot(format!(
168 "NumberOfFats at 0x6E is {num_fats}, must be 1 or 2"
169 )));
170 }
171
172 let fat_offset = le_u32(boot, 80);
173 let fat_length = le_u32(boot, 84);
174 let cluster_heap_offset = le_u32(boot, 88);
175 let count_of_clusters = le_u32(boot, 92);
176 let root_cluster = le_u32(boot, 96);
177
178 if root_cluster < 2 || root_cluster > count_of_clusters.saturating_add(1) {
179 return Err(FatError::InvalidBoot(format!(
180 "root cluster {root_cluster} outside 2..={}",
181 count_of_clusters.saturating_add(1)
182 )));
183 }
184
185 let bps = u64::from(bytes_per_sector);
186 Ok(Geometry {
187 variant: FatVariant::ExFat,
188 bytes_per_sector,
189 sectors_per_cluster,
190 cluster_size: bytes_per_sector * sectors_per_cluster,
191 reserved_sectors: 0,
192 num_fats: u32::from(num_fats),
193 fat_size_sectors: fat_length,
194 root_entry_count: 0,
195 total_sectors: crate::bytes::le_u64(boot, 72),
196 root_cluster,
197 count_of_clusters,
198 fat_start: u64::from(fat_offset) * bps,
199 root_dir_start: 0,
200 root_dir_bytes: 0,
201 data_start: u64::from(cluster_heap_offset) * bps,
202 })
203}
204
205pub fn boot_checksum(sectors: &[u8], bytes_per_sector: u32) -> u32 {
209 let n = (bytes_per_sector as usize)
210 .saturating_mul(11)
211 .min(sectors.len());
212 let mut checksum: u32 = 0;
213 for (i, &b) in sectors.iter().take(n).enumerate() {
214 if i == 106 || i == 107 || i == 112 {
215 continue;
216 }
217 checksum = checksum.rotate_right(1).wrapping_add(u32::from(b));
218 }
219 checksum
220}
221
222#[cfg(test)]
223mod tests {
224 use super::{boot_checksum, parse_boot, parse_directory};
225 use crate::boot::FatVariant;
226
227 fn file_entry(deleted: bool, secondary_count: u8, attrs: u16) -> [u8; 32] {
229 let mut e = [0u8; 32];
230 e[0] = if deleted { 0x05 } else { 0x85 };
231 e[1] = secondary_count;
232 e[4..6].copy_from_slice(&attrs.to_le_bytes());
233 e
234 }
235
236 fn stream_ext(
238 deleted: bool,
239 no_fat_chain: bool,
240 name_len: u8,
241 first: u32,
242 size: u64,
243 ) -> [u8; 32] {
244 let mut e = [0u8; 32];
245 e[0] = if deleted { 0x40 } else { 0xC0 };
246 e[1] = if no_fat_chain { 0x03 } else { 0x01 }; e[3] = name_len;
248 e[20..24].copy_from_slice(&first.to_le_bytes());
249 e[24..32].copy_from_slice(&size.to_le_bytes());
250 e
251 }
252
253 fn file_name(deleted: bool, chars: &[u16]) -> [u8; 32] {
255 let mut e = [0u8; 32];
256 e[0] = if deleted { 0x41 } else { 0xC1 };
257 for (i, &c) in chars.iter().take(15).enumerate() {
259 e[2 + i * 2..4 + i * 2].copy_from_slice(&c.to_le_bytes());
260 }
261 e
262 }
263
264 fn entry_set(
265 deleted: bool,
266 name: &str,
267 attrs: u16,
268 contiguous: bool,
269 first: u32,
270 size: u64,
271 ) -> Vec<u8> {
272 let chars: Vec<u16> = name.encode_utf16().collect();
273 let mut v = Vec::new();
274 v.extend_from_slice(&file_entry(deleted, 2, attrs));
275 v.extend_from_slice(&stream_ext(
276 deleted,
277 contiguous,
278 chars.len() as u8,
279 first,
280 size,
281 ));
282 v.extend_from_slice(&file_name(deleted, &chars));
283 v
284 }
285
286 fn synth_boot() -> Vec<u8> {
287 let mut b = vec![0u8; 512];
288 b[0] = 0xEB;
289 b[1] = 0x76;
290 b[2] = 0x90;
291 b[3..11].copy_from_slice(b"EXFAT ");
292 b[80..84].copy_from_slice(&24u32.to_le_bytes()); b[84..88].copy_from_slice(&8u32.to_le_bytes()); b[88..92].copy_from_slice(&32u32.to_le_bytes()); b[92..96].copy_from_slice(&100u32.to_le_bytes()); b[96..100].copy_from_slice(&5u32.to_le_bytes()); b[108] = 9; b[109] = 3; b[110] = 1; b[510] = 0x55;
301 b[511] = 0xAA;
302 b
303 }
304
305 #[test]
306 fn parses_exfat_geometry() {
307 let g = parse_boot(&synth_boot()).unwrap();
308 assert_eq!(g.variant, FatVariant::ExFat);
309 assert_eq!(g.bytes_per_sector, 512);
310 assert_eq!(g.cluster_size, 4096);
311 assert_eq!(g.fat_start, 24 * 512);
312 assert_eq!(g.data_start, 32 * 512); assert_eq!(g.root_cluster, 5);
314 assert_eq!(g.count_of_clusters, 100);
315 }
316
317 #[test]
318 fn rejects_bad_signature() {
319 let mut b = synth_boot();
320 b[510] = 0;
321 assert!(parse_boot(&b).is_err());
322 }
323
324 #[test]
325 fn rejects_out_of_range_sector_shift() {
326 let mut b = synth_boot();
327 b[108] = 20; assert!(parse_boot(&b).is_err());
329 }
330
331 #[test]
332 fn rejects_wrong_exfat_signature() {
333 let mut b = synth_boot();
334 b[3..11].copy_from_slice(b"NOTEXFAT");
335 assert!(parse_boot(&b).is_err());
336 }
337
338 #[test]
339 fn rejects_oversize_cluster_shift() {
340 let mut b = synth_boot();
341 b[109] = 20; assert!(parse_boot(&b).is_err());
343 }
344
345 #[test]
346 fn rejects_bad_fat_count() {
347 let mut b = synth_boot();
348 b[110] = 3; assert!(parse_boot(&b).is_err());
350 b[110] = 0;
351 assert!(parse_boot(&b).is_err());
352 }
353
354 #[test]
355 fn rejects_root_cluster_out_of_range() {
356 let mut b = synth_boot();
357 b[96..100].copy_from_slice(&1u32.to_le_bytes()); assert!(parse_boot(&b).is_err());
359 b[96..100].copy_from_slice(&9999u32.to_le_bytes()); assert!(parse_boot(&b).is_err());
361 }
362
363 #[test]
364 fn parses_a_file_entry_set() {
365 let mut dir = Vec::new();
366 dir.extend_from_slice(&entry_set(false, "Hi.txt", 0x20, true, 10, 5));
367 let entries = parse_directory(&dir);
368 assert_eq!(entries.len(), 1);
369 let e = &entries[0];
370 assert_eq!(e.name, "Hi.txt");
371 assert_eq!(e.size, 5);
372 assert_eq!(e.first_cluster, 10);
373 assert!(e.contiguous);
374 assert!(!e.is_dir);
375 assert!(!e.deleted);
376 }
377
378 #[test]
379 fn parses_directory_and_deleted_and_skips_system_entries() {
380 let mut dir = Vec::new();
381 let mut bitmap = [0u8; 32];
383 bitmap[0] = 0x81;
384 dir.extend_from_slice(&bitmap);
385 dir.extend_from_slice(&entry_set(false, "sub", 0x10, false, 20, 0)); dir.extend_from_slice(&entry_set(true, "gone.txt", 0x20, true, 30, 7)); let entries = parse_directory(&dir);
388 assert_eq!(entries.len(), 2);
389 assert_eq!(entries[0].name, "sub");
390 assert!(entries[0].is_dir);
391 assert_eq!(entries[1].name, "gone.txt");
392 assert!(entries[1].deleted);
393 }
394
395 #[test]
396 fn lying_secondary_count_is_bounded() {
397 let mut dir = Vec::new();
399 dir.extend_from_slice(&file_entry(false, 9, 0x20));
400 dir.extend_from_slice(&stream_ext(false, true, 1, 5, 1));
401 let entries = parse_directory(&dir);
403 assert_eq!(entries.len(), 1);
404 }
405
406 #[test]
407 fn unknown_secondary_type_is_ignored() {
408 let mut dir = Vec::new();
409 dir.extend_from_slice(&file_entry(false, 2, 0x20));
410 dir.extend_from_slice(&stream_ext(false, true, 1, 5, 1));
411 let mut vendor = [0u8; 32];
412 vendor[0] = 0xE0; dir.extend_from_slice(&vendor);
414 assert_eq!(parse_directory(&dir).len(), 1);
415 }
416
417 #[test]
418 fn stops_at_end_of_directory_marker() {
419 let mut dir = Vec::new();
420 dir.extend_from_slice(&entry_set(false, "a.txt", 0x20, true, 5, 1));
421 dir.extend_from_slice(&[0u8; 32]); dir.extend_from_slice(&entry_set(false, "b.txt", 0x20, true, 6, 1));
423 assert_eq!(parse_directory(&dir).len(), 1);
424 }
425
426 #[test]
427 fn long_name_spans_two_file_name_entries() {
428 let name = "twentycharsname_ok!!";
430 let chars: Vec<u16> = name.encode_utf16().collect();
431 let mut dir = Vec::new();
432 dir.extend_from_slice(&file_entry(false, 3, 0x20));
433 dir.extend_from_slice(&stream_ext(false, true, chars.len() as u8, 40, 3));
434 dir.extend_from_slice(&file_name(false, &chars[..15]));
435 dir.extend_from_slice(&file_name(false, &chars[15..]));
436 assert_eq!(parse_directory(&dir)[0].name, name);
437 }
438
439 #[test]
440 fn checksum_excludes_volume_flags_and_percent_in_use() {
441 let bps = 512usize;
444 let mut region = vec![0u8; 11 * bps];
445 region[3] = 0x41;
446 let base = boot_checksum(®ion, bps as u32);
447 region[106] = 0xFF;
448 region[107] = 0xFF;
449 region[112] = 0xFF;
450 assert_eq!(boot_checksum(®ion, bps as u32), base);
451 region[5] = 0xFF;
453 assert_ne!(boot_checksum(®ion, bps as u32), base);
454 }
455}