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/// Maximum directory nesting depth for the recursive tree walks. A corrupt
127/// image can make a directory (transitively) contain itself; without a cap
128/// the recursion overflows the stack. Matches the 64-level depth guard used
129/// by the fuzz harnesses.
130pub(super) const MAX_DIRECTORY_DEPTH: u32 = 64;
131
132/// Extension trait for FatVolume providing analysis operations.
133pub trait FatAnalysisExt<DATA: Read + Seek> {
134    /// Gather statistics about the filesystem.
135    ///
136    /// This scans the FAT table to count free, used, and bad clusters,
137    /// and optionally scans the directory tree to count files and directories.
138    fn statistics(&self) -> Result<FatStatistics>;
139
140    /// Analyze filesystem fragmentation.
141    ///
142    /// This scans all files in the filesystem and reports on their fragmentation.
143    /// The `max_files` parameter limits how many of the most fragmented files
144    /// are included in the report (default: 10).
145    fn fragmentation_report(&self, max_files: usize) -> Result<FragmentationReport>;
146
147    /// Scan the FAT table and return the state of each cluster.
148    fn scan_fat(&self) -> Result<Vec<ClusterState>>;
149
150    /// Get the cluster chain for a file.
151    fn get_cluster_chain(&self, first_cluster: u32) -> Result<Vec<u32>>;
152}
153
154impl<DATA: Read + Seek> FatAnalysisExt<DATA> for FatVolume<DATA> {
155    fn statistics(&self) -> Result<FatStatistics> {
156        let fat_type = self.fat_type();
157        let cluster_size = self.info.cluster_size;
158        let mut data = self.data.lock();
159        let sector_size = data.sector_size;
160
161        // Scan FAT to count cluster states
162        let mut free_clusters = 0u32;
163        let mut used_clusters = 0u32;
164        let mut bad_clusters = 0u32;
165        let total_clusters = self.info.max_cluster;
166
167        // Skip clusters 0 and 1 (reserved)
168        for cluster in 2..=total_clusters {
169            let state = self.read_cluster_state(data.deref_mut(), cluster)?;
170            match state {
171                ClusterState::Free => free_clusters += 1,
172                ClusterState::Used(_) | ClusterState::EndOfChain => used_clusters += 1,
173                ClusterState::Bad => bad_clusters += 1,
174                ClusterState::Reserved => {}
175            }
176        }
177
178        drop(data);
179
180        // Count files and directories by scanning directory tree
181        let (file_count, directory_count) = self.count_entries()?;
182
183        let data_clusters = total_clusters - 1; // Exclude cluster 1 (cluster 0 is reserved)
184        let total_capacity = data_clusters as u64 * cluster_size as u64;
185        let used_space = used_clusters as u64 * cluster_size as u64;
186        let free_space = free_clusters as u64 * cluster_size as u64;
187
188        Ok(FatStatistics {
189            fat_type,
190            total_clusters,
191            free_clusters,
192            used_clusters,
193            bad_clusters,
194            reserved_clusters: 2, // Clusters 0 and 1
195            cluster_size,
196            sector_size,
197            total_capacity,
198            used_space,
199            free_space,
200            file_count,
201            directory_count,
202        })
203    }
204
205    fn fragmentation_report(&self, max_files: usize) -> Result<FragmentationReport> {
206        let mut all_files = Vec::new();
207        self.collect_files_recursive(&self.root_dir(), String::new(), 0, &mut all_files)?;
208
209        let mut total_fragments = 0u32;
210        let mut fragmented_files = 0u32;
211
212        // Analyze each file
213        let mut file_infos: Vec<FileFragmentInfo> = Vec::new();
214        for (path, entry) in &all_files {
215            if entry.is_directory() {
216                continue;
217            }
218
219            let first_cluster = entry.cluster().0 as u32;
220            if first_cluster < 2 {
221                // Empty file
222                continue;
223            }
224
225            let chain = self.get_cluster_chain(first_cluster)?;
226            let fragments = count_fragments(&chain);
227
228            total_fragments += fragments;
229            if fragments > 1 {
230                fragmented_files += 1;
231            }
232
233            file_infos.push(FileFragmentInfo {
234                path: path.clone(),
235                size: entry.len() as usize,
236                fragments,
237                first_cluster,
238            });
239        }
240
241        // Sort by fragment count descending
242        file_infos.sort_by(|a, b| b.fragments.cmp(&a.fragments));
243
244        // Take the most fragmented files
245        let most_fragmented: Vec<FileFragmentInfo> =
246            file_infos.into_iter().take(max_files).collect();
247
248        let total_files = all_files.iter().filter(|(_, e)| e.is_file()).count() as u32;
249        let average_fragments = if total_files > 0 {
250            total_fragments as f64 / total_files as f64
251        } else {
252            0.0
253        };
254        let fragmentation_percentage = if total_files > 0 {
255            (fragmented_files as f64 / total_files as f64) * 100.0
256        } else {
257            0.0
258        };
259
260        Ok(FragmentationReport {
261            total_files,
262            fragmented_files,
263            total_fragments,
264            most_fragmented,
265            average_fragments,
266            fragmentation_percentage,
267        })
268    }
269
270    fn scan_fat(&self) -> Result<Vec<ClusterState>> {
271        let mut data = self.data.lock();
272        let total_clusters = self.info.max_cluster;
273        // `total_clusters` derives from the untrusted BPB total_sectors, so a
274        // corrupt image can claim ~4 billion clusters on a tiny image. Cap the
275        // up-front reservation (same 16 MiB idiom as `read_to_vec`); the Vec
276        // still grows as real entries are scanned.
277        const MAX_PREALLOC_BYTES: usize = 16 * 1024 * 1024;
278        let prealloc = (MAX_PREALLOC_BYTES / core::mem::size_of::<ClusterState>())
279            .min(total_clusters as usize + 1);
280        let mut states = Vec::with_capacity(prealloc);
281
282        // Cluster 0 and 1 are reserved
283        states.push(ClusterState::Reserved);
284        states.push(ClusterState::Reserved);
285
286        for cluster in 2..=total_clusters {
287            let state = self.read_cluster_state(data.deref_mut(), cluster)?;
288            states.push(state);
289        }
290
291        Ok(states)
292    }
293
294    fn get_cluster_chain(&self, first_cluster: u32) -> Result<Vec<u32>> {
295        let mut chain = Vec::new();
296        let mut current = first_cluster;
297        let mut data = self.data.lock();
298        let max_clusters = self.info.max_cluster;
299
300        // Prevent infinite loops
301        let mut iterations = 0usize;
302        let max_iterations = max_clusters as usize;
303
304        while current >= 2 && current <= max_clusters {
305            chain.push(current);
306            iterations += 1;
307
308            if iterations > max_iterations {
309                // Likely a loop in the FAT
310                break;
311            }
312
313            match self.fat.next_cluster(data.deref_mut(), current as usize)? {
314                Some(next) => current = next,
315                None => break,
316            }
317        }
318
319        Ok(chain)
320    }
321}
322
323// Helper implementations
324impl<DATA: Read + Seek> FatVolume<DATA> {
325    /// Read the state of a single cluster from the FAT.
326    fn read_cluster_state<T: Read + Seek>(
327        &self,
328        reader: &mut T,
329        cluster: u32,
330    ) -> Result<ClusterState> {
331        match &self.fat {
332            Fat::Fat12(fat12) => {
333                let entry = fat12.read_entry(reader, cluster as usize)?;
334                Ok(classify_fat12_entry(entry))
335            }
336            Fat::Fat16(fat16) => {
337                let entry = fat16.read_entry(reader, cluster as usize)?;
338                Ok(classify_fat16_entry(entry))
339            }
340            Fat::Fat32(fat32) => {
341                let entry = fat32.read_entry(reader, cluster as usize)?;
342                Ok(classify_fat32_entry(entry))
343            }
344        }
345    }
346
347    /// Count files and directories in the filesystem.
348    fn count_entries(&self) -> Result<(u32, u32)> {
349        let mut files = 0u32;
350        let mut dirs = 0u32;
351        self.count_entries_recursive(&self.root_dir(), 0, &mut files, &mut dirs)?;
352        Ok((files, dirs))
353    }
354
355    fn count_entries_recursive<'a>(
356        &'a self,
357        dir: &FatDir<'a, DATA>,
358        depth: u32,
359        files: &mut u32,
360        dirs: &mut u32,
361    ) -> Result<()> {
362        if depth > MAX_DIRECTORY_DEPTH {
363            return Err(crate::error::Error::CorruptFilesystem {
364                context: "directory nesting depth limit exceeded",
365            });
366        }
367        for entry in dir.entries() {
368            let entry = entry?;
369            let DirectoryEntry::Entry(file_entry) = entry;
370
371            let name = file_entry.name();
372            if name == "." || name == ".." {
373                continue;
374            }
375
376            if file_entry.is_directory() {
377                *dirs += 1;
378                let subdir = FatDir {
379                    data: self,
380                    cluster: file_entry.cluster(),
381                    fixed_root: None,
382                };
383                self.count_entries_recursive(&subdir, depth + 1, files, dirs)?;
384            } else {
385                *files += 1;
386            }
387        }
388        Ok(())
389    }
390
391    /// Collect all files recursively with their paths.
392    fn collect_files_recursive<'a>(
393        &'a self,
394        dir: &FatDir<'a, DATA>,
395        path_prefix: String,
396        depth: u32,
397        files: &mut Vec<(String, FileEntry)>,
398    ) -> Result<()> {
399        if depth > MAX_DIRECTORY_DEPTH {
400            return Err(crate::error::Error::CorruptFilesystem {
401                context: "directory nesting depth limit exceeded",
402            });
403        }
404        for entry in dir.entries() {
405            let entry = entry?;
406            let DirectoryEntry::Entry(file_entry) = entry;
407
408            let name = file_entry.name();
409            if name == "." || name == ".." {
410                continue;
411            }
412
413            let full_path = if path_prefix.is_empty() {
414                format!("/{name}")
415            } else {
416                format!("{path_prefix}/{name}")
417            };
418
419            if file_entry.is_directory() {
420                let subdir = FatDir {
421                    data: self,
422                    cluster: file_entry.cluster(),
423                    fixed_root: None,
424                };
425                self.collect_files_recursive(&subdir, full_path, depth + 1, files)?;
426            } else {
427                files.push((full_path, file_entry));
428            }
429        }
430        Ok(())
431    }
432}
433
434// FAT entry readers - these need to be added to the Fat12/16/32 implementations
435impl Fat12 {
436    /// Read a raw FAT12 entry.
437    pub fn read_entry<T: Read + Seek>(&self, reader: &mut T, cluster: usize) -> Result<u16> {
438        let byte_offset = self.entry_byte_offset(cluster);
439        reader.seek(super::super::io::SeekFrom::Start(byte_offset as u64))?;
440
441        let mut bytes = [0u8; 2];
442        reader.read_exact(&mut bytes)?;
443
444        let value = if cluster.is_multiple_of(2) {
445            u16::from(bytes[0]) | (u16::from(bytes[1] & 0x0F) << 8)
446        } else {
447            (u16::from(bytes[0]) >> 4) | (u16::from(bytes[1]) << 4)
448        };
449
450        Ok(value)
451    }
452}
453
454impl Fat16 {
455    /// Read a raw FAT16 entry.
456    pub fn read_entry<T: Read + Seek>(&self, reader: &mut T, cluster: usize) -> Result<u16> {
457        let offset = self.entry_offset(cluster);
458        reader.seek(super::super::io::SeekFrom::Start(offset as u64))?;
459
460        let mut bytes = [0u8; 2];
461        reader.read_exact(&mut bytes)?;
462
463        Ok(u16::from_le_bytes(bytes))
464    }
465}
466
467impl Fat32 {
468    /// Read a raw FAT32 entry.
469    pub fn read_entry<T: Read + Seek>(&self, reader: &mut T, cluster: usize) -> Result<u32> {
470        let offset = self.entry_offset(cluster);
471        reader.seek(super::super::io::SeekFrom::Start(offset as u64))?;
472
473        let mut bytes = [0u8; 4];
474        reader.read_exact(&mut bytes)?;
475
476        Ok(u32::from_le_bytes(bytes))
477    }
478}
479
480/// Classify a FAT12 entry value.
481fn classify_fat12_entry(entry: u16) -> ClusterState {
482    let masked = entry & 0x0FFF;
483    match masked {
484        0x000 => ClusterState::Free,
485        0x001 => ClusterState::Reserved,
486        0xFF7 => ClusterState::Bad,
487        0xFF8..=0xFFF => ClusterState::EndOfChain,
488        n => ClusterState::Used(n as u32),
489    }
490}
491
492/// Classify a FAT16 entry value.
493fn classify_fat16_entry(entry: u16) -> ClusterState {
494    match entry {
495        0x0000 => ClusterState::Free,
496        0x0001 => ClusterState::Reserved,
497        0xFFF7 => ClusterState::Bad,
498        0xFFF8..=0xFFFF => ClusterState::EndOfChain,
499        n => ClusterState::Used(n as u32),
500    }
501}
502
503/// Classify a FAT32 entry value.
504fn classify_fat32_entry(entry: u32) -> ClusterState {
505    let masked = entry & 0x0FFF_FFFF;
506    match masked {
507        0x0000_0000 => ClusterState::Free,
508        0x0000_0001 => ClusterState::Reserved,
509        0x0FFF_FFF7 => ClusterState::Bad,
510        0x0FFF_FFF8..=0x0FFF_FFFF => ClusterState::EndOfChain,
511        n => ClusterState::Used(n),
512    }
513}
514
515/// Count the number of contiguous fragments in a cluster chain.
516fn count_fragments(chain: &[u32]) -> u32 {
517    if chain.is_empty() {
518        return 0;
519    }
520    if chain.len() == 1 {
521        return 1;
522    }
523
524    let mut fragments = 1u32;
525    for window in chain.windows(2) {
526        if window[1] != window[0] + 1 {
527            fragments += 1;
528        }
529    }
530    fragments
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    #[test]
538    fn test_count_fragments() {
539        assert_eq!(count_fragments(&[]), 0);
540        assert_eq!(count_fragments(&[5]), 1);
541        assert_eq!(count_fragments(&[5, 6, 7]), 1);
542        assert_eq!(count_fragments(&[5, 6, 10]), 2);
543        assert_eq!(count_fragments(&[5, 10, 15]), 3);
544        assert_eq!(count_fragments(&[5, 6, 7, 10, 11, 15]), 3);
545    }
546
547    #[test]
548    fn test_classify_fat12() {
549        assert_eq!(classify_fat12_entry(0x000), ClusterState::Free);
550        assert_eq!(classify_fat12_entry(0x001), ClusterState::Reserved);
551        assert_eq!(classify_fat12_entry(0xFF7), ClusterState::Bad);
552        assert_eq!(classify_fat12_entry(0xFF8), ClusterState::EndOfChain);
553        assert_eq!(classify_fat12_entry(0xFFF), ClusterState::EndOfChain);
554        assert_eq!(classify_fat12_entry(0x123), ClusterState::Used(0x123));
555    }
556
557    #[test]
558    fn test_classify_fat16() {
559        assert_eq!(classify_fat16_entry(0x0000), ClusterState::Free);
560        assert_eq!(classify_fat16_entry(0x0001), ClusterState::Reserved);
561        assert_eq!(classify_fat16_entry(0xFFF7), ClusterState::Bad);
562        assert_eq!(classify_fat16_entry(0xFFF8), ClusterState::EndOfChain);
563        assert_eq!(classify_fat16_entry(0xFFFF), ClusterState::EndOfChain);
564        assert_eq!(classify_fat16_entry(0x1234), ClusterState::Used(0x1234));
565    }
566
567    #[test]
568    fn test_classify_fat32() {
569        assert_eq!(classify_fat32_entry(0x0000_0000), ClusterState::Free);
570        assert_eq!(classify_fat32_entry(0x0000_0001), ClusterState::Reserved);
571        assert_eq!(classify_fat32_entry(0x0FFF_FFF7), ClusterState::Bad);
572        assert_eq!(classify_fat32_entry(0x0FFF_FFF8), ClusterState::EndOfChain);
573        assert_eq!(classify_fat32_entry(0x0FFF_FFFF), ClusterState::EndOfChain);
574        assert_eq!(
575            classify_fat32_entry(0x0012_3456),
576            ClusterState::Used(0x0012_3456)
577        );
578    }
579}