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        let fat_start = Sector(bpb.reserved_sector_count.get()).to_bytes(data.sector_size);
616        let fat_size_per_fat =
617            Sector(bpb_ext32.sectors_per_fat_32.get()).to_bytes(data.sector_size);
618        let fat_size = bpb.fat_count as usize * fat_size_per_fat;
619
620        // Calculate total data sectors and max cluster
621        let total_sectors = if bpb.total_sectors_16 != [0, 0] {
622            u16::from_le_bytes(bpb.total_sectors_16) as u32
623        } else {
624            u32::from_le_bytes(bpb.total_sectors_32)
625        };
626        let reserved_sectors = bpb.reserved_sector_count.get() as u32;
627        let fat_sectors = bpb_ext32.sectors_per_fat_32.get() * bpb.fat_count as u32;
628        let data_sectors = total_sectors.saturating_sub(reserved_sectors + fat_sectors);
629        let max_cluster = (data_sectors / bpb.sectors_per_cluster as u32) + 1; // +1 because clusters start at 2
630
631        let fat = Fat::Fat32(Fat32::new(
632            fat_start,
633            fat_size_per_fat,
634            bpb.fat_count as usize,
635            max_cluster,
636        ));
637
638        let info = FatInfo {
639            #[cfg(feature = "alloc")]
640            cluster_size,
641            data_start: fat_start + fat_size,
642            #[cfg(feature = "alloc")]
643            max_cluster,
644        };
645
646        // Extract volume info from BPB
647        let volume_info = VolumeInfo {
648            oem_name: bpb.oem_name,
649            volume_id: u32::from_le_bytes(bpb_ext32.volume_id),
650            volume_label: bpb_ext32.volume_label,
651            fs_type_str: bpb_ext32.fs_type,
652        };
653
654        Ok(Self {
655            data: Mutex::new(data),
656            info,
657            fat,
658            ext,
659            volume_info,
660            time_provider,
661            oem_converter,
662            #[cfg(feature = "cache")]
663            fat_cache: None,
664        })
665    }
666
667    /// Borrow the configured clock used for new directory-entry timestamps.
668    pub fn time_provider(&self) -> &dyn crate::time::TimeProvider {
669        self.time_provider
670    }
671
672    /// Borrow the configured OEM codepage converter for short (8.3) names.
673    pub fn oem_converter(&self) -> &dyn crate::oem::OemCpConverter {
674        self.oem_converter
675    }
676
677    /// Borrow the FAT table descriptor.
678    ///
679    /// Required when constructing a `CachedFat` (with the `cache` feature) via
680    /// `CachedFat::new`, which needs the FAT type and
681    /// max-cluster bound. Otherwise rarely needed by callers — most FAT
682    /// operations go through [`FatVolume`] methods directly.
683    pub fn fat(&self) -> &Fat {
684        &self.fat
685    }
686
687    /// Returns the filesystem's root directory.
688    pub fn root_dir(&self) -> FatDir<'_, DATA> {
689        match &self.ext {
690            FatFsExt::Fat12_16(ext) => FatDir {
691                data: self,
692                cluster: Cluster(0), // Sentinel for fixed root directory
693                fixed_root: Some((ext.root_dir_start, ext.root_dir_size)),
694            },
695            FatFsExt::Fat32(ext) => FatDir {
696                data: self,
697                cluster: Cluster(ext.root_clus.0 as usize),
698                fixed_root: None,
699            },
700        }
701    }
702
703    /// Get the FAT type of this filesystem
704    pub fn fat_type(&self) -> FatType {
705        self.fat.fat_type()
706    }
707
708    /// Get volume metadata from the boot sector.
709    ///
710    /// This includes the OEM name, volume serial number, volume label,
711    /// and filesystem type string.
712    pub fn volume_info(&self) -> &VolumeInfo {
713        &self.volume_info
714    }
715
716    /// Get the fixed root directory info for FAT12/16 filesystems.
717    ///
718    /// Returns `Some((start_offset, size))` for FAT12/16, `None` for FAT32.
719    #[cfg(feature = "write")]
720    pub(crate) fn fixed_root_dir_info(&self) -> Option<(usize, usize)> {
721        self.ext.fixed_root_dir()
722    }
723
724    /// Returns true iff `cluster` is the FAT32 root directory cluster.
725    ///
726    /// Used by directory-creation code to honor the FAT32 spec rule that a
727    /// subdirectory's ".." entry must store cluster 0 (not the real root
728    /// cluster) when its parent is the FAT32 root.
729    #[cfg(feature = "write")]
730    pub(crate) fn is_fat32_root_cluster(&self, cluster: u32) -> bool {
731        matches!(&self.ext, FatFsExt::Fat32(ext) if ext.root_clus.0 == cluster)
732    }
733
734    /// Read the FAT-resident volume status flags from `FAT[1]`.
735    ///
736    /// `dirty` means the volume was not unmounted cleanly; `io_errors` means
737    /// the previous host saw I/O failures. FAT12 has no status bits, so the
738    /// returned flags are always `false` for FAT12 — check
739    /// [`Self::fat_type`] if that distinction matters to your caller.
740    pub async fn read_status_flags(&self) -> Result<FsStatusFlags> {
741        let (dirty, io_errors) = self.read_status_flags_routed().await?;
742        Ok(FsStatusFlags { dirty, io_errors })
743    }
744
745    /// Read the volume label from the root directory entry, if present.
746    ///
747    /// Two volume labels live on a FAT volume: one in the BPB (boot sector,
748    /// always present, available via [`Self::volume_info`]) and an optional
749    /// directory entry in the root with the `VOLUME_ID` attribute. Windows
750    /// updates the latter when a user renames the volume; the BPB copy can
751    /// drift. Use this method to read the authoritative on-disk name.
752    ///
753    /// Returns `Ok(None)` if no label entry exists.
754    pub async fn read_root_label(&self) -> Result<Option<[u8; 11]>> {
755        match self.find_root_label_entry().await? {
756            Some((_, raw)) => Ok(Some(unsafe { raw.file }.name)),
757            None => Ok(None),
758        }
759    }
760
761    /// Locate the first non-deleted, non-LFN root entry whose attribute set
762    /// is exactly `VOLUME_ID` (i.e. a real volume-label entry, not a stray
763    /// LFN component which has every bit in `LONG_NAME` set).
764    ///
765    /// Returns `Ok(Some((byte_pos, raw_entry)))` if found; `Ok(None)` if the
766    /// root iterates to its terminator without a label entry.
767    pub(crate) async fn find_root_label_entry(
768        &self,
769    ) -> Result<Option<(usize, crate::raw::RawDirectoryEntry)>> {
770        use crate::raw::{DirEntryAttrFlags, RawDirectoryEntry};
771        let entry_size = core::mem::size_of::<RawDirectoryEntry>();
772        let mut data = self.data.lock();
773
774        let is_label =
775            |attr: u8| DirEntryAttrFlags::from_bits_retain(attr).is_volume_label_entry();
776
777        match &self.ext {
778            FatFsExt::Fat12_16(ext) => {
779                let end = ext.root_dir_start + ext.root_dir_size;
780                let mut pos = ext.root_dir_start;
781                while pos + entry_size <= end {
782                    data.seek(SeekFrom::Start(pos as u64)).await?;
783                    let raw = data.read_struct::<RawDirectoryEntry>().await?;
784                    let bytes = unsafe { raw.bytes };
785                    if bytes[0] == 0 {
786                        return Ok(None);
787                    }
788                    if bytes[0] != 0xE5 && is_label(unsafe { raw.file }.attributes) {
789                        return Ok(Some((pos, raw)));
790                    }
791                    pos += entry_size;
792                }
793                Ok(None)
794            }
795            FatFsExt::Fat32(ext) => {
796                let cluster_size = data.cluster_size;
797                let mut current = ext.root_clus.0 as usize;
798                let chain_limit = self.fat.max_cluster();
799                let mut steps: u32 = 0;
800                loop {
801                    steps = steps.saturating_add(1);
802                    if steps > chain_limit {
803                        return Err(Error::ClusterLoop { cluster: current as u32 });
804                    }
805                    let cluster_start = Cluster(current).to_bytes(self.info.data_start, cluster_size);
806                    let mut offset = 0;
807                    while offset + entry_size <= cluster_size {
808                        let pos = cluster_start + offset;
809                        data.seek(SeekFrom::Start(pos as u64)).await?;
810                        let raw = data.read_struct::<RawDirectoryEntry>().await?;
811                        let bytes = unsafe { raw.bytes };
812                        if bytes[0] == 0 {
813                            return Ok(None);
814                        }
815                        if bytes[0] != 0xE5 && is_label(unsafe { raw.file }.attributes) {
816                            return Ok(Some((pos, raw)));
817                        }
818                        offset += entry_size;
819                    }
820                    // Drop the data lock before calling the routed helper —
821                    // it acquires cache+data in canonical order and would
822                    // deadlock if we still held data.
823                    drop(data);
824                    let next_cluster = self.next_cluster_routed(current).await?;
825                    data = self.data.lock();
826                    match next_cluster {
827                        Some(next) => current = next as usize,
828                        None => return Ok(None),
829                    }
830                }
831            }
832        }
833    }
834
835    /// Open a file or directory by path (e.g., "/dir/subdir/file.txt").
836    ///
837    /// Paths can use forward slashes as separators. Leading slashes are optional.
838    /// Empty path components are ignored.
839    pub async fn open_path(&self, path: &str) -> Result<FileEntry> {
840        let mut current_dir = self.root_dir();
841        let mut last_component = None;
842
843        for component in VPath::new(path).components() {
844            let component = match component {
845                Component::Root | Component::Current => continue,
846                Component::Parent => return Err(Error::InvalidPath),
847                Component::Normal(component) => component,
848            };
849            if let Some(prev) = last_component.take() {
850                // Navigate into the previous component as a directory
851                current_dir = current_dir.open_dir(prev).await?;
852            }
853            last_component = Some(component);
854        }
855
856        // Find the final entry
857        let final_name = last_component.ok_or(Error::InvalidPath)?;
858        current_dir.find(final_name).await?.ok_or(Error::EntryNotFound)
859    }
860
861    /// Open a file by path for reading.
862    ///
863    /// This is a convenience method that combines [`open_path`](Self::open_path)
864    /// with opening a file reader.
865    pub async fn open_file_path(&self, path: &str) -> Result<FileReader<'_, DATA>> {
866        let entry = self.open_path(path).await?;
867        FileReader::new(self, &entry)
868    }
869
870    /// Open a directory by path.
871    ///
872    /// This is a convenience method that combines [`open_path`](Self::open_path)
873    /// with validating the entry is a directory.
874    pub async fn open_dir_path(&self, path: &str) -> Result<FatDir<'_, DATA>> {
875        let entry = self.open_path(path).await?;
876        if !entry.is_directory() {
877            return Err(Error::NotADirectory);
878        }
879        // Subdirectories opened by path are never fixed root
880        Ok(FatDir {
881            data: self,
882            cluster: entry.cluster(),
883            fixed_root: None,
884        })
885    }
886
887    /// Open a directory from a file entry.
888    ///
889    /// The entry must be a directory.
890    pub fn open_dir_entry(&self, entry: &FileEntry) -> Result<FatDir<'_, DATA>> {
891        if !entry.is_directory() {
892            return Err(Error::NotADirectory);
893        }
894        Ok(FatDir {
895            data: self,
896            cluster: entry.cluster(),
897            fixed_root: None,
898        })
899    }
900}
901
902} // end io_transform!
903
904// ===========================================================================
905// Sync-only cache accessors
906//
907// These expose the installed FAT-sector cache to callers. They reference the
908// sync-only `crate::cache` types (`FatSectorCache`, `CachedFat`) and call
909// their synchronous I/O methods, so they are emitted only in the sync slice.
910// Under the async API the cache is bypassed (a build with both `async` and
911// `cache` keeps the field but offers no async cache accessors), which is what
912// lets `--features async,cache` (and `--all-features`) compile.
913// ===========================================================================
914
915#[cfg(feature = "cache")]
916sync_only! {
917    impl<DATA> FatVolume<DATA>
918    where
919        DATA: Read + Seek,
920    {
921        /// Borrow the optional FAT-sector cache configured via
922        /// [`FatVolumeBuilder::fat_cache`].
923        ///
924        /// Returns `None` if no cache was installed. Pair with [`Self::fat`]
925        /// and [`crate::cache::CachedFat::new`] to perform cached FAT
926        /// operations, or use the higher-level [`Self::with_cached_fat`]
927        /// helper which holds the cache and disk locks for you.
928        pub fn fat_cache(&self) -> Option<&Mutex<crate::cache::FatSectorCache>> {
929            self.fat_cache.as_ref()
930        }
931
932        /// Run a closure with a [`crate::cache::CachedFat`] view backed by this
933        /// filesystem's installed FAT cache and underlying disk handle.
934        ///
935        /// Returns `None` if no cache was installed via
936        /// [`FatVolumeBuilder::fat_cache`]. Otherwise locks the cache mutex
937        /// and the data mutex for the duration of the closure and returns
938        /// `Some(value)` where `value` is the closure's return.
939        ///
940        /// `FatVolume`'s built-in methods consult the cache
941        /// automatically; this helper remains useful for bulk FAT walks
942        /// (free-cluster scans, multi-chain traversal) where holding the
943        /// cache+disk locks across many entries is cheaper than re-acquiring
944        /// them per call.
945        ///
946        /// # Example
947        ///
948        /// ```rust,no_run
949        /// # #[cfg(all(feature = "cache", feature = "std"))]
950        /// # {
951        /// use std::fs::OpenOptions;
952        /// use hadris_fat::FatVolume;
953        ///
954        /// let disk = OpenOptions::new().read(true).write(true).open("disk.img").unwrap();
955        /// let fs = FatVolume::builder(disk).fat_cache(16).open().unwrap();
956        ///
957        /// // Walk the cluster chain of the file at first_cluster=42, using the cache.
958        /// let chain = fs
959        ///     .with_cached_fat(|cached, disk| cached.read_chain(disk, 42))
960        ///     .expect("cache installed")
961        ///     .expect("read_chain ok");
962        /// # }
963        /// ```
964        pub fn with_cached_fat<R>(
965            &self,
966            f: impl FnOnce(&mut crate::cache::CachedFat<'_>, &mut SectorCursor<DATA>) -> R,
967        ) -> Option<R> {
968            let cache_mutex = self.fat_cache.as_ref()?;
969            let mut cache = cache_mutex.lock();
970            let mut data = self.data.lock();
971            let mut cached = crate::cache::CachedFat::new(&mut cache, &self.fat);
972            Some(f(&mut cached, &mut *data))
973        }
974
975        /// Run a closure with both the [`crate::cache::FatSectorCache`] and
976        /// underlying disk locked for direct, FAT-type-specific access.
977        ///
978        /// Lower-level than [`Self::with_cached_fat`] — gives the closure
979        /// `&mut FatSectorCache` so it can call the per-type entry-point
980        /// methods ([`crate::cache::FatSectorCache::read_fat32_entry`],
981        /// [`crate::cache::FatSectorCache::write_fat32_entry`], etc.). Most
982        /// callers want [`Self::with_cached_fat`] instead, which wraps the
983        /// cache in a `CachedFat` and hides the FAT-type dispatch.
984        ///
985        /// Note: do NOT call [`Self::fat_cache`]`.lock()` inside this closure —
986        /// the cache mutex is already locked, so a second lock attempt will
987        /// deadlock (this is a `spin::Mutex`, not a re-entrant lock).
988        ///
989        /// Returns `None` if no cache was installed.
990        pub fn with_fat_cache_locked<R>(
991            &self,
992            f: impl FnOnce(&mut crate::cache::FatSectorCache, &mut SectorCursor<DATA>) -> R,
993        ) -> Option<R> {
994            let cache_mutex = self.fat_cache.as_ref()?;
995            let mut cache = cache_mutex.lock();
996            let mut data = self.data.lock();
997            Some(f(&mut cache, &mut *data))
998        }
999    }
1000}
1001
1002// ===========================================================================
1003// Sync-only cache routing (Phase C5)
1004//
1005// `cache.rs` is sync-only (its methods don't await), so the routed
1006// FAT-table helpers below are emitted only in the sync slice. The async
1007// slice gets a thin pass-through impl from `async_only!` further down so
1008// callers in `io_transform!{}` can always write `self.next_cluster_routed(...).await?`.
1009//
1010// Lock ordering invariant: cache mutex first, then data mutex — matches
1011// `with_cached_fat`. Callers MUST NOT hold the data mutex when entering
1012// these helpers (spin::Mutex is not reentrant).
1013// ===========================================================================
1014
1015#[cfg(feature = "cache")]
1016sync_only! {
1017    impl<DATA> FatVolume<DATA>
1018    where
1019        DATA: Read + Seek,
1020    {
1021        /// Read the next cluster of `cluster`, routing through the FAT-sector
1022        /// cache if one is installed.
1023        ///
1024        /// Caller must NOT hold `self.data` — this method acquires both
1025        /// locks (cache then data) in canonical order. Returns the same
1026        /// `Result<Option<u32>>` as [`Fat::next_cluster`].
1027        pub(crate) fn next_cluster_routed(&self, cluster: usize) -> Result<Option<u32>> {
1028            use core::ops::DerefMut;
1029            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1030            let mut data = self.data.lock();
1031            if let Some(cache) = cache_guard.as_mut() {
1032                let mut cached = crate::cache::CachedFat::new(cache, &self.fat);
1033                cached.next_cluster(data.deref_mut(), cluster)
1034            } else {
1035                self.fat.next_cluster(data.deref_mut(), cluster)
1036            }
1037        }
1038
1039        /// Read `FAT[1]` status flags through the cache when installed.
1040        pub(crate) fn read_status_flags_routed(&self) -> Result<(bool, bool)> {
1041            use core::ops::DerefMut;
1042            // FAT12 has no status bits regardless of cache installation;
1043            // skip the cache lock entirely.
1044            if matches!(self.fat, Fat::Fat12(_)) {
1045                return Ok((false, false));
1046            }
1047            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1048            let mut data = self.data.lock();
1049            match (&self.fat, cache_guard.as_deref_mut()) {
1050                (Fat::Fat16(_), Some(cache)) => {
1051                    let val = cache.read_fat16_entry(data.deref_mut(), 1)?;
1052                    Ok((val & 0x8000 == 0, val & 0x4000 == 0))
1053                }
1054                (Fat::Fat32(_), Some(cache)) => {
1055                    let val = cache.read_fat32_entry(data.deref_mut(), 1)?;
1056                    Ok((val & 0x0800_0000 == 0, val & 0x0400_0000 == 0))
1057                }
1058                _ => self.fat.read_status_flags(data.deref_mut()),
1059            }
1060        }
1061    }
1062}
1063
1064#[cfg(feature = "cache")]
1065async_only! {
1066    impl<DATA> FatVolume<DATA>
1067    where
1068        DATA: Read + Seek,
1069    {
1070        /// Async pass-through: cache routing is sync-only.
1071        pub(crate) async fn next_cluster_routed(&self, cluster: usize) -> Result<Option<u32>> {
1072            use core::ops::DerefMut;
1073            let mut data = self.data.lock();
1074            self.fat.next_cluster(data.deref_mut(), cluster).await
1075        }
1076
1077        /// Async pass-through.
1078        pub(crate) async fn read_status_flags_routed(&self) -> Result<(bool, bool)> {
1079            use core::ops::DerefMut;
1080            let mut data = self.data.lock();
1081            self.fat.read_status_flags(data.deref_mut()).await
1082        }
1083    }
1084}
1085
1086// When the `cache` feature is off, the routed helpers are simple
1087// pass-throughs that drop the cache layer entirely. Defining them here
1088// keeps `io_transform!{}` call sites uniform regardless of feature flags.
1089#[cfg(not(feature = "cache"))]
1090io_transform! {
1091    impl<DATA> FatVolume<DATA>
1092    where
1093        DATA: Read + Seek,
1094    {
1095        pub(crate) async fn next_cluster_routed(&self, cluster: usize) -> Result<Option<u32>> {
1096            use core::ops::DerefMut;
1097            let mut data = self.data.lock();
1098            self.fat.next_cluster(data.deref_mut(), cluster).await
1099        }
1100
1101        pub(crate) async fn read_status_flags_routed(&self) -> Result<(bool, bool)> {
1102            use core::ops::DerefMut;
1103            let mut data = self.data.lock();
1104            self.fat.read_status_flags(data.deref_mut()).await
1105        }
1106    }
1107}
1108
1109// ===========================================================================
1110// Write routing (Phase C5)
1111// ===========================================================================
1112//
1113// When the cache is installed, FAT-table mutations must go through the cache
1114// to keep cached read state coherent with on-disk writes (see
1115// `writes_then_reads_through_cache_are_consistent` in
1116// `tests/cache_integration.rs`).
1117//
1118// `cache.rs` exposes `write_fat{12,16,32}_entry` for individual entry writes;
1119// the higher-level operations (allocate / free / truncate / mark_bad) are
1120// reimplemented here against those primitives. When no cache is installed we
1121// fall through to the existing `Fat::*` helpers, preserving today's
1122// performance characteristics.
1123//
1124// Async builds receive thin pass-throughs: the cache feature requires `sync`
1125// (Cargo.toml), so a build that lacks `sync` cannot reach these methods.
1126
1127#[cfg(all(feature = "cache", feature = "write"))]
1128sync_only! {
1129    impl<DATA> FatVolume<DATA>
1130    where
1131        DATA: Read + super::io::Write + Seek,
1132    {
1133        /// Write a single FAT entry through the cache when installed.
1134        pub(crate) fn write_clus_routed(&self, cluster: usize, value: u32) -> Result<()> {
1135            use core::ops::DerefMut;
1136            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1137            let mut data = self.data.lock();
1138            if let Some(ref mut cache) = cache_guard {
1139                match self.fat.fat_type() {
1140                    FatType::Fat12 => cache.write_fat12_entry(data.deref_mut(), cluster, value as u16),
1141                    FatType::Fat16 => cache.write_fat16_entry(data.deref_mut(), cluster, value as u16),
1142                    FatType::Fat32 => cache.write_fat32_entry(data.deref_mut(), cluster, value),
1143                }
1144            } else {
1145                match &self.fat {
1146                    Fat::Fat12(f) => f.write_clus(data.deref_mut(), cluster, value as u16),
1147                    Fat::Fat16(f) => f.write_clus(data.deref_mut(), cluster, value as u16),
1148                    Fat::Fat32(f) => f.write_clus(data.deref_mut(), cluster, value),
1149                }
1150            }
1151        }
1152
1153        /// Allocate a single cluster, returning its number. Routes through
1154        /// the cache when installed; otherwise falls through to `Fat::*`.
1155        pub(crate) fn allocate_cluster_routed(&self, hint: u32) -> Result<u32> {
1156            use core::ops::DerefMut;
1157            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1158            let mut data = self.data.lock();
1159            if let Some(ref mut cache) = cache_guard {
1160                allocate_cluster_via_cache(cache, &self.fat, data.deref_mut(), hint)
1161            } else {
1162                match &self.fat {
1163                    Fat::Fat12(f) => f.allocate_cluster(data.deref_mut(), hint as u16).map(|c| c as u32),
1164                    Fat::Fat16(f) => f.allocate_cluster(data.deref_mut(), hint as u16).map(|c| c as u32),
1165                    Fat::Fat32(f) => f.allocate_cluster(data.deref_mut(), hint),
1166                }
1167            }
1168        }
1169
1170        /// Free a cluster chain starting at `start`, returning the count of
1171        /// freed clusters. Routes through the cache when installed.
1172        pub(crate) fn free_chain_routed(&self, start: u32) -> Result<u32> {
1173            use core::ops::DerefMut;
1174            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1175            let mut data = self.data.lock();
1176            if let Some(ref mut cache) = cache_guard {
1177                free_chain_via_cache(cache, &self.fat, data.deref_mut(), start)
1178            } else {
1179                self.fat.free_chain(data.deref_mut(), start as usize)
1180            }
1181        }
1182
1183        /// Truncate a chain after the specified cluster (the cluster
1184        /// becomes the new EOC; everything after is freed).
1185        pub(crate) fn truncate_chain_routed(&self, cluster: u32) -> Result<u32> {
1186            use core::ops::DerefMut;
1187            let mut cache_guard = self.fat_cache.as_ref().map(|m| m.lock());
1188            let mut data = self.data.lock();
1189            if let Some(ref mut cache) = cache_guard {
1190                truncate_chain_via_cache(cache, &self.fat, data.deref_mut(), cluster)
1191            } else {
1192                self.fat.truncate_chain(data.deref_mut(), cluster as usize)
1193            }
1194        }
1195
1196    }
1197}
1198
1199#[cfg(all(feature = "cache", feature = "write"))]
1200async_only! {
1201    impl<DATA> FatVolume<DATA>
1202    where
1203        DATA: Read + super::io::Write + Seek,
1204    {
1205        /// Async pass-through; cache routing is sync-only.
1206        pub(crate) async fn write_clus_routed(&self, cluster: usize, value: u32) -> Result<()> {
1207            use core::ops::DerefMut;
1208            let mut data = self.data.lock();
1209            match &self.fat {
1210                Fat::Fat12(f) => f.write_clus(data.deref_mut(), cluster, value as u16).await,
1211                Fat::Fat16(f) => f.write_clus(data.deref_mut(), cluster, value as u16).await,
1212                Fat::Fat32(f) => f.write_clus(data.deref_mut(), cluster, value).await,
1213            }
1214        }
1215
1216        pub(crate) async fn allocate_cluster_routed(&self, hint: u32) -> Result<u32> {
1217            use core::ops::DerefMut;
1218            let mut data = self.data.lock();
1219            match &self.fat {
1220                Fat::Fat12(f) => f.allocate_cluster(data.deref_mut(), hint as u16).await.map(|c| c as u32),
1221                Fat::Fat16(f) => f.allocate_cluster(data.deref_mut(), hint as u16).await.map(|c| c as u32),
1222                Fat::Fat32(f) => f.allocate_cluster(data.deref_mut(), hint).await,
1223            }
1224        }
1225
1226        pub(crate) async fn free_chain_routed(&self, start: u32) -> Result<u32> {
1227            use core::ops::DerefMut;
1228            let mut data = self.data.lock();
1229            self.fat.free_chain(data.deref_mut(), start as usize).await
1230        }
1231
1232        pub(crate) async fn truncate_chain_routed(&self, cluster: u32) -> Result<u32> {
1233            use core::ops::DerefMut;
1234            let mut data = self.data.lock();
1235            self.fat.truncate_chain(data.deref_mut(), cluster as usize).await
1236        }
1237
1238    }
1239}
1240
1241// When `cache` is off, callers in `io_transform!{}` still write
1242// `self.write_clus_routed(...).await?`. Provide a uniform pass-through.
1243#[cfg(all(not(feature = "cache"), feature = "write"))]
1244io_transform! {
1245    impl<DATA> FatVolume<DATA>
1246    where
1247        DATA: Read + super::io::Write + Seek,
1248    {
1249        pub(crate) async fn write_clus_routed(&self, cluster: usize, value: u32) -> Result<()> {
1250            use core::ops::DerefMut;
1251            let mut data = self.data.lock();
1252            match &self.fat {
1253                Fat::Fat12(f) => f.write_clus(data.deref_mut(), cluster, value as u16).await,
1254                Fat::Fat16(f) => f.write_clus(data.deref_mut(), cluster, value as u16).await,
1255                Fat::Fat32(f) => f.write_clus(data.deref_mut(), cluster, value).await,
1256            }
1257        }
1258
1259        pub(crate) async fn allocate_cluster_routed(&self, hint: u32) -> Result<u32> {
1260            use core::ops::DerefMut;
1261            let mut data = self.data.lock();
1262            match &self.fat {
1263                Fat::Fat12(f) => f.allocate_cluster(data.deref_mut(), hint as u16).await.map(|c| c as u32),
1264                Fat::Fat16(f) => f.allocate_cluster(data.deref_mut(), hint as u16).await.map(|c| c as u32),
1265                Fat::Fat32(f) => f.allocate_cluster(data.deref_mut(), hint).await,
1266            }
1267        }
1268
1269        pub(crate) async fn free_chain_routed(&self, start: u32) -> Result<u32> {
1270            use core::ops::DerefMut;
1271            let mut data = self.data.lock();
1272            self.fat.free_chain(data.deref_mut(), start as usize).await
1273        }
1274
1275        pub(crate) async fn truncate_chain_routed(&self, cluster: u32) -> Result<u32> {
1276            use core::ops::DerefMut;
1277            let mut data = self.data.lock();
1278            self.fat.truncate_chain(data.deref_mut(), cluster as usize).await
1279        }
1280
1281    }
1282}
1283
1284// Free helpers used by the sync cache path. Kept here (not in cache.rs) so
1285// the cache module's API stays untouched per the C5 plan. Wrapped in
1286// `sync_only!` so they exist only in the sync slice — they invoke the
1287// synchronous `FatSectorCache` methods and so cannot compile in the async
1288// slice (where `super::io` is the async trait set). This is what lets
1289// `async + cache` build with the cache simply bypassed.
1290#[cfg(all(feature = "cache", feature = "write"))]
1291sync_only! {
1292
1293fn allocate_cluster_via_cache<T>(
1294    cache: &mut crate::cache::FatSectorCache,
1295    fat: &Fat,
1296    data: &mut T,
1297    hint: u32,
1298) -> Result<u32>
1299where
1300    T: super::io::Read + super::io::Write + super::io::Seek,
1301{
1302    const FIRST: u32 = 2;
1303    let max_cluster = fat.max_cluster();
1304    let start = if hint >= FIRST && hint <= max_cluster {
1305        hint
1306    } else {
1307        FIRST
1308    };
1309
1310    let scan = |cache: &mut crate::cache::FatSectorCache,
1311                data: &mut T,
1312                fat: &Fat,
1313                lo: u32,
1314                hi: u32|
1315     -> Result<Option<u32>> {
1316        for c in lo..=hi {
1317            let free = match fat.fat_type() {
1318                FatType::Fat12 => (cache.read_fat12_entry(data, c as usize)? & 0x0FFF) == 0,
1319                FatType::Fat16 => cache.read_fat16_entry(data, c as usize)? == 0,
1320                FatType::Fat32 => (cache.read_fat32_entry(data, c as usize)? & 0x0FFF_FFFF) == 0,
1321            };
1322            if free {
1323                return Ok(Some(c));
1324            }
1325        }
1326        Ok(None)
1327    };
1328
1329    let claim =
1330        |cache: &mut crate::cache::FatSectorCache, data: &mut T, fat: &Fat, c: u32| -> Result<()> {
1331            match fat.fat_type() {
1332                FatType::Fat12 => cache.write_fat12_entry(data, c as usize, 0x0FF8),
1333                FatType::Fat16 => cache.write_fat16_entry(data, c as usize, 0xFFF8),
1334                FatType::Fat32 => cache.write_fat32_entry(data, c as usize, 0x0FFF_FFF8),
1335            }
1336        };
1337
1338    if let Some(c) = scan(cache, data, fat, start, max_cluster)? {
1339        claim(cache, data, fat, c)?;
1340        return Ok(c);
1341    }
1342    if start > FIRST
1343        && let Some(c) = scan(cache, data, fat, FIRST, start - 1)?
1344    {
1345        claim(cache, data, fat, c)?;
1346        return Ok(c);
1347    }
1348    Err(Error::NoFreeSpace)
1349}
1350
1351#[cfg(all(feature = "cache", feature = "write"))]
1352fn free_chain_via_cache<T>(
1353    cache: &mut crate::cache::FatSectorCache,
1354    fat: &Fat,
1355    data: &mut T,
1356    start: u32,
1357) -> Result<u32>
1358where
1359    T: super::io::Read + super::io::Write + super::io::Seek,
1360{
1361    const FIRST: u32 = 2;
1362    let max_cluster = fat.max_cluster();
1363    let mut count = 0u32;
1364    let mut current = start;
1365    loop {
1366        if current < FIRST || current > max_cluster {
1367            break;
1368        }
1369        let next = read_fat_entry_via_cache(cache, fat, data, current as usize)?;
1370        write_fat_entry_via_cache(cache, fat, data, current as usize, 0)?;
1371        count += 1;
1372        if is_eoc(fat.fat_type(), next) || is_bad(fat.fat_type(), next) || next == 0 {
1373            break;
1374        }
1375        current = next;
1376    }
1377    Ok(count)
1378}
1379
1380#[cfg(all(feature = "cache", feature = "write"))]
1381fn truncate_chain_via_cache<T>(
1382    cache: &mut crate::cache::FatSectorCache,
1383    fat: &Fat,
1384    data: &mut T,
1385    cluster: 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    if cluster < FIRST || cluster > max_cluster {
1393        return Ok(0);
1394    }
1395    let next = read_fat_entry_via_cache(cache, fat, data, cluster as usize)?;
1396    let eoc = match fat.fat_type() {
1397        FatType::Fat12 => 0x0FF8,
1398        FatType::Fat16 => 0xFFF8,
1399        FatType::Fat32 => 0x0FFF_FFF8,
1400    };
1401    write_fat_entry_via_cache(cache, fat, data, cluster as usize, eoc)?;
1402    if !is_eoc(fat.fat_type(), next) && next >= FIRST && next <= max_cluster {
1403        free_chain_via_cache(cache, fat, data, next)
1404    } else {
1405        Ok(0)
1406    }
1407}
1408
1409#[cfg(all(feature = "cache", feature = "write"))]
1410fn read_fat_entry_via_cache<T>(
1411    cache: &mut crate::cache::FatSectorCache,
1412    fat: &Fat,
1413    data: &mut T,
1414    cluster: usize,
1415) -> Result<u32>
1416where
1417    T: super::io::Read + super::io::Seek,
1418{
1419    Ok(match fat.fat_type() {
1420        FatType::Fat12 => (cache.read_fat12_entry(data, cluster)? & 0x0FFF) as u32,
1421        FatType::Fat16 => cache.read_fat16_entry(data, cluster)? as u32,
1422        FatType::Fat32 => cache.read_fat32_entry(data, cluster)? & 0x0FFF_FFFF,
1423    })
1424}
1425
1426#[cfg(all(feature = "cache", feature = "write"))]
1427fn write_fat_entry_via_cache<T>(
1428    cache: &mut crate::cache::FatSectorCache,
1429    fat: &Fat,
1430    data: &mut T,
1431    cluster: usize,
1432    value: u32,
1433) -> Result<()>
1434where
1435    T: super::io::Read + super::io::Write + super::io::Seek,
1436{
1437    match fat.fat_type() {
1438        FatType::Fat12 => cache.write_fat12_entry(data, cluster, value as u16),
1439        FatType::Fat16 => cache.write_fat16_entry(data, cluster, value as u16),
1440        FatType::Fat32 => cache.write_fat32_entry(data, cluster, value),
1441    }
1442}
1443
1444#[cfg(all(feature = "cache", feature = "write"))]
1445fn is_eoc(ty: FatType, value: u32) -> bool {
1446    match ty {
1447        FatType::Fat12 => value >= 0x0FF8,
1448        FatType::Fat16 => value >= 0xFFF8,
1449        FatType::Fat32 => value >= 0x0FFF_FFF8,
1450    }
1451}
1452
1453#[cfg(all(feature = "cache", feature = "write"))]
1454fn is_bad(ty: FatType, value: u32) -> bool {
1455    match ty {
1456        FatType::Fat12 => value == 0x0FF7,
1457        FatType::Fat16 => value == 0xFFF7,
1458        FatType::Fat32 => value == 0x0FFF_FFF7,
1459    }
1460}
1461
1462} // end sync_only! (free cache helpers)
1463
1464// ===========================================================================
1465// Flush
1466// ===========================================================================
1467
1468// Flush is only available with `cache` + `write`: nothing to flush without a
1469// cache, and a writable backing store is required to mirror dirty sectors to
1470// every FAT copy. Sync-only because `FatSectorCache::flush` uses synchronous
1471// I/O traits.
1472#[cfg(all(feature = "cache", feature = "write"))]
1473sync_only! {
1474    impl<DATA> FatVolume<DATA>
1475    where
1476        DATA: Read + super::io::Write + Seek,
1477    {
1478        /// Flush all dirty FAT cache sectors back to every FAT copy on disk.
1479        ///
1480        /// No-op when no cache was installed. Without an explicit `flush()`,
1481        /// dirty sectors are written through to disk on LRU eviction (see
1482        /// `FatSectorCache::evict_lru_flush`) or are still in memory when the
1483        /// [`FatVolume`] is dropped. Call this before tearing down the
1484        /// filesystem to guarantee the on-disk FAT is consistent.
1485        pub fn flush(&self) -> Result<()> {
1486            use core::ops::DerefMut;
1487            if let Some(cache_mutex) = &self.fat_cache {
1488                let mut cache = cache_mutex.lock();
1489                let mut data = self.data.lock();
1490                cache.flush(data.deref_mut())?;
1491            }
1492            Ok(())
1493        }
1494    }
1495}