Skip to main content

hadris_fat/tool/
analysis.rs

1//! Statistics and fragmentation analysis for FAT filesystems.
2
3use alloc::format;
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::ops::DerefMut;
7
8use super::super::{
9    dir::{DirectoryEntry, FatDir, FileEntry},
10    fat_table::{Fat, Fat12, Fat16, Fat32, FatType},
11    fs::FatVolume,
12    io::{Read, Seek},
13};
14use crate::error::Result;
15
16/// Statistics about a FAT filesystem.
17#[derive(Debug, Clone)]
18pub struct FatStatistics {
19    /// FAT type (FAT12, FAT16, or FAT32)
20    pub fat_type: FatType,
21    /// Total number of clusters in the filesystem
22    pub total_clusters: u32,
23    /// Number of free clusters
24    pub free_clusters: u32,
25    /// Number of used clusters
26    pub used_clusters: u32,
27    /// Number of bad clusters
28    pub bad_clusters: u32,
29    /// Number of reserved clusters (0 and 1)
30    pub reserved_clusters: u32,
31    /// Cluster size in bytes
32    pub cluster_size: usize,
33    /// Sector size in bytes
34    pub sector_size: usize,
35    /// Total filesystem capacity in bytes
36    pub total_capacity: u64,
37    /// Used space in bytes
38    pub used_space: u64,
39    /// Free space in bytes
40    pub free_space: u64,
41    /// Total number of files (not including directories)
42    pub file_count: u32,
43    /// Total number of directories
44    pub directory_count: u32,
45}
46
47impl FatStatistics {
48    /// Calculate the percentage of used space.
49    pub fn used_percentage(&self) -> f64 {
50        if self.total_capacity == 0 {
51            0.0
52        } else {
53            (self.used_space as f64 / self.total_capacity as f64) * 100.0
54        }
55    }
56
57    /// Calculate the percentage of free space.
58    pub fn free_percentage(&self) -> f64 {
59        if self.total_capacity == 0 {
60            0.0
61        } else {
62            (self.free_space as f64 / self.total_capacity as f64) * 100.0
63        }
64    }
65}
66
67/// State of a single cluster in the FAT.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum ClusterState {
70    /// Free cluster (value 0)
71    Free,
72    /// Reserved cluster (clusters 0 and 1, or reserved values)
73    Reserved,
74    /// Bad cluster
75    Bad,
76    /// Used cluster, contains next cluster number
77    Used(u32),
78    /// End of cluster chain
79    EndOfChain,
80}
81
82/// Information about file fragmentation.
83#[derive(Debug, Clone)]
84pub struct FileFragmentInfo {
85    /// File path
86    pub path: String,
87    /// File size in bytes
88    pub size: usize,
89    /// Number of fragments (contiguous extents)
90    pub fragments: u32,
91    /// Starting cluster of first fragment
92    pub first_cluster: u32,
93}
94
95impl FileFragmentInfo {
96    /// Calculate the fragmentation ratio (1.0 = not fragmented, >1.0 = fragmented).
97    pub fn fragmentation_ratio(&self, cluster_size: usize) -> f64 {
98        if self.size == 0 {
99            return 1.0;
100        }
101        let ideal_clusters = self.size.div_ceil(cluster_size);
102        if ideal_clusters == 0 {
103            return 1.0;
104        }
105        self.fragments as f64 / ideal_clusters as f64
106    }
107}
108
109/// Report on filesystem fragmentation.
110#[derive(Debug, Clone)]
111pub struct FragmentationReport {
112    /// Total number of files analyzed
113    pub total_files: u32,
114    /// Number of fragmented files (more than 1 fragment)
115    pub fragmented_files: u32,
116    /// Total number of fragments across all files
117    pub total_fragments: u32,
118    /// Files with the most fragmentation, sorted by fragment count descending
119    pub most_fragmented: Vec<FileFragmentInfo>,
120    /// Average fragments per file
121    pub average_fragments: f64,
122    /// Fragmentation percentage (fragmented files / total files * 100)
123    pub fragmentation_percentage: f64,
124}
125
126/// Extension trait for FatVolume providing analysis operations.
127pub trait FatAnalysisExt<DATA: Read + Seek> {
128    /// Gather statistics about the filesystem.
129    ///
130    /// This scans the FAT table to count free, used, and bad clusters,
131    /// and optionally scans the directory tree to count files and directories.
132    fn statistics(&self) -> Result<FatStatistics>;
133
134    /// Analyze filesystem fragmentation.
135    ///
136    /// This scans all files in the filesystem and reports on their fragmentation.
137    /// The `max_files` parameter limits how many of the most fragmented files
138    /// are included in the report (default: 10).
139    fn fragmentation_report(&self, max_files: usize) -> Result<FragmentationReport>;
140
141    /// Scan the FAT table and return the state of each cluster.
142    fn scan_fat(&self) -> Result<Vec<ClusterState>>;
143
144    /// Get the cluster chain for a file.
145    fn get_cluster_chain(&self, first_cluster: u32) -> Result<Vec<u32>>;
146}
147
148impl<DATA: Read + Seek> FatAnalysisExt<DATA> for FatVolume<DATA> {
149    fn statistics(&self) -> Result<FatStatistics> {
150        let fat_type = self.fat_type();
151        let cluster_size = self.info.cluster_size;
152        let mut data = self.data.lock();
153        let sector_size = data.sector_size;
154
155        // Scan FAT to count cluster states
156        let mut free_clusters = 0u32;
157        let mut used_clusters = 0u32;
158        let mut bad_clusters = 0u32;
159        let total_clusters = self.info.max_cluster;
160
161        // Skip clusters 0 and 1 (reserved)
162        for cluster in 2..=total_clusters {
163            let state = self.read_cluster_state(data.deref_mut(), cluster)?;
164            match state {
165                ClusterState::Free => free_clusters += 1,
166                ClusterState::Used(_) | ClusterState::EndOfChain => used_clusters += 1,
167                ClusterState::Bad => bad_clusters += 1,
168                ClusterState::Reserved => {}
169            }
170        }
171
172        drop(data);
173
174        // Count files and directories by scanning directory tree
175        let (file_count, directory_count) = self.count_entries()?;
176
177        let data_clusters = total_clusters - 1; // Exclude cluster 1 (cluster 0 is reserved)
178        let total_capacity = data_clusters as u64 * cluster_size as u64;
179        let used_space = used_clusters as u64 * cluster_size as u64;
180        let free_space = free_clusters as u64 * cluster_size as u64;
181
182        Ok(FatStatistics {
183            fat_type,
184            total_clusters,
185            free_clusters,
186            used_clusters,
187            bad_clusters,
188            reserved_clusters: 2, // Clusters 0 and 1
189            cluster_size,
190            sector_size,
191            total_capacity,
192            used_space,
193            free_space,
194            file_count,
195            directory_count,
196        })
197    }
198
199    fn fragmentation_report(&self, max_files: usize) -> Result<FragmentationReport> {
200        let mut all_files = Vec::new();
201        self.collect_files_recursive(&self.root_dir(), String::new(), &mut all_files)?;
202
203        let mut total_fragments = 0u32;
204        let mut fragmented_files = 0u32;
205
206        // Analyze each file
207        let mut file_infos: Vec<FileFragmentInfo> = Vec::new();
208        for (path, entry) in &all_files {
209            if entry.is_directory() {
210                continue;
211            }
212
213            let first_cluster = entry.cluster().0 as u32;
214            if first_cluster < 2 {
215                // Empty file
216                continue;
217            }
218
219            let chain = self.get_cluster_chain(first_cluster)?;
220            let fragments = count_fragments(&chain);
221
222            total_fragments += fragments;
223            if fragments > 1 {
224                fragmented_files += 1;
225            }
226
227            file_infos.push(FileFragmentInfo {
228                path: path.clone(),
229                size: entry.len() as usize,
230                fragments,
231                first_cluster,
232            });
233        }
234
235        // Sort by fragment count descending
236        file_infos.sort_by(|a, b| b.fragments.cmp(&a.fragments));
237
238        // Take the most fragmented files
239        let most_fragmented: Vec<FileFragmentInfo> =
240            file_infos.into_iter().take(max_files).collect();
241
242        let total_files = all_files.iter().filter(|(_, e)| e.is_file()).count() as u32;
243        let average_fragments = if total_files > 0 {
244            total_fragments as f64 / total_files as f64
245        } else {
246            0.0
247        };
248        let fragmentation_percentage = if total_files > 0 {
249            (fragmented_files as f64 / total_files as f64) * 100.0
250        } else {
251            0.0
252        };
253
254        Ok(FragmentationReport {
255            total_files,
256            fragmented_files,
257            total_fragments,
258            most_fragmented,
259            average_fragments,
260            fragmentation_percentage,
261        })
262    }
263
264    fn scan_fat(&self) -> Result<Vec<ClusterState>> {
265        let mut data = self.data.lock();
266        let total_clusters = self.info.max_cluster;
267        let mut states = Vec::with_capacity(total_clusters as usize + 1);
268
269        // Cluster 0 and 1 are reserved
270        states.push(ClusterState::Reserved);
271        states.push(ClusterState::Reserved);
272
273        for cluster in 2..=total_clusters {
274            let state = self.read_cluster_state(data.deref_mut(), cluster)?;
275            states.push(state);
276        }
277
278        Ok(states)
279    }
280
281    fn get_cluster_chain(&self, first_cluster: u32) -> Result<Vec<u32>> {
282        let mut chain = Vec::new();
283        let mut current = first_cluster;
284        let mut data = self.data.lock();
285        let max_clusters = self.info.max_cluster;
286
287        // Prevent infinite loops
288        let mut iterations = 0usize;
289        let max_iterations = max_clusters as usize;
290
291        while current >= 2 && current <= max_clusters {
292            chain.push(current);
293            iterations += 1;
294
295            if iterations > max_iterations {
296                // Likely a loop in the FAT
297                break;
298            }
299
300            match self.fat.next_cluster(data.deref_mut(), current as usize)? {
301                Some(next) => current = next,
302                None => break,
303            }
304        }
305
306        Ok(chain)
307    }
308}
309
310// Helper implementations
311impl<DATA: Read + Seek> FatVolume<DATA> {
312    /// Read the state of a single cluster from the FAT.
313    fn read_cluster_state<T: Read + Seek>(
314        &self,
315        reader: &mut T,
316        cluster: u32,
317    ) -> Result<ClusterState> {
318        match &self.fat {
319            Fat::Fat12(fat12) => {
320                let entry = fat12.read_entry(reader, cluster as usize)?;
321                Ok(classify_fat12_entry(entry))
322            }
323            Fat::Fat16(fat16) => {
324                let entry = fat16.read_entry(reader, cluster as usize)?;
325                Ok(classify_fat16_entry(entry))
326            }
327            Fat::Fat32(fat32) => {
328                let entry = fat32.read_entry(reader, cluster as usize)?;
329                Ok(classify_fat32_entry(entry))
330            }
331        }
332    }
333
334    /// Count files and directories in the filesystem.
335    fn count_entries(&self) -> Result<(u32, u32)> {
336        let mut files = 0u32;
337        let mut dirs = 0u32;
338        self.count_entries_recursive(&self.root_dir(), &mut files, &mut dirs)?;
339        Ok((files, dirs))
340    }
341
342    fn count_entries_recursive<'a>(
343        &'a self,
344        dir: &FatDir<'a, DATA>,
345        files: &mut u32,
346        dirs: &mut u32,
347    ) -> Result<()> {
348        for entry in dir.entries() {
349            let entry = entry?;
350            let DirectoryEntry::Entry(file_entry) = entry;
351
352            let name = file_entry.name();
353            if name == "." || name == ".." {
354                continue;
355            }
356
357            if file_entry.is_directory() {
358                *dirs += 1;
359                let subdir = FatDir {
360                    data: self,
361                    cluster: file_entry.cluster(),
362                    fixed_root: None,
363                };
364                self.count_entries_recursive(&subdir, files, dirs)?;
365            } else {
366                *files += 1;
367            }
368        }
369        Ok(())
370    }
371
372    /// Collect all files recursively with their paths.
373    fn collect_files_recursive<'a>(
374        &'a self,
375        dir: &FatDir<'a, DATA>,
376        path_prefix: String,
377        files: &mut Vec<(String, FileEntry)>,
378    ) -> Result<()> {
379        for entry in dir.entries() {
380            let entry = entry?;
381            let DirectoryEntry::Entry(file_entry) = entry;
382
383            let name = file_entry.name();
384            if name == "." || name == ".." {
385                continue;
386            }
387
388            let full_path = if path_prefix.is_empty() {
389                format!("/{name}")
390            } else {
391                format!("{path_prefix}/{name}")
392            };
393
394            if file_entry.is_directory() {
395                let subdir = FatDir {
396                    data: self,
397                    cluster: file_entry.cluster(),
398                    fixed_root: None,
399                };
400                self.collect_files_recursive(&subdir, full_path, files)?;
401            } else {
402                files.push((full_path, file_entry));
403            }
404        }
405        Ok(())
406    }
407}
408
409// FAT entry readers - these need to be added to the Fat12/16/32 implementations
410impl Fat12 {
411    /// Read a raw FAT12 entry.
412    pub fn read_entry<T: Read + Seek>(&self, reader: &mut T, cluster: usize) -> Result<u16> {
413        let byte_offset = self.entry_byte_offset(cluster);
414        reader.seek(super::super::io::SeekFrom::Start(byte_offset as u64))?;
415
416        let mut bytes = [0u8; 2];
417        reader.read_exact(&mut bytes)?;
418
419        let value = if cluster.is_multiple_of(2) {
420            u16::from(bytes[0]) | (u16::from(bytes[1] & 0x0F) << 8)
421        } else {
422            (u16::from(bytes[0]) >> 4) | (u16::from(bytes[1]) << 4)
423        };
424
425        Ok(value)
426    }
427}
428
429impl Fat16 {
430    /// Read a raw FAT16 entry.
431    pub fn read_entry<T: Read + Seek>(&self, reader: &mut T, cluster: usize) -> Result<u16> {
432        let offset = self.entry_offset(cluster);
433        reader.seek(super::super::io::SeekFrom::Start(offset as u64))?;
434
435        let mut bytes = [0u8; 2];
436        reader.read_exact(&mut bytes)?;
437
438        Ok(u16::from_le_bytes(bytes))
439    }
440}
441
442impl Fat32 {
443    /// Read a raw FAT32 entry.
444    pub fn read_entry<T: Read + Seek>(&self, reader: &mut T, cluster: usize) -> Result<u32> {
445        let offset = self.entry_offset(cluster);
446        reader.seek(super::super::io::SeekFrom::Start(offset as u64))?;
447
448        let mut bytes = [0u8; 4];
449        reader.read_exact(&mut bytes)?;
450
451        Ok(u32::from_le_bytes(bytes))
452    }
453}
454
455/// Classify a FAT12 entry value.
456fn classify_fat12_entry(entry: u16) -> ClusterState {
457    let masked = entry & 0x0FFF;
458    match masked {
459        0x000 => ClusterState::Free,
460        0x001 => ClusterState::Reserved,
461        0xFF7 => ClusterState::Bad,
462        0xFF8..=0xFFF => ClusterState::EndOfChain,
463        n => ClusterState::Used(n as u32),
464    }
465}
466
467/// Classify a FAT16 entry value.
468fn classify_fat16_entry(entry: u16) -> ClusterState {
469    match entry {
470        0x0000 => ClusterState::Free,
471        0x0001 => ClusterState::Reserved,
472        0xFFF7 => ClusterState::Bad,
473        0xFFF8..=0xFFFF => ClusterState::EndOfChain,
474        n => ClusterState::Used(n as u32),
475    }
476}
477
478/// Classify a FAT32 entry value.
479fn classify_fat32_entry(entry: u32) -> ClusterState {
480    let masked = entry & 0x0FFF_FFFF;
481    match masked {
482        0x0000_0000 => ClusterState::Free,
483        0x0000_0001 => ClusterState::Reserved,
484        0x0FFF_FFF7 => ClusterState::Bad,
485        0x0FFF_FFF8..=0x0FFF_FFFF => ClusterState::EndOfChain,
486        n => ClusterState::Used(n),
487    }
488}
489
490/// Count the number of contiguous fragments in a cluster chain.
491fn count_fragments(chain: &[u32]) -> u32 {
492    if chain.is_empty() {
493        return 0;
494    }
495    if chain.len() == 1 {
496        return 1;
497    }
498
499    let mut fragments = 1u32;
500    for window in chain.windows(2) {
501        if window[1] != window[0] + 1 {
502            fragments += 1;
503        }
504    }
505    fragments
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    #[test]
513    fn test_count_fragments() {
514        assert_eq!(count_fragments(&[]), 0);
515        assert_eq!(count_fragments(&[5]), 1);
516        assert_eq!(count_fragments(&[5, 6, 7]), 1);
517        assert_eq!(count_fragments(&[5, 6, 10]), 2);
518        assert_eq!(count_fragments(&[5, 10, 15]), 3);
519        assert_eq!(count_fragments(&[5, 6, 7, 10, 11, 15]), 3);
520    }
521
522    #[test]
523    fn test_classify_fat12() {
524        assert_eq!(classify_fat12_entry(0x000), ClusterState::Free);
525        assert_eq!(classify_fat12_entry(0x001), ClusterState::Reserved);
526        assert_eq!(classify_fat12_entry(0xFF7), ClusterState::Bad);
527        assert_eq!(classify_fat12_entry(0xFF8), ClusterState::EndOfChain);
528        assert_eq!(classify_fat12_entry(0xFFF), ClusterState::EndOfChain);
529        assert_eq!(classify_fat12_entry(0x123), ClusterState::Used(0x123));
530    }
531
532    #[test]
533    fn test_classify_fat16() {
534        assert_eq!(classify_fat16_entry(0x0000), ClusterState::Free);
535        assert_eq!(classify_fat16_entry(0x0001), ClusterState::Reserved);
536        assert_eq!(classify_fat16_entry(0xFFF7), ClusterState::Bad);
537        assert_eq!(classify_fat16_entry(0xFFF8), ClusterState::EndOfChain);
538        assert_eq!(classify_fat16_entry(0xFFFF), ClusterState::EndOfChain);
539        assert_eq!(classify_fat16_entry(0x1234), ClusterState::Used(0x1234));
540    }
541
542    #[test]
543    fn test_classify_fat32() {
544        assert_eq!(classify_fat32_entry(0x0000_0000), ClusterState::Free);
545        assert_eq!(classify_fat32_entry(0x0000_0001), ClusterState::Reserved);
546        assert_eq!(classify_fat32_entry(0x0FFF_FFF7), ClusterState::Bad);
547        assert_eq!(classify_fat32_entry(0x0FFF_FFF8), ClusterState::EndOfChain);
548        assert_eq!(classify_fat32_entry(0x0FFF_FFFF), ClusterState::EndOfChain);
549        assert_eq!(
550            classify_fat32_entry(0x0012_3456),
551            ClusterState::Used(0x0012_3456)
552        );
553    }
554}