1use std::io::{Read, Seek, SeekFrom};
6use std::sync::Mutex;
7
8use crate::boot::{FatVariant, Geometry};
9use crate::dirent::{parse_directory, DirEntry};
10use crate::error::{FatError, Result};
11use crate::fat::follow_chain;
12use crate::time::{decode as decode_time, FatTimestamp};
13
14const MAX_DIR_BYTES: usize = 64 * 1024 * 1024;
16const MAX_FAT_BYTES: u64 = 256 * 1024 * 1024;
18const FIXED_ROOT: u32 = u32::MAX;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FileId {
26 Root,
28 Entry {
31 dir_cluster: u32,
33 index: u16,
35 },
36}
37
38#[derive(Debug, Clone)]
40#[allow(clippy::struct_excessive_bools)] pub struct Node {
42 pub id: FileId,
44 pub name: String,
46 pub short_name: String,
48 pub is_dir: bool,
50 pub is_deleted: bool,
52 pub is_volume_label: bool,
54 pub size: u64,
56 pub attributes: u32,
58 pub first_cluster: u32,
60 pub contiguous: bool,
62 pub created: Option<FatTimestamp>,
64 pub modified: Option<FatTimestamp>,
66 pub accessed: Option<FatTimestamp>,
68}
69
70#[derive(Clone, Copy)]
72enum DirSource {
73 FixedRoot,
75 Chain {
78 first_cluster: u32,
79 size: u64,
80 contiguous: bool,
81 },
82}
83
84pub struct FatFs<R> {
86 inner: Mutex<R>,
87 geom: Geometry,
88 fat: Vec<u8>,
89 max_clusters: usize,
90}
91
92impl<R: Read + Seek> FatFs<R> {
93 pub fn open(mut reader: R) -> Result<Self> {
97 let mut boot = [0u8; 512];
98 read_exact_into(&mut reader, 0, &mut boot)?;
99
100 let geom = if &boot[3..11] == b"EXFAT " {
103 crate::exfat::parse_boot(&boot)?
104 } else {
105 Geometry::parse(&boot)?
106 };
107 let fat_bytes = u64::from(geom.fat_size_sectors) * u64::from(geom.bytes_per_sector);
110 let fat_len = usize::try_from(fat_bytes.min(MAX_FAT_BYTES)).unwrap_or(0);
111 let fat = read_upto(&mut reader, geom.fat_start, fat_len)?;
112 let max_clusters = geom.count_of_clusters as usize + 2;
113
114 Ok(FatFs {
115 inner: Mutex::new(reader),
116 geom,
117 fat,
118 max_clusters,
119 })
120 }
121
122 pub fn variant(&self) -> FatVariant {
124 self.geom.variant
125 }
126
127 pub fn geometry(&self) -> &Geometry {
131 &self.geom
132 }
133
134 pub fn runs(&self, id: FileId) -> Result<Vec<(u64, u64)>> {
139 let (chain, total) = self.data_extent(id)?;
140 if chain.is_empty() {
141 return Ok(if total == 0 {
142 Vec::new()
143 } else {
144 vec![(self.geom.root_dir_start, total)]
145 });
146 }
147 let cluster_size = u64::from(self.geom.cluster_size);
148 let mut runs: Vec<(u64, u64)> = Vec::new();
149 let mut remaining = total;
150 for &c in &chain {
151 if remaining == 0 {
152 break;
153 }
154 let Some(off) = self.geom.cluster_offset(c) else {
155 break; };
157 let len = cluster_size.min(remaining);
158 remaining -= len;
159 match runs.last_mut() {
160 Some(last) if last.0 + last.1 == off => last.1 += len,
161 _ => runs.push((off, len)),
162 }
163 }
164 Ok(runs)
165 }
166
167 pub fn root(&self) -> FileId {
169 FileId::Root
170 }
171
172 pub fn read_dir(&self, id: FileId) -> Result<Vec<Node>> {
174 let source = self.dir_source(id)?;
175 let marker = source_marker(source);
176 let bytes = self.read_dir_bytes(source)?;
177 Ok(self.list_nodes(&bytes, marker))
178 }
179
180 pub fn lookup(&self, dir: FileId, name: &[u8]) -> Result<Option<FileId>> {
182 for node in self.read_dir(dir)? {
183 if !node.is_deleted && !node.is_volume_label && node.name.as_bytes() == name {
184 return Ok(Some(node.id));
185 }
186 }
187 Ok(None)
188 }
189
190 pub fn meta(&self, id: FileId) -> Result<Node> {
192 match id {
193 FileId::Root => Ok(root_node()),
194 FileId::Entry { .. } => self.resolve_entry(id),
195 }
196 }
197
198 pub fn read_at(&self, id: FileId, off: u64, buf: &mut [u8]) -> Result<usize> {
201 let (chain, total) = self.data_extent(id)?;
202 if off >= total {
203 return Ok(0);
204 }
205 let cluster_size = u64::from(self.geom.cluster_size);
206 let end = off.saturating_add(buf.len() as u64).min(total);
207 let mut pos = off;
208 let mut written = 0usize;
209 while pos < end {
210 let ci = usize::try_from(pos / cluster_size).unwrap_or(usize::MAX);
211 let Some(&cluster) = chain.get(ci) else {
212 break; };
214 let Some(base) = self.geom.cluster_offset(cluster) else {
215 break; };
217 let intra = pos % cluster_size;
218 let avail = cluster_size - intra;
219 let want = usize::try_from((end - pos).min(avail)).unwrap_or(0);
220 let got = self.read_region(base + intra, &mut buf[written..written + want])?;
221 if got == 0 {
222 break; }
224 written += got;
225 pos += got as u64;
226 }
227 Ok(written)
228 }
229
230 fn list_nodes(&self, bytes: &[u8], marker: u32) -> Vec<Node> {
234 if self.geom.variant == FatVariant::ExFat {
235 crate::exfat::parse_directory(bytes)
236 .into_iter()
237 .map(|e| node_from_exfat(marker, e))
238 .collect()
239 } else {
240 parse_directory(bytes)
241 .into_iter()
242 .map(|e| node_from(marker, e))
243 .collect()
244 }
245 }
246
247 fn dir_source(&self, id: FileId) -> Result<DirSource> {
249 match id {
250 FileId::Root => Ok(if self.root_is_clustered() {
251 DirSource::Chain {
252 first_cluster: self.geom.root_cluster,
253 size: 0,
254 contiguous: false,
255 }
256 } else {
257 DirSource::FixedRoot
258 }),
259 FileId::Entry { .. } => {
260 let node = self.resolve_entry(id)?;
261 if !node.is_dir {
262 return Err(FatError::Corrupt(format!(
263 "{:?} is not a directory",
264 node.name
265 )));
266 }
267 Ok(DirSource::Chain {
268 first_cluster: node.first_cluster,
269 size: node.size,
270 contiguous: node.contiguous,
271 })
272 }
273 }
274 }
275
276 fn root_is_clustered(&self) -> bool {
279 matches!(self.geom.variant, FatVariant::Fat32 | FatVariant::ExFat)
280 }
281
282 fn resolve_entry(&self, id: FileId) -> Result<Node> {
286 let FileId::Entry { dir_cluster, index } = id else {
287 return Ok(root_node()); };
289 let source = if dir_cluster == FIXED_ROOT {
290 DirSource::FixedRoot
291 } else {
292 DirSource::Chain {
293 first_cluster: dir_cluster,
294 size: 0,
295 contiguous: false,
296 }
297 };
298 let bytes = self.read_dir_bytes(source)?;
299 self.list_nodes(&bytes, dir_cluster)
300 .into_iter()
301 .find(|n| matches!(n.id, FileId::Entry { index: i, .. } if i == index))
302 .ok_or_else(|| FatError::Corrupt(format!("no directory entry at slot {index}")))
303 }
304
305 fn data_extent(&self, id: FileId) -> Result<(Vec<u32>, u64)> {
307 match id {
308 FileId::Root => match self.dir_source(id)? {
309 DirSource::FixedRoot => Ok((Vec::new(), u64::from(self.geom.root_dir_bytes))),
310 DirSource::Chain {
311 first_cluster,
312 size,
313 contiguous,
314 } => Ok(self.extent_of(first_cluster, size, contiguous)),
315 },
316 FileId::Entry { .. } => {
317 let node = self.resolve_entry(id)?;
318 Ok(self.extent_of(node.first_cluster, node.size, node.contiguous))
319 }
320 }
321 }
322
323 fn extent_of(&self, first_cluster: u32, size: u64, contiguous: bool) -> (Vec<u32>, u64) {
326 let clusters = self.clusters(first_cluster, size, contiguous);
327 let total = if size > 0 {
328 size
329 } else {
330 clusters.len() as u64 * u64::from(self.geom.cluster_size)
331 };
332 (clusters, total)
333 }
334
335 fn clusters(&self, first_cluster: u32, size: u64, contiguous: bool) -> Vec<u32> {
338 if first_cluster < 2 {
339 return Vec::new();
340 }
341 if contiguous && size > 0 {
342 let cluster_size = u64::from(self.geom.cluster_size);
343 let n = usize::try_from(size.div_ceil(cluster_size)).unwrap_or(0);
344 let last = u64::from(first_cluster) + n as u64;
345 (u64::from(first_cluster)..last)
346 .take(self.max_clusters)
347 .filter_map(|c| u32::try_from(c).ok())
348 .collect()
349 } else {
350 follow_chain(
351 &self.fat,
352 self.geom.variant,
353 first_cluster,
354 self.max_clusters,
355 )
356 }
357 }
358
359 fn read_dir_bytes(&self, source: DirSource) -> Result<Vec<u8>> {
361 let (first_cluster, size, contiguous) = match source {
362 DirSource::FixedRoot => {
363 return self
364 .read_region_vec(self.geom.root_dir_start, self.geom.root_dir_bytes as usize);
365 }
366 DirSource::Chain {
367 first_cluster,
368 size,
369 contiguous,
370 } => (first_cluster, size, contiguous),
371 };
372 let cluster_size = self.geom.cluster_size as usize;
373 let mut out = Vec::new();
374 for cluster in self.clusters(first_cluster, size, contiguous) {
375 if out.len() >= MAX_DIR_BYTES {
376 break; }
378 let Some(base) = self.geom.cluster_offset(cluster) else {
379 break; };
381 out.extend(self.read_region_vec(base, cluster_size)?);
382 }
383 Ok(out)
384 }
385
386 fn read_region(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
388 let mut guard = self
389 .inner
390 .lock()
391 .map_err(|_| FatError::Corrupt("reader lock poisoned".into()))?;
392 guard
393 .seek(SeekFrom::Start(offset))
394 .map_err(|e| FatError::io("seek", e))?;
395 read_fill(&mut *guard, buf)
396 }
397
398 fn read_region_vec(&self, offset: u64, len: usize) -> Result<Vec<u8>> {
400 let mut buf = vec![0u8; len];
401 let n = self.read_region(offset, &mut buf)?;
402 buf.truncate(n);
403 Ok(buf)
404 }
405}
406
407fn source_marker(source: DirSource) -> u32 {
409 match source {
410 DirSource::FixedRoot => FIXED_ROOT,
411 DirSource::Chain { first_cluster, .. } => first_cluster,
412 }
413}
414
415fn root_node() -> Node {
417 Node {
418 id: FileId::Root,
419 name: String::new(),
420 short_name: String::new(),
421 is_dir: true,
422 is_deleted: false,
423 is_volume_label: false,
424 size: 0,
425 attributes: u32::from(crate::dirent::ATTR_DIRECTORY),
426 first_cluster: 0,
427 contiguous: false,
428 created: None,
429 modified: None,
430 accessed: None,
431 }
432}
433
434fn node_from(marker: u32, e: DirEntry) -> Node {
436 Node {
437 id: FileId::Entry {
438 dir_cluster: marker,
439 index: e.index,
440 },
441 name: e.name,
442 short_name: e.short_name,
443 is_dir: e.is_dir,
444 is_deleted: e.deleted,
445 is_volume_label: e.is_volume_label,
446 size: u64::from(e.size),
447 attributes: u32::from(e.attributes),
448 first_cluster: e.first_cluster,
449 contiguous: false,
450 created: decode_time(e.created.0, e.created.1, e.created.2),
451 modified: decode_time(e.modified.0, e.modified.1, 0),
452 accessed: decode_time(e.accessed, 0, 0),
453 }
454}
455
456fn node_from_exfat(marker: u32, e: crate::exfat::ExfatDirEntry) -> Node {
458 Node {
459 id: FileId::Entry {
460 dir_cluster: marker,
461 index: e.index,
462 },
463 short_name: e.name.clone(),
464 name: e.name,
465 is_dir: e.is_dir,
466 is_deleted: e.deleted,
467 is_volume_label: false,
468 size: e.size,
469 attributes: u32::from(e.attributes),
470 first_cluster: e.first_cluster,
471 contiguous: e.contiguous,
472 created: e.created,
473 modified: e.modified,
474 accessed: e.accessed,
475 }
476}
477
478fn read_exact_into<R: Read + Seek>(reader: &mut R, offset: u64, buf: &mut [u8]) -> Result<()> {
480 reader
481 .seek(SeekFrom::Start(offset))
482 .map_err(|e| FatError::io("seek", e))?;
483 reader
484 .read_exact(buf)
485 .map_err(|e| FatError::io("read boot sector", e))
486}
487
488fn read_upto<R: Read + Seek>(reader: &mut R, offset: u64, len: usize) -> Result<Vec<u8>> {
490 reader
491 .seek(SeekFrom::Start(offset))
492 .map_err(|e| FatError::io("seek", e))?;
493 let mut buf = vec![0u8; len];
494 let n = read_fill(reader, &mut buf)?;
495 buf.truncate(n);
496 Ok(buf)
497}
498
499fn read_fill<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize> {
502 let mut filled = 0;
503 while filled < buf.len() {
504 match reader.read(&mut buf[filled..]) {
505 Ok(0) => break,
506 Ok(n) => filled += n,
507 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
509 Err(e) => return Err(FatError::io("read", e)),
510 }
511 }
512 Ok(filled)
513}
514
515#[cfg(test)]
521pub(crate) fn synth_fat32() -> Vec<u8> {
522 {
523 let bps = 512usize;
524 let reserved = 32usize;
525 let fat_sectors = 512usize;
526 let num_fats = 2usize;
527 let data_start = (reserved + num_fats * fat_sectors) * bps; let mut img = vec![0u8; data_start + 8 * bps]; img[0] = 0xEB;
532 img[2] = 0x90;
533 img[11..13].copy_from_slice(&512u16.to_le_bytes());
534 img[13] = 1; img[14..16].copy_from_slice(&(reserved as u16).to_le_bytes());
536 img[16] = num_fats as u8;
537 img[32..36].copy_from_slice(&70000u32.to_le_bytes()); img[36..40].copy_from_slice(&(fat_sectors as u32).to_le_bytes());
539 img[44..48].copy_from_slice(&2u32.to_le_bytes()); img[510] = 0x55;
541 img[511] = 0xAA;
542
543 let fat = reserved * bps;
545 let eoc = 0x0FFF_FFFFu32.to_le_bytes();
546 let put = |img: &mut [u8], c: usize, v: [u8; 4]| {
547 img[fat + c * 4..fat + c * 4 + 4].copy_from_slice(&v);
548 };
549 put(&mut img, 2, eoc); put(&mut img, 3, eoc); put(&mut img, 4, 5u32.to_le_bytes()); put(&mut img, 5, eoc);
553 put(&mut img, 6, eoc); put(&mut img, 7, 8u32.to_le_bytes()); put(&mut img, 8, eoc);
556 put(&mut img, 10, eoc); let entry = |name: &[u8; 11], cluster: u16, size: u32| {
560 let mut e = [0u8; 32];
561 e[0..11].copy_from_slice(name);
562 e[11] = 0x20;
563 e[26..28].copy_from_slice(&cluster.to_le_bytes());
564 e[28..32].copy_from_slice(&size.to_le_bytes());
565 e
566 };
567 let root = data_start;
568 img[root..root + 32].copy_from_slice(&entry(b"TEST TXT", 3, 9));
569 img[root + 32..root + 64].copy_from_slice(&entry(b"BIG TXT", 4, 600));
570 img[root + 64..root + 96].copy_from_slice(&entry(b"TRUNC TXT", 6, 2000));
571 img[root + 96..root + 128].copy_from_slice(&entry(b"EOF TXT", 10, 512));
572 img[root + 128..root + 160].copy_from_slice(&entry(b"ZERO TXT", 0, 0)); img[root + 160..root + 192].copy_from_slice(&entry(b"HALF TXT", 7, 512)); let mut del = entry(b"GONE TXT", 3, 9);
575 del[0] = 0xE5; img[root + 192..root + 224].copy_from_slice(&del);
577
578 img[data_start + bps..data_start + bps + 9].copy_from_slice(b"hi fat32\n"); for i in 0..600 {
581 img[data_start + 2 * bps + i] = (i % 251) as u8; }
583 img
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use super::{synth_fat32, FatFs};
590 use crate::boot::FatVariant;
591 use std::io::Cursor;
592
593 #[test]
594 fn opens_and_reads_synthetic_fat32() {
595 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
596 assert_eq!(fs.variant(), FatVariant::Fat32);
597
598 let root = fs.root();
599 let nodes = fs.read_dir(root).unwrap();
600 let test = nodes.iter().find(|n| n.name == "TEST.TXT").unwrap();
601 assert_eq!(test.size, 9);
602 assert!(!test.is_dir);
603
604 let id = fs.lookup(root, b"TEST.TXT").unwrap().unwrap();
605 assert_eq!(fs.meta(id).unwrap().size, 9);
606
607 let mut buf = vec![0u8; 16];
608 let n = fs.read_at(id, 0, &mut buf).unwrap();
609 assert_eq!(&buf[..n], b"hi fat32\n");
610 }
611
612 #[test]
613 fn read_at_past_eof_returns_zero() {
614 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
615 let id = fs.lookup(fs.root(), b"TEST.TXT").unwrap().unwrap();
616 let mut buf = [0u8; 8];
617 assert_eq!(fs.read_at(id, 100, &mut buf).unwrap(), 0);
618 }
619
620 #[test]
621 fn open_rejects_non_fat() {
622 let img = vec![0u8; 1024];
623 assert!(FatFs::open(Cursor::new(img)).is_err());
624 }
625
626 #[test]
627 fn lookup_absent_name_is_none() {
628 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
629 assert!(fs.lookup(fs.root(), b"NOPE.TXT").unwrap().is_none());
630 }
631
632 #[test]
633 fn reads_multi_cluster_file_across_boundary() {
634 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
635 let id = fs.lookup(fs.root(), b"BIG.TXT").unwrap().unwrap();
636 assert_eq!(fs.meta(id).unwrap().size, 600);
637 let mut buf = vec![0u8; 600];
638 assert_eq!(fs.read_at(id, 0, &mut buf).unwrap(), 600);
639 for (i, &b) in buf.iter().enumerate() {
640 assert_eq!(b, (i % 251) as u8);
641 }
642 let runs = fs.runs(id).unwrap();
644 assert_eq!(runs.len(), 1);
645 assert_eq!(runs[0].1, 600);
646 }
647
648 #[test]
649 fn empty_file_has_no_runs() {
650 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
651 let id = fs.lookup(fs.root(), b"ZERO.TXT").unwrap().unwrap();
652 assert_eq!(fs.meta(id).unwrap().size, 0);
653 assert!(fs.runs(id).unwrap().is_empty());
654 let mut buf = [0u8; 4];
655 assert_eq!(fs.read_at(id, 0, &mut buf).unwrap(), 0);
656 }
657
658 #[test]
659 fn read_stops_when_chain_shorter_than_size() {
660 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
661 let id = fs.lookup(fs.root(), b"TRUNC.TXT").unwrap().unwrap();
662 let mut buf = vec![0u8; 2000];
664 assert_eq!(fs.read_at(id, 0, &mut buf).unwrap(), 512);
665 }
666
667 #[test]
668 fn read_stops_at_truncated_image() {
669 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
670 let id = fs.lookup(fs.root(), b"EOF.TXT").unwrap().unwrap();
671 let mut buf = vec![0u8; 512];
673 assert_eq!(fs.read_at(id, 0, &mut buf).unwrap(), 0);
674 }
675
676 #[test]
677 fn meta_of_root_is_a_directory() {
678 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
679 let m = fs.meta(fs.root()).unwrap();
680 assert!(m.is_dir);
681 assert_eq!(m.size, 0);
682 }
683
684 #[test]
685 fn read_dir_of_a_file_errs() {
686 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
687 let id = fs.lookup(fs.root(), b"TEST.TXT").unwrap().unwrap();
688 assert!(fs.read_dir(id).is_err());
689 }
690
691 #[test]
692 fn size_smaller_than_chain_bounds_the_last_run() {
693 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
695 let id = fs.lookup(fs.root(), b"HALF.TXT").unwrap().unwrap();
696 let runs = fs.runs(id).unwrap();
697 assert_eq!(runs.iter().map(|r| r.1).sum::<u64>(), 512);
698 }
699
700 #[test]
701 fn runs_of_clustered_root() {
702 let fs = FatFs::open(Cursor::new(synth_fat32())).unwrap();
704 let runs = fs.runs(fs.root()).unwrap();
705 assert!(!runs.is_empty());
706 }
707
708 #[test]
709 fn read_error_mid_stream_surfaces_loud() {
710 struct DataFails {
713 img: Vec<u8>,
714 pos: u64,
715 data_start: u64,
716 }
717 impl std::io::Read for DataFails {
718 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
719 if self.pos >= self.data_start {
720 return Err(std::io::Error::other("data read blocked"));
721 }
722 let start = self.pos as usize;
723 let n = buf.len().min(self.img.len().saturating_sub(start));
724 buf[..n].copy_from_slice(&self.img[start..start + n]);
725 self.pos += n as u64;
726 Ok(n)
727 }
728 }
729 impl std::io::Seek for DataFails {
730 fn seek(&mut self, from: std::io::SeekFrom) -> std::io::Result<u64> {
731 if let std::io::SeekFrom::Start(p) = from {
732 self.pos = p;
733 }
734 Ok(self.pos)
735 }
736 }
737 let img = synth_fat32();
738 let data_start = (32 + 2 * 512) * 512;
739 let reader = DataFails {
740 img,
741 pos: 0,
742 data_start,
743 };
744 let fs = FatFs::open(reader).unwrap(); assert!(matches!(
746 fs.read_dir(fs.root()),
747 Err(crate::FatError::Io { .. })
748 ));
749 }
750
751 #[test]
752 fn io_error_surfaces_loud() {
753 struct Failing;
754 impl std::io::Read for Failing {
755 fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
756 Err(std::io::Error::other("boom"))
757 }
758 }
759 impl std::io::Seek for Failing {
760 fn seek(&mut self, _: std::io::SeekFrom) -> std::io::Result<u64> {
761 Ok(0)
762 }
763 }
764 assert!(matches!(
765 FatFs::open(Failing),
766 Err(crate::FatError::Io { .. })
767 ));
768 }
769}