Skip to main content

fat/
fs.rs

1//! [`FatFs`] — the unified reader. [`FatFs::open`] auto-detects FAT12/16/32
2//! (exFAT is wired in a later unit), then serves uniform navigation: `root`,
3//! `read_dir`, `lookup`, `meta`, `read_at`.
4
5use 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
14/// Cap on bytes materialized for one directory (defends against a lying chain).
15const MAX_DIR_BYTES: usize = 64 * 1024 * 1024;
16/// Cap on the cached FAT region (defends against an absurd `fat_size`).
17const MAX_FAT_BYTES: u64 = 256 * 1024 * 1024;
18/// Marker `dir_cluster` value meaning "the FAT12/16 fixed root region".
19const FIXED_ROOT: u32 = u32::MAX;
20
21/// A handle to a node. The root is distinct; every other node is addressed by
22/// its parent directory's first cluster plus its 32-byte slot index — so the
23/// entry (including a deleted one) can always be re-read from the structure.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FileId {
26    /// The volume root directory.
27    Root,
28    /// A directory entry: `dir_cluster` is the parent directory's first cluster
29    /// (or `FIXED_ROOT` for the FAT12/16 fixed root), `index` the slot.
30    Entry {
31        /// First cluster of the parent directory (or the fixed-root marker).
32        dir_cluster: u32,
33        /// 32-byte slot index of the entry within its directory.
34        index: u16,
35    },
36}
37
38/// A resolved node: identity plus the fields a caller needs to navigate or read.
39#[derive(Debug, Clone)]
40#[allow(clippy::struct_excessive_bools)] // a metadata record, not a state machine
41pub struct Node {
42    /// Stable handle to this node.
43    pub id: FileId,
44    /// Effective name (long name if valid, else 8.3).
45    pub name: String,
46    /// Raw 8.3 short name.
47    pub short_name: String,
48    /// Whether this is a directory.
49    pub is_dir: bool,
50    /// Whether this entry is deleted (`0xE5`).
51    pub is_deleted: bool,
52    /// Whether this is the volume-label entry.
53    pub is_volume_label: bool,
54    /// File size in bytes (0 for directories).
55    pub size: u64,
56    /// Raw attribute bits (FAT 8-bit attr or exFAT 16-bit `FileAttributes`).
57    pub attributes: u32,
58    /// First cluster of the node's data (0 = empty).
59    pub first_cluster: u32,
60    /// Whether the data is contiguous (exFAT NoFatChain; always false on FAT).
61    pub contiguous: bool,
62    /// Decoded creation timestamp (local time), if set.
63    pub created: Option<FatTimestamp>,
64    /// Decoded last-modified timestamp (local time), if set.
65    pub modified: Option<FatTimestamp>,
66    /// Decoded last-access timestamp (date only; local time), if set.
67    pub accessed: Option<FatTimestamp>,
68}
69
70/// Where a directory's raw bytes live.
71#[derive(Clone, Copy)]
72enum DirSource {
73    /// FAT12/16 fixed root region.
74    FixedRoot,
75    /// A cluster extent (FAT32/exFAT root, or any subdirectory). `size == 0`
76    /// means "follow the whole FAT chain" (FAT directories carry no size).
77    Chain {
78        first_cluster: u32,
79        size: u64,
80        contiguous: bool,
81    },
82}
83
84/// A pure-Rust reader over a FAT12/16/32 volume.
85pub 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    /// Open a FAT volume, auto-detecting the variant from the boot sector.
94    ///
95    /// Fails loud if the boot sector is not a recognized FAT/exFAT filesystem.
96    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        // exFAT is architecturally distinct but shares the cluster-offset math;
101        // its boot parser yields a Geometry with variant ExFat.
102        let geom = if &boot[3..11] == b"EXFAT   " {
103            crate::exfat::parse_boot(&boot)?
104        } else {
105            Geometry::parse(&boot)?
106        };
107        // Cache FAT1. Its size is bounded by the (validated) BPB but capped
108        // against an absurd fat_size claim.
109        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    /// The detected FAT variant.
123    pub fn variant(&self) -> FatVariant {
124        self.geom.variant
125    }
126
127    /// The resolved on-disk volume geometry (variant, sector/cluster sizes, FAT
128    /// layout, and byte offsets). Useful to a forensic consumer that needs the
129    /// raw layout the reader computed.
130    pub fn geometry(&self) -> &Geometry {
131        &self.geom
132    }
133
134    /// Contiguous image byte runs `(offset, len)` backing node `id`'s data,
135    /// merging physically adjacent clusters (the FAT12/16 fixed root is a single
136    /// region run). Useful to a forensic consumer that needs the on-disk run
137    /// list of a file or directory.
138    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; // cov:unreachable: chain clusters are >= 2 by construction
156            };
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    /// The root directory handle.
168    pub fn root(&self) -> FileId {
169        FileId::Root
170    }
171
172    /// List the entries of the directory `id` (allocated and deleted).
173    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    /// Resolve a single child of `dir` by name (allocated entries only).
181    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    /// Resolve metadata for `id`.
191    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    /// Read up to `buf.len()` bytes at byte offset `off` within node `id`.
199    /// Returns the number of bytes read (0 at or past the end of the data).
200    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; // chain shorter than the size field claims → stop, no panic
213            };
214            let Some(base) = self.geom.cluster_offset(cluster) else {
215                break; // cov:unreachable: chain clusters are >= 2 by construction
216            };
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; // truncated image
223            }
224            written += got;
225            pos += got as u64;
226        }
227        Ok(written)
228    }
229
230    // ---- internals -------------------------------------------------------
231
232    /// Parse a directory's raw bytes into nodes, dispatching by variant.
233    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    /// Where the directory that `id` *is* stores its bytes.
248    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    /// Whether the root directory lives in the cluster heap (FAT32/exFAT) rather
277    /// than a fixed region (FAT12/16).
278    fn root_is_clustered(&self) -> bool {
279        matches!(self.geom.variant, FatVariant::Fat32 | FatVariant::ExFat)
280    }
281
282    /// Re-read the directory entry an [`FileId::Entry`] points at. The parent is
283    /// read by following its FAT chain (correct for FAT and for single-cluster /
284    /// chained exFAT directories).
285    fn resolve_entry(&self, id: FileId) -> Result<Node> {
286        let FileId::Entry { dir_cluster, index } = id else {
287            return Ok(root_node()); // cov:unreachable: callers pass Entry only
288        };
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    /// The cluster chain and total byte length backing `id`'s data.
306    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    /// The cluster list and total byte length for `(first_cluster, size,
324    /// contiguous)`. A `0` size (FAT directories) is taken as the full chain.
325    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    /// Resolve the cluster list. A contiguous (exFAT NoFatChain) extent is a
336    /// sequential run sized from `size`; otherwise the FAT chain is followed.
337    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    /// Read the raw bytes of a directory.
360    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; // cov:unreachable: a real directory chain is far under 64 MiB
377            }
378            let Some(base) = self.geom.cluster_offset(cluster) else {
379                break; // cov:unreachable: chain clusters are >= 2
380            };
381            out.extend(self.read_region_vec(base, cluster_size)?);
382        }
383        Ok(out)
384    }
385
386    /// Read up to `buf.len()` bytes at `offset`, returning the count read.
387    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    /// Read `len` bytes at `offset` into a fresh vector (short read tolerated).
399    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
407/// The marker `dir_cluster` for children enumerated under `source`.
408fn source_marker(source: DirSource) -> u32 {
409    match source {
410        DirSource::FixedRoot => FIXED_ROOT,
411        DirSource::Chain { first_cluster, .. } => first_cluster,
412    }
413}
414
415/// The synthetic root node.
416fn 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
434/// Build a [`Node`] from a decoded FAT directory entry under parent `marker`.
435fn 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
456/// Build a [`Node`] from a decoded exFAT directory entry set under `marker`.
457fn 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
478/// Seek to `offset` and read exactly `buf.len()` bytes, failing loud on EOF.
479fn 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
488/// Seek to `offset` and read up to `len` bytes (short read tolerated).
489fn 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
499/// Fill `buf` as far as the reader allows, returning the byte count (a short
500/// read at EOF is not an error — a truncated image degrades, never panics).
501fn 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            // cov:unreachable: EINTR retry — in-memory/file readers never raise it
508            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/// Build a small but structurally-valid FAT32 image: 512 B sectors, 32
516/// reserved, 2 FATs of 512 sectors, root at cluster 2 with a handful of files
517/// (`TEST`, `BIG` 2-cluster, `TRUNC`, `EOF`, `ZERO`, `HALF`, a deleted `GONE`).
518/// The claimed volume size yields > 65525 clusters (→ FAT32) while only the
519/// used clusters are physically backed. Shared by the fs and vfs test modules.
520#[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; // cluster 2
528        let mut img = vec![0u8; data_start + 8 * bps]; // clusters 2..=9 backed
529
530        // Boot sector / BPB.
531        img[0] = 0xEB;
532        img[2] = 0x90;
533        img[11..13].copy_from_slice(&512u16.to_le_bytes());
534        img[13] = 1; // sectors/cluster
535        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()); // total sectors 32
538        img[36..40].copy_from_slice(&(fat_sectors as u32).to_le_bytes());
539        img[44..48].copy_from_slice(&2u32.to_le_bytes()); // root cluster
540        img[510] = 0x55;
541        img[511] = 0xAA;
542
543        // FAT1 (cluster N entry at byte offset N*4). EOC = 0x0FFFFFFF.
544        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); // root
550        put(&mut img, 3, eoc); // TEST.TXT
551        put(&mut img, 4, 5u32.to_le_bytes()); // BIG.TXT: 4 -> 5
552        put(&mut img, 5, eoc);
553        put(&mut img, 6, eoc); // TRUNC.TXT (claims more than its chain)
554        put(&mut img, 7, 8u32.to_le_bytes()); // HALF.TXT: 7 -> 8 (chain longer than size)
555        put(&mut img, 8, eoc);
556        put(&mut img, 10, eoc); // EOF.TXT (cluster beyond the image)
557
558        // Root directory (cluster 2): four short entries.
559        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)); // empty
573        img[root + 160..root + 192].copy_from_slice(&entry(b"HALF    TXT", 7, 512)); // 512 B, 2-cluster chain
574        let mut del = entry(b"GONE    TXT", 3, 9);
575        del[0] = 0xE5; // deleted
576        img[root + 192..root + 224].copy_from_slice(&del);
577
578        // File data.
579        img[data_start + bps..data_start + bps + 9].copy_from_slice(b"hi fat32\n"); // cluster 3
580        for i in 0..600 {
581            img[data_start + 2 * bps + i] = (i % 251) as u8; // BIG.TXT spans clusters 4,5
582        }
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        // Two physically-adjacent clusters (4,5) merge into one run.
643        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        // size claims 2000 B (4 clusters) but the chain is one cluster.
663        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        // cluster 10 lies beyond the backing image → 0 bytes, no panic.
672        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        // HALF.TXT: 512-byte size but a two-cluster chain (7 -> 8).
694        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        // data_extent(Root) on a clustered (FAT32) root.
703        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        // A reader that serves the boot + FAT (offsets < data_start) but fails
711        // on data reads, so read_fill returns a loud I/O error.
712        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(); // boot + FAT are before data_start
745        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}