Skip to main content

hadris_fat/
cache.rs

1//! FAT sector caching for reduced I/O operations.
2//!
3//! This module provides a write-back sector cache for FAT table operations,
4//! significantly reducing the number of seek and read operations when
5//! traversing cluster chains.
6//!
7//! # Quick start
8//!
9//! Most callers don't construct [`FatSectorCache`] directly — install one
10//! on a [`crate::FatVolume`] via [`crate::FatVolumeBuilder::fat_cache`] and
11//! drive it through [`crate::FatVolume::with_cached_fat`]:
12//!
13//! ```rust,no_run
14//! # #[cfg(all(feature = "cache", feature = "std"))]
15//! # {
16//! use std::fs::OpenOptions;
17//! use hadris_fat::FatVolume;
18//!
19//! let disk = OpenOptions::new().read(true).write(true).open("disk.img").unwrap();
20//! let fs = FatVolume::builder(disk).fat_cache(16).open().unwrap();
21//!
22//! // Walk the FAT cluster chain starting at cluster 42, using the cache.
23//! // The closure runs with both the cache and disk mutexes held.
24//! let chain = fs
25//!     .with_cached_fat(|cached, disk| cached.read_chain(disk, 42))
26//!     .expect("cache installed")
27//!     .expect("read_chain ok");
28//!
29//! // After dirty writes, flush before dropping the FatVolume to persist.
30//! fs.flush().unwrap();
31//! # }
32//! ```
33//!
34//! Built-in `FatVolume` operations (`read_file`, `create_file`, `delete`,
35//! `truncate`, etc.) consult the cache
36//! transparently — internal FAT-table reads go through
37//! `next_cluster_routed` and writes go through `write_clus_routed` /
38//! `allocate_cluster_routed` / `free_chain_routed` /
39//! `truncate_chain_routed`. [`crate::FatVolume::with_cached_fat`] remains the
40//! recommended entry point for *bulk* user-driven walks (free-cluster
41//! scans, chain traversal) where holding the cache+disk locks across
42//! many entries is cheaper than re-acquiring them per call.
43//!
44//! Caching is a **synchronous-only** feature: the `cache` Cargo feature
45//! implies `sync`, and the `FatVolume` cache wiring is emitted only in the
46//! sync slice. Async operations access the FAT directly.
47//!
48//! # Semantics
49//!
50//! Reads ([`FatSectorCache::read_fat12_entry`] etc.) take any
51//! `Read + Seek` source — they never need to write. If the cache is full of
52//! *dirty* entries (lines you wrote and haven't flushed), a fresh read may
53//! need to evict a dirty entry; in that case it returns
54//! [`Error::CacheDirtyEviction`] rather than silently dropping bytes.
55//! Call [`FatSectorCache::flush`] (or [`crate::FatVolume::flush`]) to empty the
56//! dirty pool.
57//!
58//! Writes ([`FatSectorCache::write_fat12_entry`] etc.) take
59//! `Read + Write + Seek` and *write through* on eviction: a dirty sector is
60//! flushed to every FAT copy before it leaves the cache, so writes are never
61//! silently lost.
62
63use alloc::vec::Vec;
64
65use super::fat_table::{Fat, FatType};
66use super::io::{Read, Seek, SeekFrom, Write};
67use crate::error::{Error, Result};
68
69/// Default number of sectors to cache.
70pub const DEFAULT_CACHE_CAPACITY: usize = 16;
71
72/// A cached FAT sector with LRU tracking.
73#[derive(Debug)]
74struct CacheEntry {
75    /// Sector number within the FAT
76    sector: usize,
77    /// Sector data
78    data: Vec<u8>,
79    /// Whether the sector has been modified
80    dirty: bool,
81    /// Access counter for LRU eviction
82    access_count: u64,
83}
84
85/// FAT sector cache with LRU eviction.
86///
87/// This cache stores FAT table sectors in memory to reduce seek operations
88/// during cluster chain traversal and allocation.
89#[derive(Debug)]
90pub struct FatSectorCache {
91    /// Cached sectors
92    entries: Vec<CacheEntry>,
93    /// Maximum number of sectors to cache
94    capacity: usize,
95    /// Size of each sector in bytes
96    sector_size: usize,
97    /// Start offset of the FAT in bytes
98    fat_start: usize,
99    /// Size of one FAT copy in bytes
100    fat_size: usize,
101    /// Number of FAT copies
102    fat_count: usize,
103    /// Global access counter for LRU tracking
104    access_counter: u64,
105    /// Cache statistics
106    stats: CacheStats,
107}
108
109/// Cache performance statistics.
110#[derive(Debug, Default, Clone, Copy)]
111pub struct CacheStats {
112    /// Number of cache hits
113    pub hits: u64,
114    /// Number of cache misses
115    pub misses: u64,
116    /// Number of evictions
117    pub evictions: u64,
118    /// Number of dirty sector writes
119    pub dirty_writes: u64,
120}
121
122impl CacheStats {
123    /// Calculate the cache hit ratio (0.0 to 1.0).
124    pub fn hit_ratio(&self) -> f64 {
125        let total = self.hits + self.misses;
126        if total == 0 {
127            0.0
128        } else {
129            self.hits as f64 / total as f64
130        }
131    }
132}
133
134impl FatSectorCache {
135    /// Create a new FAT sector cache.
136    ///
137    /// # Arguments
138    ///
139    /// * `fat_start` - Start offset of the FAT in bytes
140    /// * `fat_size` - Size of one FAT copy in bytes
141    /// * `fat_count` - Number of FAT copies (typically 2)
142    /// * `sector_size` - Size of each sector in bytes
143    /// * `capacity` - Maximum number of sectors to cache
144    pub fn new(
145        fat_start: usize,
146        fat_size: usize,
147        fat_count: usize,
148        sector_size: usize,
149        capacity: usize,
150    ) -> Self {
151        Self {
152            entries: Vec::with_capacity(capacity),
153            capacity,
154            sector_size,
155            fat_start,
156            fat_size,
157            fat_count,
158            access_counter: 0,
159            stats: CacheStats::default(),
160        }
161    }
162
163    /// Create a cache with default capacity.
164    pub fn with_default_capacity(
165        fat_start: usize,
166        fat_size: usize,
167        fat_count: usize,
168        sector_size: usize,
169    ) -> Self {
170        Self::new(
171            fat_start,
172            fat_size,
173            fat_count,
174            sector_size,
175            DEFAULT_CACHE_CAPACITY,
176        )
177    }
178
179    /// Get cache statistics.
180    pub fn stats(&self) -> CacheStats {
181        self.stats
182    }
183
184    /// Reset cache statistics.
185    pub fn reset_stats(&mut self) {
186        self.stats = CacheStats::default();
187    }
188
189    /// Clear the cache, optionally flushing dirty sectors first.
190    pub fn clear<T: Read + Write + Seek>(&mut self, writer: Option<&mut T>) -> Result<()> {
191        if let Some(w) = writer {
192            self.flush(w)?;
193        }
194        self.entries.clear();
195        Ok(())
196    }
197
198    /// Get the number of cached sectors.
199    pub fn len(&self) -> usize {
200        self.entries.len()
201    }
202
203    /// Check if the cache is empty.
204    pub fn is_empty(&self) -> bool {
205        self.entries.is_empty()
206    }
207
208    /// Flush all dirty sectors to disk.
209    pub fn flush<T: Write + Seek>(&mut self, writer: &mut T) -> Result<()> {
210        let fat_start = self.fat_start;
211        let fat_size = self.fat_size;
212        let fat_count = self.fat_count;
213        let sector_size = self.sector_size;
214
215        for entry in &mut self.entries {
216            if entry.dirty {
217                // Write to all FAT copies
218                for i in 0..fat_count {
219                    let offset = fat_start + i * fat_size + entry.sector * sector_size;
220                    writer.seek(SeekFrom::Start(offset as u64))?;
221                    writer.write_all(&entry.data)?;
222                }
223                entry.dirty = false;
224                self.stats.dirty_writes += 1;
225            }
226        }
227        Ok(())
228    }
229
230    /// Find a cached sector by sector number.
231    fn find_sector(&mut self, sector: usize) -> Option<usize> {
232        for (i, entry) in self.entries.iter().enumerate() {
233            if entry.sector == sector {
234                return Some(i);
235            }
236        }
237        None
238    }
239
240    /// Read a sector into the cache. Read-only path: never marks the entry
241    /// dirty, never needs a writer. Returns [`Error::CacheDirtyEviction`]
242    /// if the cache is at capacity and *every* resident sector is dirty —
243    /// the caller must `flush()` first because dropping a dirty entry would
244    /// silently lose unwritten data.
245    fn get_sector<T: Read + Seek>(&mut self, reader: &mut T, sector: usize) -> Result<&[u8]> {
246        self.access_counter += 1;
247
248        // Check if already cached
249        if let Some(idx) = self.find_sector(sector) {
250            self.stats.hits += 1;
251            self.entries[idx].access_count = self.access_counter;
252            return Ok(&self.entries[idx].data);
253        }
254
255        self.stats.misses += 1;
256
257        // Evict before reading: read-paths can only evict clean entries, so
258        // the eviction may fail with CacheDirtyEviction. Returning early
259        // means we never read a sector we then can't park.
260        if self.entries.len() >= self.capacity {
261            self.evict_lru_clean()?;
262        }
263
264        // Load from disk
265        let mut data = alloc::vec![0u8; self.sector_size];
266        let offset = self.fat_start + sector * self.sector_size;
267        reader.seek(SeekFrom::Start(offset as u64))?;
268        reader.read_exact(&mut data)?;
269
270        self.entries.push(CacheEntry {
271            sector,
272            data,
273            dirty: false,
274            access_count: self.access_counter,
275        });
276
277        Ok(&self.entries.last().unwrap().data)
278    }
279
280    /// Get a mutable reference to a sector, loading it if necessary.
281    ///
282    /// Write path: requires `Read + Write + Seek` so a dirty LRU can be
283    /// flushed (write-through to every FAT copy) before being evicted.
284    /// Marks the returned entry dirty.
285    fn get_sector_mut<T: Read + Write + Seek>(
286        &mut self,
287        io: &mut T,
288        sector: usize,
289    ) -> Result<&mut [u8]> {
290        self.access_counter += 1;
291
292        if let Some(idx) = self.find_sector(sector) {
293            self.stats.hits += 1;
294            self.entries[idx].access_count = self.access_counter;
295            self.entries[idx].dirty = true;
296            return Ok(&mut self.entries[idx].data);
297        }
298
299        self.stats.misses += 1;
300
301        // Evict before reading: write-paths can flush dirty entries on
302        // eviction, so this never fails for cache-pressure reasons (only if
303        // the underlying writer fails).
304        if self.entries.len() >= self.capacity {
305            self.evict_lru_flush(io)?;
306        }
307
308        let mut data = alloc::vec![0u8; self.sector_size];
309        let offset = self.fat_start + sector * self.sector_size;
310        io.seek(SeekFrom::Start(offset as u64))?;
311        io.read_exact(&mut data)?;
312
313        self.entries.push(CacheEntry {
314            sector,
315            data,
316            dirty: true,
317            access_count: self.access_counter,
318        });
319
320        let idx = self.entries.len() - 1;
321        Ok(&mut self.entries[idx].data)
322    }
323
324    /// Find the index of the LRU entry, or `None` if the cache is empty.
325    fn find_lru_index(&self) -> Option<usize> {
326        let mut lru_idx = None;
327        let mut lru_count = u64::MAX;
328        for (i, entry) in self.entries.iter().enumerate() {
329            if entry.access_count < lru_count {
330                lru_count = entry.access_count;
331                lru_idx = Some(i);
332            }
333        }
334        lru_idx
335    }
336
337    /// Find the LRU among *non-dirty* entries, or `None` if all entries are
338    /// dirty (or the cache is empty).
339    fn find_lru_clean_index(&self) -> Option<usize> {
340        let mut lru_idx = None;
341        let mut lru_count = u64::MAX;
342        for (i, entry) in self.entries.iter().enumerate() {
343            if !entry.dirty && entry.access_count < lru_count {
344                lru_count = entry.access_count;
345                lru_idx = Some(i);
346            }
347        }
348        lru_idx
349    }
350
351    /// Read-path eviction: drops the LRU non-dirty entry. If every entry is
352    /// dirty, returns [`Error::CacheDirtyEviction`] — read paths can't
353    /// safely drop unwritten data, and there's no writer available here to
354    /// flush it.
355    fn evict_lru_clean(&mut self) -> Result<()> {
356        if self.entries.is_empty() {
357            return Ok(());
358        }
359        match self.find_lru_clean_index() {
360            Some(idx) => {
361                self.entries.swap_remove(idx);
362                self.stats.evictions += 1;
363                Ok(())
364            }
365            None => {
366                // Every resident entry is dirty. Surface the LRU's sector
367                // so the caller has a useful breadcrumb (which sector they
368                // need to flush before retrying).
369                let lru = self.find_lru_index().expect("non-empty above");
370                Err(Error::CacheDirtyEviction {
371                    sector: self.entries[lru].sector as u32,
372                })
373            }
374        }
375    }
376
377    /// Write-path eviction: flushes the LRU entry to every FAT copy if
378    /// dirty, then removes it. Always succeeds unless the underlying writer
379    /// fails.
380    fn evict_lru_flush<T: Write + Seek>(&mut self, writer: &mut T) -> Result<()> {
381        let Some(idx) = self.find_lru_index() else {
382            return Ok(());
383        };
384        if self.entries[idx].dirty {
385            // Mirror `flush`'s loop: write to every FAT copy so backup FATs
386            // stay consistent with the primary.
387            for copy in 0..self.fat_count {
388                let offset = self.fat_start
389                    + copy * self.fat_size
390                    + self.entries[idx].sector * self.sector_size;
391                writer.seek(SeekFrom::Start(offset as u64))?;
392                writer.write_all(&self.entries[idx].data)?;
393            }
394            self.entries[idx].dirty = false;
395            self.stats.dirty_writes += 1;
396        }
397        self.entries.swap_remove(idx);
398        self.stats.evictions += 1;
399        Ok(())
400    }
401
402    // =========================================================================
403    // FAT12 cached operations
404    // =========================================================================
405
406    /// Read a FAT12 entry using the cache.
407    pub fn read_fat12_entry<T: Read + Seek>(
408        &mut self,
409        reader: &mut T,
410        cluster: usize,
411    ) -> Result<u16> {
412        let sector_size = self.sector_size;
413
414        // FAT12 packs 2 entries into 3 bytes
415        let byte_offset = (cluster * 3) / 2;
416        let sector = byte_offset / sector_size;
417        let offset_in_sector = byte_offset % sector_size;
418
419        // We might need to read from two sectors if the entry spans a boundary
420        let bytes = if offset_in_sector + 1 < sector_size {
421            // Entry is within one sector
422            let data = self.get_sector(reader, sector)?;
423            [data[offset_in_sector], data[offset_in_sector + 1]]
424        } else {
425            // Entry spans two sectors - need to load the next sector too
426            let first_byte = {
427                let data = self.get_sector(reader, sector)?;
428                data[offset_in_sector]
429            };
430
431            let second_byte = {
432                let next_sector_data = self.get_sector(reader, sector + 1)?;
433                next_sector_data[0]
434            };
435            [first_byte, second_byte]
436        };
437
438        // FAT12 entry layout:
439        // If cluster N is even: entry = (bytes[1] & 0x0F) << 8 | bytes[0]
440        // If cluster N is odd:  entry = bytes[1] << 4 | (bytes[0] >> 4)
441        let value = if cluster.is_multiple_of(2) {
442            u16::from(bytes[0]) | (u16::from(bytes[1] & 0x0F) << 8)
443        } else {
444            (u16::from(bytes[0]) >> 4) | (u16::from(bytes[1]) << 4)
445        };
446
447        Ok(value)
448    }
449
450    /// Write a FAT12 entry through the cache.
451    ///
452    /// Requires `Read + Write + Seek` because the cache is write-back: an
453    /// LRU eviction during this call must flush a dirty sector to disk
454    /// before discarding it.
455    pub fn write_fat12_entry<T: Read + Write + Seek>(
456        &mut self,
457        io: &mut T,
458        cluster: usize,
459        value: u16,
460    ) -> Result<()> {
461        let sector_size = self.sector_size;
462
463        let byte_offset = (cluster * 3) / 2;
464        let sector = byte_offset / sector_size;
465        let offset_in_sector = byte_offset % sector_size;
466
467        if offset_in_sector + 1 < sector_size {
468            // Entry is within one sector
469            let data = self.get_sector_mut(io, sector)?;
470
471            if cluster.is_multiple_of(2) {
472                data[offset_in_sector] = value as u8;
473                data[offset_in_sector + 1] =
474                    (data[offset_in_sector + 1] & 0xF0) | ((value >> 8) as u8 & 0x0F);
475            } else {
476                data[offset_in_sector] = (data[offset_in_sector] & 0x0F) | ((value << 4) as u8);
477                data[offset_in_sector + 1] = (value >> 4) as u8;
478            }
479        } else {
480            // Entry spans two sectors
481            {
482                let data = self.get_sector_mut(io, sector)?;
483                if cluster.is_multiple_of(2) {
484                    data[offset_in_sector] = value as u8;
485                } else {
486                    data[offset_in_sector] = (data[offset_in_sector] & 0x0F) | ((value << 4) as u8);
487                }
488            }
489
490            {
491                let data = self.get_sector_mut(io, sector + 1)?;
492                if cluster.is_multiple_of(2) {
493                    data[0] = (data[0] & 0xF0) | ((value >> 8) as u8 & 0x0F);
494                } else {
495                    data[0] = (value >> 4) as u8;
496                }
497            }
498        }
499
500        Ok(())
501    }
502
503    // =========================================================================
504    // FAT16 cached operations
505    // =========================================================================
506
507    /// Read a FAT16 entry using the cache.
508    pub fn read_fat16_entry<T: Read + Seek>(
509        &mut self,
510        reader: &mut T,
511        cluster: usize,
512    ) -> Result<u16> {
513        let sector_size = self.sector_size;
514        let byte_offset = cluster * 2;
515        let sector = byte_offset / sector_size;
516        let offset_in_sector = byte_offset % sector_size;
517
518        let data = self.get_sector(reader, sector)?;
519        let value = u16::from_le_bytes([data[offset_in_sector], data[offset_in_sector + 1]]);
520
521        Ok(value)
522    }
523
524    /// Write a FAT16 entry through the cache.
525    ///
526    /// Requires `Read + Write + Seek` because eviction may flush a dirty
527    /// sector before discarding it (see [`Self::write_fat12_entry`]).
528    pub fn write_fat16_entry<T: Read + Write + Seek>(
529        &mut self,
530        io: &mut T,
531        cluster: usize,
532        value: u16,
533    ) -> Result<()> {
534        let sector_size = self.sector_size;
535        let byte_offset = cluster * 2;
536        let sector = byte_offset / sector_size;
537        let offset_in_sector = byte_offset % sector_size;
538
539        let data = self.get_sector_mut(io, sector)?;
540        let bytes = value.to_le_bytes();
541        data[offset_in_sector] = bytes[0];
542        data[offset_in_sector + 1] = bytes[1];
543
544        Ok(())
545    }
546
547    // =========================================================================
548    // FAT32 cached operations
549    // =========================================================================
550
551    /// Read a FAT32 entry using the cache.
552    pub fn read_fat32_entry<T: Read + Seek>(
553        &mut self,
554        reader: &mut T,
555        cluster: usize,
556    ) -> Result<u32> {
557        let sector_size = self.sector_size;
558        let byte_offset = cluster * 4;
559        let sector = byte_offset / sector_size;
560        let offset_in_sector = byte_offset % sector_size;
561
562        let data = self.get_sector(reader, sector)?;
563        let value = u32::from_le_bytes([
564            data[offset_in_sector],
565            data[offset_in_sector + 1],
566            data[offset_in_sector + 2],
567            data[offset_in_sector + 3],
568        ]);
569
570        Ok(value)
571    }
572
573    /// Write a FAT32 entry through the cache.
574    ///
575    /// Requires `Read + Write + Seek` because eviction may flush a dirty
576    /// sector before discarding it (see [`Self::write_fat12_entry`]).
577    pub fn write_fat32_entry<T: Read + Write + Seek>(
578        &mut self,
579        io: &mut T,
580        cluster: usize,
581        value: u32,
582    ) -> Result<()> {
583        let sector_size = self.sector_size;
584        let byte_offset = cluster * 4;
585        let sector = byte_offset / sector_size;
586        let offset_in_sector = byte_offset % sector_size;
587
588        let data = self.get_sector_mut(io, sector)?;
589        let existing = u32::from_le_bytes([
590            data[offset_in_sector],
591            data[offset_in_sector + 1],
592            data[offset_in_sector + 2],
593            data[offset_in_sector + 3],
594        ]);
595        let bytes = ((existing & 0xF000_0000) | (value & 0x0FFF_FFFF)).to_le_bytes();
596        data[offset_in_sector] = bytes[0];
597        data[offset_in_sector + 1] = bytes[1];
598        data[offset_in_sector + 2] = bytes[2];
599        data[offset_in_sector + 3] = bytes[3];
600
601        Ok(())
602    }
603}
604
605/// Cached FAT operations that work with any FAT type.
606pub struct CachedFat<'a> {
607    cache: &'a mut FatSectorCache,
608    fat_type: FatType,
609    max_cluster: u32,
610}
611
612impl<'a> CachedFat<'a> {
613    /// Create a new cached FAT wrapper.
614    pub fn new(cache: &'a mut FatSectorCache, fat: &Fat) -> Self {
615        let (fat_type, max_cluster) = match fat {
616            Fat::Fat12(f) => (FatType::Fat12, f.max_cluster() as u32),
617            Fat::Fat16(f) => (FatType::Fat16, f.max_cluster() as u32),
618            Fat::Fat32(f) => (FatType::Fat32, f.max_cluster()),
619        };
620        Self {
621            cache,
622            fat_type,
623            max_cluster,
624        }
625    }
626
627    /// Get the next cluster in a chain using the cache.
628    pub fn next_cluster<T: Read + Seek>(
629        &mut self,
630        reader: &mut T,
631        cluster: usize,
632    ) -> Result<Option<u32>> {
633        match self.fat_type {
634            FatType::Fat12 => {
635                let entry = self.cache.read_fat12_entry(reader, cluster)? & 0x0FFF;
636                if entry >= 0x0FF8 {
637                    Ok(None) // End of chain
638                } else if entry == 0x0FF7 {
639                    Err(Error::BadCluster {
640                        cluster: cluster as u32,
641                    })
642                } else if entry < 2 || entry as u32 > self.max_cluster {
643                    Err(Error::ClusterOutOfBounds {
644                        cluster: entry as u32,
645                        max: self.max_cluster,
646                    })
647                } else {
648                    Ok(Some(entry as u32))
649                }
650            }
651            FatType::Fat16 => {
652                let entry = self.cache.read_fat16_entry(reader, cluster)?;
653                if entry >= 0xFFF8 {
654                    Ok(None) // End of chain
655                } else if entry == 0xFFF7 {
656                    Err(Error::BadCluster {
657                        cluster: cluster as u32,
658                    })
659                } else if entry < 2 || entry as u32 > self.max_cluster {
660                    Err(Error::ClusterOutOfBounds {
661                        cluster: entry as u32,
662                        max: self.max_cluster,
663                    })
664                } else {
665                    Ok(Some(entry as u32))
666                }
667            }
668            FatType::Fat32 => {
669                let entry = self.cache.read_fat32_entry(reader, cluster)? & 0x0FFF_FFFF;
670                if entry >= 0x0FFF_FFF8 {
671                    Ok(None) // End of chain
672                } else if entry == 0x0FFF_FFF7 {
673                    Err(Error::BadCluster {
674                        cluster: cluster as u32,
675                    })
676                } else if entry < 2 || entry > self.max_cluster {
677                    Err(Error::ClusterOutOfBounds {
678                        cluster: entry,
679                        max: self.max_cluster,
680                    })
681                } else {
682                    Ok(Some(entry))
683                }
684            }
685        }
686    }
687
688    /// Read the entire cluster chain starting from a cluster.
689    ///
690    /// This is efficient because it uses the cache to avoid repeated seeks.
691    /// On a chain longer than `max_cluster` clusters (only possible if the
692    /// FAT is corrupt and contains a loop), returns
693    /// [`Error::ClusterLoop`] rather than silently returning a
694    /// partial chain.
695    pub fn read_chain<T: Read + Seek>(
696        &mut self,
697        reader: &mut T,
698        start_cluster: u32,
699    ) -> Result<Vec<u32>> {
700        let mut chain = Vec::new();
701        let mut current = start_cluster;
702
703        // A healthy chain visits each cluster at most once, so it can't
704        // exceed `max_cluster - 1` clusters (clusters 0 and 1 are reserved).
705        // Anything larger means the chain has a loop.
706        let max_iterations = self.max_cluster as usize;
707
708        loop {
709            if current < 2 || current > self.max_cluster {
710                break;
711            }
712
713            chain.push(current);
714
715            if chain.len() > max_iterations {
716                return Err(Error::ClusterLoop { cluster: current });
717            }
718
719            match self.next_cluster(reader, current as usize)? {
720                Some(next) => current = next,
721                None => break,
722            }
723        }
724
725        Ok(chain)
726    }
727
728    /// Flush the cache to disk.
729    pub fn flush<T: Write + Seek>(&mut self, writer: &mut T) -> Result<()> {
730        self.cache.flush(writer)
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737
738    #[test]
739    fn test_cache_stats() {
740        let stats = CacheStats {
741            hits: 80,
742            misses: 20,
743            evictions: 5,
744            dirty_writes: 3,
745        };
746        assert!((stats.hit_ratio() - 0.8).abs() < 0.001);
747    }
748
749    #[test]
750    fn test_cache_stats_empty() {
751        let stats = CacheStats::default();
752        assert_eq!(stats.hit_ratio(), 0.0);
753    }
754}