Skip to main content

hadris_fat/
fs.rs

1io_transform! {
2
3use core::{cell::Cell, fmt};
4
5use spin::Mutex;
6
7use hadris_common::types::endian::Endian;
8use hadris_path::{Component, VPath};
9
10use crate::error::{Error, Result};
11use crate::raw::{RawBpb, RawBpbExt16, RawBpbExt32, RawFsInfo};
12use super::dir::{FatDir, FileEntry};
13use super::fat_table::{Fat, Fat12, Fat16, Fat32, FatType};
14use super::io::{Cluster, ClusterLike, Read, ReadExt, Sector, SectorCursor, SectorLike, Seek, SeekFrom};
15use super::read::FileReader;
16
17/// Volume metadata from the boot sector.
18///
19/// This struct contains information about the volume such as the OEM name,
20/// volume serial number, volume label, and filesystem type string.
21#[derive(Debug, Clone)]
22pub struct VolumeInfo {
23    /// OEM name (8 bytes, space-padded)
24    oem_name: [u8; 8],
25    /// Volume serial number (4 bytes)
26    volume_id: u32,
27    /// Volume label (11 bytes, space-padded)
28    volume_label: [u8; 11],
29    /// Filesystem type string (8 bytes, space-padded)
30    fs_type_str: [u8; 8],
31}
32
33impl VolumeInfo {
34    /// Get the OEM name as a trimmed string.
35    pub fn oem_name(&self) -> &str {
36        core::str::from_utf8(&self.oem_name)
37            .unwrap_or("")
38            .trim_end()
39    }
40
41    /// Get the volume serial number.
42    pub fn volume_id(&self) -> u32 {
43        self.volume_id
44    }
45
46    /// Get the volume label as a trimmed string.
47    pub fn volume_label(&self) -> &str {
48        core::str::from_utf8(&self.volume_label)
49            .unwrap_or("")
50            .trim_end()
51    }
52
53    /// Get the filesystem type string as a trimmed string.
54    ///
55    /// Note: This is informational only and should not be used to determine
56    /// the actual FAT type. Use [`FatVolume::fat_type()`] instead.
57    pub fn fs_type_str(&self) -> &str {
58        core::str::from_utf8(&self.fs_type_str)
59            .unwrap_or("")
60            .trim_end()
61    }
62
63    /// Get the raw OEM name bytes.
64    pub fn oem_name_raw(&self) -> &[u8; 8] {
65        &self.oem_name
66    }
67
68    /// Get the raw volume label bytes.
69    pub fn volume_label_raw(&self) -> &[u8; 11] {
70        &self.volume_label
71    }
72
73    /// Get the raw filesystem type string bytes.
74    pub fn fs_type_str_raw(&self) -> &[u8; 8] {
75        &self.fs_type_str
76    }
77}
78
79#[derive(Debug)]
80pub(crate) struct FatInfo {
81    #[cfg(feature = "alloc")]
82    pub(crate) cluster_size: usize,
83    pub(crate) data_start: usize,
84    #[cfg(feature = "alloc")]
85    pub(crate) max_cluster: u32,
86}
87
88/// Extension info for FAT12/16 filesystems (fixed root directory)
89#[derive(Debug)]
90pub(crate) struct Fat12_16FsExt {
91    /// Root directory start byte offset
92    root_dir_start: usize,
93    /// Root directory size in bytes
94    root_dir_size: usize,
95}
96
97#[derive(Debug)]
98pub(crate) enum FatFsExt {
99    Fat12_16(Fat12_16FsExt),
100    Fat32(Fat32FsExt),
101}
102
103impl FatFsExt {
104    /// Get fixed root directory info for FAT12/16
105    #[cfg(feature = "write")]
106    fn fixed_root_dir(&self) -> Option<(usize, usize)> {
107        match self {
108            Self::Fat12_16(ext) => Some((ext.root_dir_start, ext.root_dir_size)),
109            Self::Fat32(_) => None,
110        }
111    }
112}
113
114/// Extension info for FAT32 filesystems.
115///
116/// Uses `Cell` for `free_count` and `next_free` to allow updating the FSInfo
117/// sector without requiring mutable access to the entire FatVolume.
118pub(crate) struct Fat32FsExt {
119    /// Sector number of the FSInfo structure
120    pub(crate) fs_info_sec: Sector<u16>,
121    /// Root directory cluster
122    root_clus: Cluster<u32>,
123    /// Number of free clusters (from FSInfo, may be stale)
124    pub(crate) free_count: Cell<u32>,
125    /// Hint for next free cluster (from FSInfo)
126    pub(crate) next_free: Cell<Cluster<u32>>,
127}
128
129impl fmt::Debug for Fat32FsExt {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        f.debug_struct("Fat32FsExt")
132            .field("fs_info_sec", &self.fs_info_sec)
133            .field("root_clus", &self.root_clus)
134            .field("free_count", &self.free_count.get())
135            .field("next_free", &self.next_free.get())
136            .finish()
137    }
138}
139
140/// A mounted FAT filesystem backed by a seekable data source.
141pub struct FatVolume<DATA: Seek> {
142    pub(crate) data: Mutex<SectorCursor<DATA>>,
143    pub(crate) info: FatInfo,
144    pub(crate) fat: Fat,
145    pub(crate) ext: FatFsExt,
146    volume_info: VolumeInfo,
147    /// Clock used to stamp newly-created or modified directory entries.
148    /// Defaults to [`crate::time::DEFAULT_TIME_PROVIDER`].
149    time_provider: &'static dyn crate::time::TimeProvider,
150    /// Codepage converter used for short-name encoding/decoding.
151    /// Defaults to [`crate::oem::DEFAULT_OEM_CONVERTER`].
152    oem_converter: &'static dyn crate::oem::OemCpConverter,
153    /// Optional FAT-sector LRU cache. Installed by the builder via
154    /// [`FatVolumeBuilder::fat_cache`]; `None` means uncached behaviour
155    /// identical to pre-cache versions of this crate. The cache itself
156    /// is sync-only and lives behind a [`spin::Mutex`] so it can be
157    /// shared between read paths and write paths.
158    #[cfg(feature = "cache")]
159    pub(crate) fat_cache: Option<Mutex<crate::cache::FatSectorCache>>,
160}
161
162impl<DATA: Seek> FatVolume<DATA> {
163    /// Consumes the filesystem handle and returns its underlying data source.
164    pub fn into_inner(self) -> DATA {
165        self.data.into_inner().data
166    }
167}
168
169impl<DATA: Seek> fmt::Debug for FatVolume<DATA> {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.debug_struct("FatVolume")
172            .field("info", &self.info)
173            .field("ext", &self.ext)
174            .field("time_provider", &self.time_provider)
175            .field("oem_converter", &self.oem_converter)
176            .finish_non_exhaustive()
177    }
178}
179
180/// Builder for [`FatVolume`] that lets callers install custom providers (clock,
181/// codepage) before mounting.
182///
183/// Construct via [`FatVolume::builder`]. Call [`open`](Self::open) once configured.
184/// Without any with_* calls, [`open`](Self::open) behaves identically to
185/// [`FatVolume::open`].
186pub struct FatVolumeBuilder<DATA: Read + Seek> {
187    data: DATA,
188    time_provider: &'static dyn crate::time::TimeProvider,
189    oem_converter: &'static dyn crate::oem::OemCpConverter,
190    /// FAT-cache capacity in sectors, if requested. `None` means no cache.
191    #[cfg(feature = "cache")]
192    fat_cache_capacity: Option<usize>,
193}
194
195impl<DATA: Read + Seek> FatVolumeBuilder<DATA> {
196    /// Start a new builder with default providers.
197    pub fn new(data: DATA) -> Self {
198        Self {
199            data,
200            time_provider: &crate::time::DEFAULT_TIME_PROVIDER,
201            oem_converter: &crate::oem::DEFAULT_OEM_CONVERTER,
202            #[cfg(feature = "cache")]
203            fat_cache_capacity: None,
204        }
205    }
206
207    /// Override the clock used for directory-entry timestamps.
208    pub fn time_provider(
209        mut self,
210        provider: &'static dyn crate::time::TimeProvider,
211    ) -> Self {
212        self.time_provider = provider;
213        self
214    }
215
216    /// Override the codepage converter used for short (8.3) filenames.
217    pub fn oem_converter(
218        mut self,
219        converter: &'static dyn crate::oem::OemCpConverter,
220    ) -> Self {
221        self.oem_converter = converter;
222        self
223    }
224
225    /// Install an LRU FAT-sector cache backing read and write operations.
226    ///
227    /// `capacity_sectors` caps how many FAT sectors the cache holds in
228    /// memory at once. Use [`crate::cache::DEFAULT_CACHE_CAPACITY`] (16) as
229    /// a sensible starting point. The cache is sync-only — it's silently
230    /// not consulted when the filesystem is driven through the async API.
231    ///
232    /// `capacity_sectors == 0` is treated as "no cache" — the call returns
233    /// the builder unchanged rather than installing a degenerate
234    /// zero-capacity cache that would refuse every insert.
235    ///
236    /// Without this call, `FatVolume` performs a seek + read on the underlying
237    /// data source for every FAT entry access (today's behaviour).
238    #[cfg(feature = "cache")]
239    pub fn fat_cache(mut self, capacity_sectors: usize) -> Self {
240        if capacity_sectors == 0 {
241            self.fat_cache_capacity = None;
242        } else {
243            self.fat_cache_capacity = Some(capacity_sectors);
244        }
245        self
246    }
247
248    /// Mount the filesystem with the configured providers.
249    pub async fn open(self) -> Result<FatVolume<DATA>> {
250        #[cfg(feature = "cache")]
251        let cap = self.fat_cache_capacity;
252        #[cfg(not(feature = "cache"))]
253        let fs = FatVolume::open_with_providers(self.data, self.time_provider, self.oem_converter).await?;
254        #[cfg(feature = "cache")]
255        let mut fs = FatVolume::open_with_providers(self.data, self.time_provider, self.oem_converter).await?;
256        #[cfg(feature = "cache")]
257        if let Some(capacity) = cap {
258            // Build the cache once we know the FAT layout from the boot sector.
259            let (fat_start, fat_size, fat_count, sector_size) = {
260                let data = fs.data.lock();
261                let sector_size = data.sector_size;
262                let (start, size, count) = match &fs.fat {
263                    Fat::Fat12(f) => f.cache_layout(),
264                    Fat::Fat16(f) => f.cache_layout(),
265                    Fat::Fat32(f) => f.cache_layout(),
266                };
267                (start, size, count, sector_size)
268            };
269            let cache = crate::cache::FatSectorCache::new(
270                fat_start, fat_size, fat_count, sector_size, capacity,
271            );
272            fs.fat_cache = Some(Mutex::new(cache));
273        }
274        Ok(fs)
275    }
276}
277
278/// FAT-resident volume status flags read from `FAT[1]`.
279///
280/// The FAT spec dedicates two high bits of the cluster-1 entry to volume
281/// hygiene: one for "clean shutdown" and one for "no I/O errors during last
282/// mount". Both bits are *cleared* when something is wrong. This struct
283/// inverts that polarity so a `true` value always means trouble.
284///
285/// FAT12 has no spare bits in its packed 12-bit entries; the spec doesn't
286/// define status flags for FAT12, so a FAT12 mount always reports
287/// `dirty: false, io_errors: false`.
288#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
289#[cfg_attr(feature = "defmt", derive(defmt::Format))]
290pub struct FsStatusFlags {
291    /// `true` if the volume was not unmounted cleanly last time.
292    pub dirty: bool,
293    /// `true` if I/O errors were reported during the last mount.
294    pub io_errors: bool,
295}
296
297/// FSInfo signature constants
298pub(crate) const FSINFO_LEAD_SIG: u32 = 0x41615252; // "RRaA"
299pub(crate) const FSINFO_STRUC_SIG: u32 = 0x61417272; // "rrAa"
300pub(crate) const FSINFO_TRAIL_SIG: u32 = 0xAA550000;
301
302/// Implementations for Read APIs
303impl<DATA> FatVolume<DATA>
304where
305    DATA: Read + Seek,
306{
307    /// Open a FAT filesystem from a data source with default providers.
308    ///
309    /// Automatically detects FAT12, FAT16, or FAT32 based on the BPB fields.
310    /// Uses [`crate::time::DEFAULT_TIME_PROVIDER`] and
311    /// [`crate::oem::DEFAULT_OEM_CONVERTER`]; for custom providers use
312    /// [`FatVolume::builder`].
313    pub async fn open(data: DATA) -> Result<Self> {
314        Self::open_with_providers(
315            data,
316            &crate::time::DEFAULT_TIME_PROVIDER,
317            &crate::oem::DEFAULT_OEM_CONVERTER,
318        )
319        .await
320    }
321
322    /// Start a [`FatVolumeBuilder`] for advanced configuration (custom clock,
323    /// codepage, etc.).
324    pub fn builder(data: DATA) -> FatVolumeBuilder<DATA> {
325        FatVolumeBuilder::new(data)
326    }
327
328    /// Internal entry point shared by [`open`](Self::open) and
329    /// [`FatVolumeBuilder::open`].
330    pub(crate) async fn open_with_providers(
331        mut data: DATA,
332        time_provider: &'static dyn crate::time::TimeProvider,
333        oem_converter: &'static dyn crate::oem::OemCpConverter,
334    ) -> Result<Self> {
335        // Boot sector is a trust boundary — wrap I/O failures so a truncated
336        // or unreadable image surfaces "boot sector" instead of an opaque
337        // `Io(...)` and the user knows where to look.
338        let bpb = data
339            .read_struct::<RawBpb>()
340            .await
341            .map_err(|source| Error::IoContext {
342                op: "boot sector",
343                sector: Some(0),
344                source: source.erase(),
345            })?;
346        let sector_size = bpb.bytes_per_sector.get() as usize;
347        if !matches!(sector_size, 512 | 1024 | 2048 | 4096) {
348            return Err(Error::CorruptFilesystem {
349                context: "BPB bytes_per_sector must be 512, 1024, 2048, or 4096",
350            });
351        }
352        if !bpb.sectors_per_cluster.is_power_of_two() || bpb.sectors_per_cluster > 128 {
353            return Err(Error::CorruptFilesystem {
354                context: "BPB sectors_per_cluster must be a power of two from 1 through 128",
355            });
356        }
357        let cluster_size = (bpb.sectors_per_cluster as usize) * sector_size;
358        if cluster_size > 32 * 1024 {
359            return Err(Error::CorruptFilesystem {
360                context: "BPB cluster size must not exceed 32 KiB",
361            });
362        }
363        let data = SectorCursor::new(data, sector_size, cluster_size);
364
365        // Determine FAT type by checking root_entry_count and sectors_per_fat_16
366        // FAT32 has root_entry_count = 0 and sectors_per_fat_16 = 0
367        let root_entry_count = u16::from_le_bytes(bpb.root_entry_count);
368        let sectors_per_fat_16 = u16::from_le_bytes(bpb.sectors_per_fat_16);
369
370        if root_entry_count == 0 && sectors_per_fat_16 == 0 {
371            // FAT32
372            Self::open_fat32(data, bpb, time_provider, oem_converter).await
373        } else {
374            // FAT12 or FAT16
375            Self::open_fat12_16(data, bpb, time_provider, oem_converter).await
376        }
377    }
378
379    /// Open a FAT12/16 filesystem.
380    async fn open_fat12_16(
381        mut data: SectorCursor<DATA>,
382        bpb: RawBpb,
383        time_provider: &'static dyn crate::time::TimeProvider,
384        oem_converter: &'static dyn crate::oem::OemCpConverter,
385    ) -> Result<Self> {
386        // Read FAT12/16 extended boot sector
387        let bpb_ext16 = data
388            .read_struct::<RawBpbExt16>()
389            .await
390            .map_err(|source| Error::IoContext {
391                op: "boot sector (FAT12/16 extended fields)",
392                sector: Some(0),
393                source: source.erase(),
394            })?;
395
396        // Validate boot signature
397        let signature = u16::from_le_bytes(bpb_ext16.signature_word);
398        if signature != 0xAA55 {
399            return Err(Error::InvalidBootSignature { found: signature });
400        }
401
402        // FAT requires 1 or 2 file allocation tables (BPB_NumFATs). A corrupt
403        // count trips a debug_assert deep in the FAT constructors and, in
404        // release builds where the assert is stripped, silently corrupts
405        // FAT-copy math — reject it here (after the signature check so a
406        // non-FAT sector still surfaces InvalidBootSignature first).
407        if bpb.fat_count != 1 && bpb.fat_count != 2 {
408            return Err(Error::CorruptFilesystem {
409                context: "BPB fat_count must be 1 or 2",
410            });
411        }
412
413        let sector_size = data.sector_size;
414        #[cfg(feature = "alloc")]
415        let cluster_size = data.cluster_size;
416        let reserved_sectors = bpb.reserved_sector_count.get() as usize;
417        let fat_count = bpb.fat_count as usize;
418        let root_entry_count = u16::from_le_bytes(bpb.root_entry_count);
419        let sectors_per_fat = u16::from_le_bytes(bpb.sectors_per_fat_16) as usize;
420
421        // Calculate root directory location with checked arithmetic — the
422        // BPB fields are untrusted and a corrupt image with absurd values
423        // (e.g. sectors_per_fat = 0xFFFF) could otherwise wrap usize on
424        // 32-bit targets and seek to garbage.
425        let fat_start = reserved_sectors
426            .checked_mul(sector_size)
427            .ok_or(Error::CorruptFilesystem {
428                context: "reserved_sectors * sector_size",
429            })?;
430        let fat_total_size = fat_count
431            .checked_mul(sectors_per_fat)
432            .and_then(|v| v.checked_mul(sector_size))
433            .ok_or(Error::CorruptFilesystem {
434                context: "fat_count * sectors_per_fat * sector_size",
435            })?;
436        let root_dir_start = fat_start
437            .checked_add(fat_total_size)
438            .ok_or(Error::CorruptFilesystem {
439                context: "fat_start + fat_total_size",
440            })?;
441        let root_dir_size = (root_entry_count as usize) * 32;
442        let root_dir_sectors = root_dir_size.div_ceil(sector_size);
443
444        // Calculate data area start
445        let data_start = root_dir_start
446            .checked_add(root_dir_sectors * sector_size)
447            .ok_or(Error::CorruptFilesystem {
448                context: "data_start arithmetic",
449            })?;
450
451        // Calculate total data sectors and cluster count
452        let total_sectors = if bpb.total_sectors_16 != [0, 0] {
453            u16::from_le_bytes(bpb.total_sectors_16) as u32
454        } else {
455            u32::from_le_bytes(bpb.total_sectors_32)
456        };
457        // Saturating subtraction: a corrupt total_sectors smaller than the
458        // metadata region size produces 0 data sectors rather than wrapping
459        // usize to a huge number.
460        let metadata_sectors = reserved_sectors
461            .checked_add(fat_count.checked_mul(sectors_per_fat).ok_or(
462                Error::CorruptFilesystem {
463                    context: "fat_count * sectors_per_fat",
464                },
465            )?)
466            .and_then(|v| v.checked_add(root_dir_sectors))
467            .ok_or(Error::CorruptFilesystem {
468                context: "metadata sector total",
469            })?;
470        let data_sectors = (total_sectors as usize).saturating_sub(metadata_sectors);
471        let count_of_clusters = data_sectors / (bpb.sectors_per_cluster as usize);
472
473        // Determine FAT12 vs FAT16 based on cluster count (per Microsoft spec)
474        let (fat, max_cluster) = if count_of_clusters < 4085 {
475            // FAT12
476            let fat12 = Fat12::new(
477                fat_start,
478                sectors_per_fat * sector_size,
479                fat_count,
480                (count_of_clusters + 1) as u16, // +1 because valid clusters are 2..=max
481            );
482            (Fat::Fat12(fat12), count_of_clusters as u32 + 1)
483        } else {
484            // FAT16
485            let fat16 = Fat16::new(
486                fat_start,
487                sectors_per_fat * sector_size,
488                fat_count,
489                (count_of_clusters + 1) as u16,
490            );
491            (Fat::Fat16(fat16), count_of_clusters as u32 + 1)
492        };
493        #[cfg(not(feature = "alloc"))]
494        let _ = max_cluster;
495
496        let ext = FatFsExt::Fat12_16(Fat12_16FsExt {
497            root_dir_start,
498            root_dir_size,
499        });
500
501        let info = FatInfo {
502            #[cfg(feature = "alloc")]
503            cluster_size,
504            data_start,
505            #[cfg(feature = "alloc")]
506            max_cluster,
507        };
508
509        // Extract volume info from BPB
510        let volume_info = VolumeInfo {
511            oem_name: bpb.oem_name,
512            volume_id: u32::from_le_bytes(bpb_ext16.volume_id),
513            volume_label: bpb_ext16.volume_label,
514            fs_type_str: bpb_ext16.fs_type,
515        };
516
517        Ok(Self {
518            data: Mutex::new(data),
519            info,
520            fat,
521            ext,
522            volume_info,
523            time_provider,
524            oem_converter,
525            #[cfg(feature = "cache")]
526            fat_cache: None,
527        })
528    }
529
530    /// Open a FAT32 filesystem.
531    async fn open_fat32(
532        mut data: SectorCursor<DATA>,
533        bpb: RawBpb,
534        time_provider: &'static dyn crate::time::TimeProvider,
535        oem_converter: &'static dyn crate::oem::OemCpConverter,
536    ) -> Result<Self> {
537        let bpb_ext32 = data
538            .read_struct::<RawBpbExt32>()
539            .await
540            .map_err(|source| Error::IoContext {
541                op: "boot sector (FAT32 extended fields)",
542                sector: Some(0),
543                source: source.erase(),
544            })?;
545
546        // Validate boot signature
547        let signature = bpb_ext32.signature_word.get();
548        if signature != 0xAA55 {
549            return Err(Error::InvalidBootSignature { found: signature });
550        }
551        if bpb_ext32.version != [0, 0] {
552            return Err(Error::CorruptFilesystem {
553                context: "unsupported FAT32 filesystem version",
554            });
555        }
556
557        // FAT requires 1 or 2 file allocation tables (BPB_NumFATs) — see the
558        // FAT12/16 path. Reject a corrupt count before it reaches Fat32::new's
559        // debug_assert (and before it skews FAT-copy math in release).
560        if bpb.fat_count != 1 && bpb.fat_count != 2 {
561            return Err(Error::CorruptFilesystem {
562                context: "BPB fat_count must be 1 or 2",
563            });
564        }
565
566        // Read and validate FSInfo
567        let fs_info_sec = Sector(bpb_ext32.fs_info_sector.get());
568        data.seek_sector(fs_info_sec).await?;
569        let fs_info = data
570            .read_struct::<RawFsInfo>()
571            .await
572            .map_err(|source| Error::IoContext {
573                op: "FSInfo",
574                sector: Some(fs_info_sec.0 as u64),
575                source: source.erase(),
576            })?;
577
578        // Validate FSInfo signatures
579        let lead_sig = u32::from_le_bytes(fs_info.signature);
580        if lead_sig != FSINFO_LEAD_SIG {
581            return Err(Error::InvalidFsInfoSignature {
582                field: "FSI_LeadSig",
583                expected: FSINFO_LEAD_SIG,
584                found: lead_sig,
585            });
586        }
587
588        let struc_sig = u32::from_le_bytes(fs_info.structure_signature);
589        if struc_sig != FSINFO_STRUC_SIG {
590            return Err(Error::InvalidFsInfoSignature {
591                field: "FSI_StrucSig",
592                expected: FSINFO_STRUC_SIG,
593                found: struc_sig,
594            });
595        }
596
597        let trail_sig = fs_info.trail_signature.get();
598        if trail_sig != FSINFO_TRAIL_SIG {
599            return Err(Error::InvalidFsInfoSignature {
600                field: "FSI_TrailSig",
601                expected: FSINFO_TRAIL_SIG,
602                found: trail_sig,
603            });
604        }
605
606        let ext = FatFsExt::Fat32(Fat32FsExt {
607            fs_info_sec,
608            root_clus: Cluster(bpb_ext32.root_cluster.get()),
609            free_count: Cell::new(fs_info.free_count.get()),
610            next_free: Cell::new(Cluster(fs_info.next_free.get())),
611        });
612
613        #[cfg(feature = "alloc")]
614        let cluster_size = data.cluster_size;
615        // sectors_per_fat_32 is a 32-bit BPB field (the FAT12/16 field is only
616        // 16-bit), so these products overflow u32 — and usize on 32-bit
617        // targets — on corrupt images. Keep the geometry math checked.
618        let fat_start = Sector(bpb.reserved_sector_count.get()).to_bytes(data.sector_size);
619        let fat_size_per_fat = (bpb_ext32.sectors_per_fat_32.get() as usize)
620            .checked_mul(data.sector_size)
621            .ok_or(Error::CorruptFilesystem {
622                context: "sectors_per_fat_32 * sector_size",
623            })?;
624        let fat_size = (bpb.fat_count as usize)
625            .checked_mul(fat_size_per_fat)
626            .ok_or(Error::CorruptFilesystem {
627                context: "fat_count * sectors_per_fat_32",
628            })?;
629
630        // Calculate total data sectors and max cluster
631        let total_sectors = if bpb.total_sectors_16 != [0, 0] {
632            u16::from_le_bytes(bpb.total_sectors_16) as u32
633        } else {
634            u32::from_le_bytes(bpb.total_sectors_32)
635        };
636        // u64: fat_count * sectors_per_fat_32 alone can exceed u32.
637        let metadata_sectors = bpb.reserved_sector_count.get() as u64
638            + bpb_ext32.sectors_per_fat_32.get() as u64 * bpb.fat_count as u64;
639        let data_sectors = (total_sectors as u64).saturating_sub(metadata_sectors);
640        let max_cluster =
641            (data_sectors / bpb.sectors_per_cluster as u64).min(u32::MAX as u64 - 1) as u32 + 1; // +1 because clusters start at 2
642
643        // The FAT32 root directory is an ordinary cluster chain, so its first
644        // cluster must be a valid data cluster; anything else would underflow
645        // the cluster-to-offset math when the root is iterated.
646        let root_cluster = bpb_ext32.root_cluster.get();
647        if !(2..=max_cluster).contains(&root_cluster) {
648            return Err(Error::ClusterOutOfBounds {
649                cluster: root_cluster,
650                max: max_cluster,
651            });
652        }
653
654        let fat = Fat::Fat32(Fat32::new(
655            fat_start,
656            fat_size_per_fat,
657            bpb.fat_count as usize,
658            max_cluster,
659        ));
660
661        let data_start = fat_start
662            .checked_add(fat_size)
663            .ok_or(Error::CorruptFilesystem {
664                context: "fat_start + fat_size",
665            })?;
666
667        let info = FatInfo {
668            #[cfg(feature = "alloc")]
669            cluster_size,
670            data_start,
671            #[cfg(feature = "alloc")]
672            max_cluster,
673        };
674
675        // Extract volume info from BPB
676        let volume_info = VolumeInfo {
677            oem_name: bpb.oem_name,
678            volume_id: u32::from_le_bytes(bpb_ext32.volume_id),
679            volume_label: bpb_ext32.volume_label,
680            fs_type_str: bpb_ext32.fs_type,
681        };
682
683        Ok(Self {
684            data: Mutex::new(data),
685            info,
686            fat,
687            ext,
688            volume_info,
689            time_provider,
690            oem_converter,
691            #[cfg(feature = "cache")]
692            fat_cache: None,
693        })
694    }
695
696    /// Borrow the configured clock used for new directory-entry timestamps.
697    pub fn time_provider(&self) -> &dyn crate::time::TimeProvider {
698        self.time_provider
699    }
700
701    /// Borrow the configured OEM codepage converter for short (8.3) names.
702    pub fn oem_converter(&self) -> &dyn crate::oem::OemCpConverter {
703        self.oem_converter
704    }
705
706    /// Borrow the FAT table descriptor.
707    ///
708    /// Required when constructing a `CachedFat` (with the `cache` feature) via
709    /// `CachedFat::new`, which needs the FAT type and
710    /// max-cluster bound. Otherwise rarely needed by callers — most FAT
711    /// operations go through [`FatVolume`] methods directly.
712    pub fn fat(&self) -> &Fat {
713        &self.fat
714    }
715
716    /// Returns the filesystem's root directory.
717    pub fn root_dir(&self) -> FatDir<'_, DATA> {
718        match &self.ext {
719            FatFsExt::Fat12_16(ext) => FatDir {
720                data: self,
721                cluster: Cluster(0), // Sentinel for fixed root directory
722                fixed_root: Some((ext.root_dir_start, ext.root_dir_size)),
723            },
724            FatFsExt::Fat32(ext) => FatDir {
725                data: self,
726                cluster: Cluster(ext.root_clus.0 as usize),
727                fixed_root: None,
728            },
729        }
730    }
731
732    /// Get the FAT type of this filesystem
733    pub fn fat_type(&self) -> FatType {
734        self.fat.fat_type()
735    }
736
737    /// Get volume metadata from the boot sector.
738    ///
739    /// This includes the OEM name, volume serial number, volume label,
740    /// and filesystem type string.
741    pub fn volume_info(&self) -> &VolumeInfo {
742        &self.volume_info
743    }
744
745    /// Get the fixed root directory info for FAT12/16 filesystems.
746    ///
747    /// Returns `Some((start_offset, size))` for FAT12/16, `None` for FAT32.
748    #[cfg(feature = "write")]
749    pub(crate) fn fixed_root_dir_info(&self) -> Option<(usize, usize)> {
750        self.ext.fixed_root_dir()
751    }
752
753    /// Returns true iff `cluster` is the FAT32 root directory cluster.
754    ///
755    /// Used by directory-creation code to honor the FAT32 spec rule that a
756    /// subdirectory's ".." entry must store cluster 0 (not the real root
757    /// cluster) when its parent is the FAT32 root.
758    #[cfg(feature = "write")]
759    pub(crate) fn is_fat32_root_cluster(&self, cluster: u32) -> bool {
760        matches!(&self.ext, FatFsExt::Fat32(ext) if ext.root_clus.0 == cluster)
761    }
762
763    /// Read the FAT-resident volume status flags from `FAT[1]`.
764    ///
765    /// `dirty` means the volume was not unmounted cleanly; `io_errors` means
766    /// the previous host saw I/O failures. FAT12 has no status bits, so the
767    /// returned flags are always `false` for FAT12 — check
768    /// [`Self::fat_type`] if that distinction matters to your caller.
769    pub async fn read_status_flags(&self) -> Result<FsStatusFlags> {
770        let (dirty, io_errors) = self.read_status_flags_routed().await?;
771        Ok(FsStatusFlags { dirty, io_errors })
772    }
773
774    /// Read the volume label from the root directory entry, if present.
775    ///
776    /// Two volume labels live on a FAT volume: one in the BPB (boot sector,
777    /// always present, available via [`Self::volume_info`]) and an optional
778    /// directory entry in the root with the `VOLUME_ID` attribute. Windows
779    /// updates the latter when a user renames the volume; the BPB copy can
780    /// drift. Use this method to read the authoritative on-disk name.
781    ///
782    /// Returns `Ok(None)` if no label entry exists.
783    pub async fn read_root_label(&self) -> Result<Option<[u8; 11]>> {
784        match self.find_root_label_entry().await? {
785            Some((_, raw)) => Ok(Some(unsafe { raw.file }.name)),
786            None => Ok(None),
787        }
788    }
789
790    /// Locate the first non-deleted, non-LFN root entry whose attribute set
791    /// is exactly `VOLUME_ID` (i.e. a real volume-label entry, not a stray
792    /// LFN component which has every bit in `LONG_NAME` set).
793    ///
794    /// Returns `Ok(Some((byte_pos, raw_entry)))` if found; `Ok(None)` if the
795    /// root iterates to its terminator without a label entry.
796    pub(crate) async fn find_root_label_entry(
797        &self,
798    ) -> Result<Option<(usize, crate::raw::RawDirectoryEntry)>> {
799        use crate::raw::{DirEntryAttrFlags, RawDirectoryEntry};
800        let entry_size = core::mem::size_of::<RawDirectoryEntry>();
801        let mut data = self.data.lock();
802
803        let is_label =
804            |attr: u8| DirEntryAttrFlags::from_bits_retain(attr).is_volume_label_entry();
805
806        match &self.ext {
807            FatFsExt::Fat12_16(ext) => {
808                let end = ext.root_dir_start + ext.root_dir_size;
809                let mut pos = ext.root_dir_start;
810                while pos + entry_size <= end {
811                    data.seek(SeekFrom::Start(pos as u64)).await?;
812                    let raw = data.read_struct::<RawDirectoryEntry>().await?;
813                    let bytes = unsafe { raw.bytes };
814                    if bytes[0] == 0 {
815                        return Ok(None);
816                    }
817                    if bytes[0] != 0xE5 && is_label(unsafe { raw.file }.attributes) {
818                        return Ok(Some((pos, raw)));
819                    }
820                    pos += entry_size;
821                }
822                Ok(None)
823            }
824            FatFsExt::Fat32(ext) => {
825                let cluster_size = data.cluster_size;
826                let mut current = ext.root_clus.0 as usize;
827                let chain_limit = self.fat.max_cluster();
828                let mut steps: u32 = 0;
829                loop {
830                    steps = steps.saturating_add(1);
831                    if steps > chain_limit {
832                        return Err(Error::ClusterLoop { cluster: current as u32 });
833                    }
834                    let cluster_start = Cluster(current).to_bytes(self.info.data_start, cluster_size);
835                    let mut offset = 0;
836                    while offset + entry_size <= cluster_size {
837                        let pos = cluster_start + offset;
838                        data.seek(SeekFrom::Start(pos as u64)).await?;
839                        let raw = data.read_struct::<RawDirectoryEntry>().await?;
840                        let bytes = unsafe { raw.bytes };
841                        if bytes[0] == 0 {
842                            return Ok(None);
843                        }
844                        if bytes[0] != 0xE5 && is_label(unsafe { raw.file }.attributes) {
845                            return Ok(Some((pos, raw)));
846                        }
847                        offset += entry_size;
848                    }
849                    // Drop the data lock before calling the routed helper —
850                    // it acquires cache+data in canonical order and would
851                    // deadlock if we still held data.
852                    drop(data);
853                    let next_cluster = self.next_cluster_routed(current).await?;
854                    data = self.data.lock();
855                    match next_cluster {
856                        Some(next) => current = next as usize,
857                        None => return Ok(None),
858                    }
859                }
860            }
861        }
862    }
863
864    /// Open a file or directory by path (e.g., "/dir/subdir/file.txt").
865    ///
866    /// Paths can use forward slashes as separators. Leading slashes are optional.
867    /// Empty path components are ignored.
868    pub async fn open_path(&self, path: &str) -> Result<FileEntry> {
869        let mut current_dir = self.root_dir();
870        let mut last_component = None;
871
872        for component in VPath::new(path).components() {
873            let component = match component {
874                Component::Root | Component::Current => continue,
875                Component::Parent => return Err(Error::InvalidPath),
876                Component::Normal(component) => component,
877            };
878            if let Some(prev) = last_component.take() {
879                // Navigate into the previous component as a directory
880                current_dir = current_dir.open_dir(prev).await?;
881            }
882            last_component = Some(component);
883        }
884
885        // Find the final entry
886        let final_name = last_component.ok_or(Error::InvalidPath)?;
887        current_dir.find(final_name).await?.ok_or(Error::EntryNotFound)
888    }
889
890    /// Open a file by path for reading.
891    ///
892    /// This is a convenience method that combines [`open_path`](Self::open_path)
893    /// with opening a file reader.
894    pub async fn open_file_path(&self, path: &str) -> Result<FileReader<'_, DATA>> {
895        let entry = self.open_path(path).await?;
896        FileReader::new(self, &entry)
897    }
898
899    /// Open a directory by path.
900    ///
901    /// This is a convenience method that combines [`open_path`](Self::open_path)
902    /// with validating the entry is a directory.
903    pub async fn open_dir_path(&self, path: &str) -> Result<FatDir<'_, DATA>> {
904        let entry = self.open_path(path).await?;
905        if !entry.is_directory() {
906            return Err(Error::NotADirectory);
907        }
908        // Subdirectories opened by path are never fixed root
909        Ok(FatDir {
910            data: self,
911            cluster: entry.cluster(),
912            fixed_root: None,
913        })
914    }
915
916    /// Open a directory from a file entry.
917    ///
918    /// The entry must be a directory.
919    pub fn open_dir_entry(&self, entry: &FileEntry) -> Result<FatDir<'_, DATA>> {
920        if !entry.is_directory() {
921            return Err(Error::NotADirectory);
922        }
923        Ok(FatDir {
924            data: self,
925            cluster: entry.cluster(),
926            fixed_root: None,
927        })
928    }
929}
930
931} // end io_transform!
932
933// ===========================================================================
934// Sync-only cache accessors
935//
936// These expose the installed FAT-sector cache to callers. They reference the
937// sync-only `crate::cache` types (`FatSectorCache`, `CachedFat`) and call
938// their synchronous I/O methods, so they are emitted only in the sync slice.
939// Under the async API the cache is bypassed (a build with both `async` and
940// `cache` keeps the field but offers no async cache accessors), which is what
941// lets `--features async,cache` (and `--all-features`) compile.
942// ===========================================================================
943
944#[cfg(feature = "cache")]
945sync_only! {
946    impl<DATA> FatVolume<DATA>
947    where
948        DATA: Read + Seek,
949    {
950        /// Borrow the optional FAT-sector cache configured via
951        /// [`FatVolumeBuilder::fat_cache`].
952        ///
953        /// Returns `None` if no cache was installed. Pair with [`Self::fat`]
954        /// and [`crate::cache::CachedFat::new`] to perform cached FAT
955        /// operations, or use the higher-level [`Self::with_cached_fat`]
956        /// helper which holds the cache and disk locks for you.
957        pub fn fat_cache(&self) -> Option<&Mutex<crate::cache::FatSectorCache>> {
958            self.fat_cache.as_ref()
959        }
960
961        /// Run a closure with a [`crate::cache::CachedFat`] view backed by this
962        /// filesystem's installed FAT cache and underlying disk handle.
963        ///
964        /// Returns `None` if no cache was installed via
965        /// [`FatVolumeBuilder::fat_cache`]. Otherwise locks the cache mutex
966        /// and the data mutex for the duration of the closure and returns
967        /// `Some(value)` where `value` is the closure's return.
968        ///
969        /// `FatVolume`'s built-in methods consult the cache
970        /// automatically; this helper remains useful for bulk FAT walks
971        /// (free-cluster scans, multi-chain traversal) where holding the
972        /// cache+disk locks across many entries is cheaper than re-acquiring
973        /// them per call.
974        ///
975        /// # Example
976        ///
977        /// ```rust,no_run
978        /// # #[cfg(all(feature = "cache", feature = "std"))]
979        /// # {
980        /// use std::fs::OpenOptions;
981        /// use hadris_fat::FatVolume;
982        ///
983        /// let disk = OpenOptions::new().read(true).write(true).open("disk.img").unwrap();
984        /// let fs = FatVolume::builder(disk).fat_cache(16).open().unwrap();
985        ///
986        /// // Walk the cluster chain of the file at first_cluster=42, using the cache.
987        /// let chain = fs
988        ///     .with_cached_fat(|cached, disk| cached.read_chain(disk, 42))
989        ///     .expect("cache installed")
990        ///     .expect("read_chain ok");
991        /// # }
992        /// ```
993        pub fn with_cached_fat<R>(
994            &self,
995            f: impl FnOnce(&mut crate::cache::CachedFat<'_>, &mut SectorCursor<DATA>) -> R,
996        ) -> Option<R> {
997            let cache_mutex = self.fat_cache.as_ref()?;
998            let mut cache = cache_mutex.lock();
999            let mut data = self.data.lock();
1000            let mut cached = crate::cache::CachedFat::new(&mut cache, &self.fat);
1001            Some(f(&mut cached, &mut *data))
1002        }
1003
1004        /// Run a closure with both the [`crate::cache::FatSectorCache`] and
1005        /// underlying disk locked for direct, FAT-type-specific access.
1006        ///
1007        /// Lower-level than [`Self::with_cached_fat`] — gives the closure
1008        /// `&mut FatSectorCache` so it can call the per-type entry-point
1009        /// methods ([`crate::cache::FatSectorCache::read_fat32_entry`],
1010        /// [`crate::cache::FatSectorCache::write_fat32_entry`], etc.). Most
1011        /// callers want [`Self::with_cached_fat`] instead, which wraps the
1012        /// cache in a `CachedFat` and hides the FAT-type dispatch.
1013        ///
1014        /// Note: do NOT call [`Self::fat_cache`]`.lock()` inside this closure —
1015        /// the cache mutex is already locked, so a second lock attempt will
1016        /// deadlock (this is a `spin::Mutex`, not a re-entrant lock).
1017        ///
1018        /// Returns `None` if no cache was installed.
1019        pub fn with_fat_cache_locked<R>(
1020            &self,
1021            f: impl FnOnce(&mut crate::cache::FatSectorCache, &mut SectorCursor<DATA>) -> R,
1022        ) -> Option<R> {
1023            let cache_mutex = self.fat_cache.as_ref()?;
1024            let mut cache = cache_mutex.lock();
1025            let mut data = self.data.lock();
1026            Some(f(&mut cache, &mut *data))
1027        }
1028    }
1029}
1030
1031// ===========================================================================
1032// Sync-only cache routing (Phase C5)
1033//
1034// `cache.rs` is sync-only (its methods don't await), so the routed
1035// FAT-table helpers below are emitted only in the sync slice. The async
1036// slice gets a thin pass-through impl from `async_only!` further down so
1037// callers in `io_transform!{}` can always write `self.next_cluster_routed(...).await?`.
1038//
1039// Lock ordering invariant: cache mutex first, then data mutex — matches
1040// `with_cached_fat`. Callers MUST NOT hold the data mutex when entering
1041// these helpers (spin::Mutex is not reentrant).
1042// ===========================================================================
1043
1044#[cfg(feature = "cache")]
1045sync_only! {
1046    impl<DATA> FatVolume<DATA>
1047    where
1048        DATA: Read + Seek,
1049    {
1050        /// Read the next cluster of `cluster`, routing through the FAT-sector
1051        /// cache if one is installed.
1052        ///
1053        /// Caller must NOT hold `self.data` — this method acquires both
1054        /// locks (cache then data) in canonical order. Returns the same
1055        /// `Result<Option<u32>>` as [`Fat::next_cluster`].
1056        pub(crate) fn next_cluster_routed(&self, cluster: usize) -> Result<Option<u32>> {
1057            use core::ops::DerefMut;
1058            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1059            let mut data = self.data.lock();
1060            if let Some(cache) = cache_guard.as_mut() {
1061                let mut cached = crate::cache::CachedFat::new(cache, &self.fat);
1062                cached.next_cluster(data.deref_mut(), cluster)
1063            } else {
1064                self.fat.next_cluster(data.deref_mut(), cluster)
1065            }
1066        }
1067
1068        /// Read `FAT[1]` status flags through the cache when installed.
1069        pub(crate) fn read_status_flags_routed(&self) -> Result<(bool, bool)> {
1070            use core::ops::DerefMut;
1071            // FAT12 has no status bits regardless of cache installation;
1072            // skip the cache lock entirely.
1073            if matches!(self.fat, Fat::Fat12(_)) {
1074                return Ok((false, false));
1075            }
1076            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1077            let mut data = self.data.lock();
1078            match (&self.fat, cache_guard.as_deref_mut()) {
1079                (Fat::Fat16(_), Some(cache)) => {
1080                    let val = cache.read_fat16_entry(data.deref_mut(), 1)?;
1081                    Ok((val & 0x8000 == 0, val & 0x4000 == 0))
1082                }
1083                (Fat::Fat32(_), Some(cache)) => {
1084                    let val = cache.read_fat32_entry(data.deref_mut(), 1)?;
1085                    Ok((val & 0x0800_0000 == 0, val & 0x0400_0000 == 0))
1086                }
1087                _ => self.fat.read_status_flags(data.deref_mut()),
1088            }
1089        }
1090    }
1091}
1092
1093#[cfg(feature = "cache")]
1094async_only! {
1095    impl<DATA> FatVolume<DATA>
1096    where
1097        DATA: Read + Seek,
1098    {
1099        /// Async pass-through: cache routing is sync-only.
1100        pub(crate) async fn next_cluster_routed(&self, cluster: usize) -> Result<Option<u32>> {
1101            use core::ops::DerefMut;
1102            let mut data = self.data.lock();
1103            self.fat.next_cluster(data.deref_mut(), cluster).await
1104        }
1105
1106        /// Async pass-through.
1107        pub(crate) async fn read_status_flags_routed(&self) -> Result<(bool, bool)> {
1108            use core::ops::DerefMut;
1109            let mut data = self.data.lock();
1110            self.fat.read_status_flags(data.deref_mut()).await
1111        }
1112    }
1113}
1114
1115// When the `cache` feature is off, the routed helpers are simple
1116// pass-throughs that drop the cache layer entirely. Defining them here
1117// keeps `io_transform!{}` call sites uniform regardless of feature flags.
1118#[cfg(not(feature = "cache"))]
1119io_transform! {
1120    impl<DATA> FatVolume<DATA>
1121    where
1122        DATA: Read + Seek,
1123    {
1124        pub(crate) async fn next_cluster_routed(&self, cluster: usize) -> Result<Option<u32>> {
1125            use core::ops::DerefMut;
1126            let mut data = self.data.lock();
1127            self.fat.next_cluster(data.deref_mut(), cluster).await
1128        }
1129
1130        pub(crate) async fn read_status_flags_routed(&self) -> Result<(bool, bool)> {
1131            use core::ops::DerefMut;
1132            let mut data = self.data.lock();
1133            self.fat.read_status_flags(data.deref_mut()).await
1134        }
1135    }
1136}
1137
1138// ===========================================================================
1139// Write routing (Phase C5)
1140// ===========================================================================
1141//
1142// When the cache is installed, FAT-table mutations must go through the cache
1143// to keep cached read state coherent with on-disk writes (see
1144// `writes_then_reads_through_cache_are_consistent` in
1145// `tests/cache_integration.rs`).
1146//
1147// `cache.rs` exposes `write_fat{12,16,32}_entry` for individual entry writes;
1148// the higher-level operations (allocate / free / truncate / mark_bad) are
1149// reimplemented here against those primitives. When no cache is installed we
1150// fall through to the existing `Fat::*` helpers, preserving today's
1151// performance characteristics.
1152//
1153// Async builds receive thin pass-throughs: the cache feature requires `sync`
1154// (Cargo.toml), so a build that lacks `sync` cannot reach these methods.
1155
1156#[cfg(all(feature = "cache", feature = "write"))]
1157sync_only! {
1158    impl<DATA> FatVolume<DATA>
1159    where
1160        DATA: Read + super::io::Write + Seek,
1161    {
1162        /// Write a single FAT entry through the cache when installed.
1163        pub(crate) fn write_clus_routed(&self, cluster: usize, value: u32) -> Result<()> {
1164            use core::ops::DerefMut;
1165            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1166            let mut data = self.data.lock();
1167            if let Some(ref mut cache) = cache_guard {
1168                match self.fat.fat_type() {
1169                    FatType::Fat12 => cache.write_fat12_entry(data.deref_mut(), cluster, value as u16),
1170                    FatType::Fat16 => cache.write_fat16_entry(data.deref_mut(), cluster, value as u16),
1171                    FatType::Fat32 => cache.write_fat32_entry(data.deref_mut(), cluster, value),
1172                }
1173            } else {
1174                match &self.fat {
1175                    Fat::Fat12(f) => f.write_clus(data.deref_mut(), cluster, value as u16),
1176                    Fat::Fat16(f) => f.write_clus(data.deref_mut(), cluster, value as u16),
1177                    Fat::Fat32(f) => f.write_clus(data.deref_mut(), cluster, value),
1178                }
1179            }
1180        }
1181
1182        /// Allocate a single cluster, returning its number. Routes through
1183        /// the cache when installed; otherwise falls through to `Fat::*`.
1184        pub(crate) fn allocate_cluster_routed(&self, hint: u32) -> Result<u32> {
1185            use core::ops::DerefMut;
1186            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1187            let mut data = self.data.lock();
1188            if let Some(ref mut cache) = cache_guard {
1189                allocate_cluster_via_cache(cache, &self.fat, data.deref_mut(), hint)
1190            } else {
1191                match &self.fat {
1192                    Fat::Fat12(f) => f.allocate_cluster(data.deref_mut(), hint as u16).map(|c| c as u32),
1193                    Fat::Fat16(f) => f.allocate_cluster(data.deref_mut(), hint as u16).map(|c| c as u32),
1194                    Fat::Fat32(f) => f.allocate_cluster(data.deref_mut(), hint),
1195                }
1196            }
1197        }
1198
1199        /// Free a cluster chain starting at `start`, returning the count of
1200        /// freed clusters. Routes through the cache when installed.
1201        pub(crate) fn free_chain_routed(&self, start: u32) -> Result<u32> {
1202            use core::ops::DerefMut;
1203            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1204            let mut data = self.data.lock();
1205            if let Some(ref mut cache) = cache_guard {
1206                free_chain_via_cache(cache, &self.fat, data.deref_mut(), start)
1207            } else {
1208                self.fat.free_chain(data.deref_mut(), start as usize)
1209            }
1210        }
1211
1212        /// Truncate a chain after the specified cluster (the cluster
1213        /// becomes the new EOC; everything after is freed).
1214        pub(crate) fn truncate_chain_routed(&self, cluster: u32) -> Result<u32> {
1215            use core::ops::DerefMut;
1216            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1217            let mut data = self.data.lock();
1218            if let Some(ref mut cache) = cache_guard {
1219                truncate_chain_via_cache(cache, &self.fat, data.deref_mut(), cluster)
1220            } else {
1221                self.fat.truncate_chain(data.deref_mut(), cluster as usize)
1222            }
1223        }
1224
1225    }
1226}
1227
1228#[cfg(all(feature = "cache", feature = "write"))]
1229async_only! {
1230    impl<DATA> FatVolume<DATA>
1231    where
1232        DATA: Read + super::io::Write + Seek,
1233    {
1234        /// Async pass-through; cache routing is sync-only.
1235        pub(crate) async fn write_clus_routed(&self, cluster: usize, value: u32) -> Result<()> {
1236            use core::ops::DerefMut;
1237            let mut data = self.data.lock();
1238            match &self.fat {
1239                Fat::Fat12(f) => f.write_clus(data.deref_mut(), cluster, value as u16).await,
1240                Fat::Fat16(f) => f.write_clus(data.deref_mut(), cluster, value as u16).await,
1241                Fat::Fat32(f) => f.write_clus(data.deref_mut(), cluster, value).await,
1242            }
1243        }
1244
1245        pub(crate) async fn allocate_cluster_routed(&self, hint: u32) -> Result<u32> {
1246            use core::ops::DerefMut;
1247            let mut data = self.data.lock();
1248            match &self.fat {
1249                Fat::Fat12(f) => f.allocate_cluster(data.deref_mut(), hint as u16).await.map(|c| c as u32),
1250                Fat::Fat16(f) => f.allocate_cluster(data.deref_mut(), hint as u16).await.map(|c| c as u32),
1251                Fat::Fat32(f) => f.allocate_cluster(data.deref_mut(), hint).await,
1252            }
1253        }
1254
1255        pub(crate) async fn free_chain_routed(&self, start: u32) -> Result<u32> {
1256            use core::ops::DerefMut;
1257            let mut data = self.data.lock();
1258            self.fat.free_chain(data.deref_mut(), start as usize).await
1259        }
1260
1261        pub(crate) async fn truncate_chain_routed(&self, cluster: u32) -> Result<u32> {
1262            use core::ops::DerefMut;
1263            let mut data = self.data.lock();
1264            self.fat.truncate_chain(data.deref_mut(), cluster as usize).await
1265        }
1266
1267    }
1268}
1269
1270// When `cache` is off, callers in `io_transform!{}` still write
1271// `self.write_clus_routed(...).await?`. Provide a uniform pass-through.
1272#[cfg(all(not(feature = "cache"), feature = "write"))]
1273io_transform! {
1274    impl<DATA> FatVolume<DATA>
1275    where
1276        DATA: Read + super::io::Write + Seek,
1277    {
1278        pub(crate) async fn write_clus_routed(&self, cluster: usize, value: u32) -> Result<()> {
1279            use core::ops::DerefMut;
1280            let mut data = self.data.lock();
1281            match &self.fat {
1282                Fat::Fat12(f) => f.write_clus(data.deref_mut(), cluster, value as u16).await,
1283                Fat::Fat16(f) => f.write_clus(data.deref_mut(), cluster, value as u16).await,
1284                Fat::Fat32(f) => f.write_clus(data.deref_mut(), cluster, value).await,
1285            }
1286        }
1287
1288        pub(crate) async fn allocate_cluster_routed(&self, hint: u32) -> Result<u32> {
1289            use core::ops::DerefMut;
1290            let mut data = self.data.lock();
1291            match &self.fat {
1292                Fat::Fat12(f) => f.allocate_cluster(data.deref_mut(), hint as u16).await.map(|c| c as u32),
1293                Fat::Fat16(f) => f.allocate_cluster(data.deref_mut(), hint as u16).await.map(|c| c as u32),
1294                Fat::Fat32(f) => f.allocate_cluster(data.deref_mut(), hint).await,
1295            }
1296        }
1297
1298        pub(crate) async fn free_chain_routed(&self, start: u32) -> Result<u32> {
1299            use core::ops::DerefMut;
1300            let mut data = self.data.lock();
1301            self.fat.free_chain(data.deref_mut(), start as usize).await
1302        }
1303
1304        pub(crate) async fn truncate_chain_routed(&self, cluster: u32) -> Result<u32> {
1305            use core::ops::DerefMut;
1306            let mut data = self.data.lock();
1307            self.fat.truncate_chain(data.deref_mut(), cluster as usize).await
1308        }
1309
1310    }
1311}
1312
1313// Free helpers used by the sync cache path. Kept here (not in cache.rs) so
1314// the cache module's API stays untouched per the C5 plan. Wrapped in
1315// `sync_only!` so they exist only in the sync slice — they invoke the
1316// synchronous `FatSectorCache` methods and so cannot compile in the async
1317// slice (where `super::io` is the async trait set). This is what lets
1318// `async + cache` build with the cache simply bypassed.
1319#[cfg(all(feature = "cache", feature = "write"))]
1320sync_only! {
1321
1322fn allocate_cluster_via_cache<T>(
1323    cache: &mut crate::cache::FatSectorCache,
1324    fat: &Fat,
1325    data: &mut T,
1326    hint: u32,
1327) -> Result<u32>
1328where
1329    T: super::io::Read + super::io::Write + super::io::Seek,
1330{
1331    const FIRST: u32 = 2;
1332    let max_cluster = fat.max_cluster();
1333    let start = if hint >= FIRST && hint <= max_cluster {
1334        hint
1335    } else {
1336        FIRST
1337    };
1338
1339    let scan = |cache: &mut crate::cache::FatSectorCache,
1340                data: &mut T,
1341                fat: &Fat,
1342                lo: u32,
1343                hi: u32|
1344     -> Result<Option<u32>> {
1345        for c in lo..=hi {
1346            let free = match fat.fat_type() {
1347                FatType::Fat12 => (cache.read_fat12_entry(data, c as usize)? & 0x0FFF) == 0,
1348                FatType::Fat16 => cache.read_fat16_entry(data, c as usize)? == 0,
1349                FatType::Fat32 => (cache.read_fat32_entry(data, c as usize)? & 0x0FFF_FFFF) == 0,
1350            };
1351            if free {
1352                return Ok(Some(c));
1353            }
1354        }
1355        Ok(None)
1356    };
1357
1358    let claim =
1359        |cache: &mut crate::cache::FatSectorCache, data: &mut T, fat: &Fat, c: u32| -> Result<()> {
1360            match fat.fat_type() {
1361                FatType::Fat12 => cache.write_fat12_entry(data, c as usize, 0x0FF8),
1362                FatType::Fat16 => cache.write_fat16_entry(data, c as usize, 0xFFF8),
1363                FatType::Fat32 => cache.write_fat32_entry(data, c as usize, 0x0FFF_FFF8),
1364            }
1365        };
1366
1367    if let Some(c) = scan(cache, data, fat, start, max_cluster)? {
1368        claim(cache, data, fat, c)?;
1369        return Ok(c);
1370    }
1371    if start > FIRST
1372        && let Some(c) = scan(cache, data, fat, FIRST, start - 1)?
1373    {
1374        claim(cache, data, fat, c)?;
1375        return Ok(c);
1376    }
1377    Err(Error::NoFreeSpace)
1378}
1379
1380#[cfg(all(feature = "cache", feature = "write"))]
1381fn free_chain_via_cache<T>(
1382    cache: &mut crate::cache::FatSectorCache,
1383    fat: &Fat,
1384    data: &mut T,
1385    start: u32,
1386) -> Result<u32>
1387where
1388    T: super::io::Read + super::io::Write + super::io::Seek,
1389{
1390    const FIRST: u32 = 2;
1391    let max_cluster = fat.max_cluster();
1392    let mut count = 0u32;
1393    let mut current = start;
1394    loop {
1395        if current < FIRST || current > max_cluster {
1396            break;
1397        }
1398        let next = read_fat_entry_via_cache(cache, fat, data, current as usize)?;
1399        write_fat_entry_via_cache(cache, fat, data, current as usize, 0)?;
1400        count += 1;
1401        if is_eoc(fat.fat_type(), next) || is_bad(fat.fat_type(), next) || next == 0 {
1402            break;
1403        }
1404        current = next;
1405    }
1406    Ok(count)
1407}
1408
1409#[cfg(all(feature = "cache", feature = "write"))]
1410fn truncate_chain_via_cache<T>(
1411    cache: &mut crate::cache::FatSectorCache,
1412    fat: &Fat,
1413    data: &mut T,
1414    cluster: u32,
1415) -> Result<u32>
1416where
1417    T: super::io::Read + super::io::Write + super::io::Seek,
1418{
1419    const FIRST: u32 = 2;
1420    let max_cluster = fat.max_cluster();
1421    if cluster < FIRST || cluster > max_cluster {
1422        return Ok(0);
1423    }
1424    let next = read_fat_entry_via_cache(cache, fat, data, cluster as usize)?;
1425    let eoc = match fat.fat_type() {
1426        FatType::Fat12 => 0x0FF8,
1427        FatType::Fat16 => 0xFFF8,
1428        FatType::Fat32 => 0x0FFF_FFF8,
1429    };
1430    write_fat_entry_via_cache(cache, fat, data, cluster as usize, eoc)?;
1431    if !is_eoc(fat.fat_type(), next) && next >= FIRST && next <= max_cluster {
1432        free_chain_via_cache(cache, fat, data, next)
1433    } else {
1434        Ok(0)
1435    }
1436}
1437
1438#[cfg(all(feature = "cache", feature = "write"))]
1439fn read_fat_entry_via_cache<T>(
1440    cache: &mut crate::cache::FatSectorCache,
1441    fat: &Fat,
1442    data: &mut T,
1443    cluster: usize,
1444) -> Result<u32>
1445where
1446    T: super::io::Read + super::io::Seek,
1447{
1448    Ok(match fat.fat_type() {
1449        FatType::Fat12 => (cache.read_fat12_entry(data, cluster)? & 0x0FFF) as u32,
1450        FatType::Fat16 => cache.read_fat16_entry(data, cluster)? as u32,
1451        FatType::Fat32 => cache.read_fat32_entry(data, cluster)? & 0x0FFF_FFFF,
1452    })
1453}
1454
1455#[cfg(all(feature = "cache", feature = "write"))]
1456fn write_fat_entry_via_cache<T>(
1457    cache: &mut crate::cache::FatSectorCache,
1458    fat: &Fat,
1459    data: &mut T,
1460    cluster: usize,
1461    value: u32,
1462) -> Result<()>
1463where
1464    T: super::io::Read + super::io::Write + super::io::Seek,
1465{
1466    match fat.fat_type() {
1467        FatType::Fat12 => cache.write_fat12_entry(data, cluster, value as u16),
1468        FatType::Fat16 => cache.write_fat16_entry(data, cluster, value as u16),
1469        FatType::Fat32 => cache.write_fat32_entry(data, cluster, value),
1470    }
1471}
1472
1473#[cfg(all(feature = "cache", feature = "write"))]
1474fn is_eoc(ty: FatType, value: u32) -> bool {
1475    match ty {
1476        FatType::Fat12 => value >= 0x0FF8,
1477        FatType::Fat16 => value >= 0xFFF8,
1478        FatType::Fat32 => value >= 0x0FFF_FFF8,
1479    }
1480}
1481
1482#[cfg(all(feature = "cache", feature = "write"))]
1483fn is_bad(ty: FatType, value: u32) -> bool {
1484    match ty {
1485        FatType::Fat12 => value == 0x0FF7,
1486        FatType::Fat16 => value == 0xFFF7,
1487        FatType::Fat32 => value == 0x0FFF_FFF7,
1488    }
1489}
1490
1491} // end sync_only! (free cache helpers)
1492
1493// ===========================================================================
1494// Flush
1495// ===========================================================================
1496
1497// Flush is only available with `cache` + `write`: nothing to flush without a
1498// cache, and a writable backing store is required to mirror dirty sectors to
1499// every FAT copy. Sync-only because `FatSectorCache::flush` uses synchronous
1500// I/O traits.
1501#[cfg(all(feature = "cache", feature = "write"))]
1502sync_only! {
1503    impl<DATA> FatVolume<DATA>
1504    where
1505        DATA: Read + super::io::Write + Seek,
1506    {
1507        /// Flush all dirty FAT cache sectors back to every FAT copy on disk.
1508        ///
1509        /// No-op when no cache was installed. Without an explicit `flush()`,
1510        /// dirty sectors are written through to disk on LRU eviction (see
1511        /// `FatSectorCache::evict_lru_flush`) or are still in memory when the
1512        /// [`FatVolume`] is dropped. Call this before tearing down the
1513        /// filesystem to guarantee the on-disk FAT is consistent.
1514        pub fn flush(&self) -> Result<()> {
1515            use core::ops::DerefMut;
1516            if let Some(cache_mutex) = &self.fat_cache {
1517                let mut cache = cache_mutex.lock();
1518                let mut data = self.data.lock();
1519                cache.flush(data.deref_mut())?;
1520            }
1521            Ok(())
1522        }
1523    }
1524}