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