Skip to main content

hadris_fat/tool/
verify.rs

1//! Filesystem integrity verification for FAT filesystems.
2
3use alloc::collections::BTreeMap;
4use alloc::format;
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use core::ops::DerefMut;
8
9use super::super::{
10    dir::{DirectoryEntry, FatDir},
11    fs::FatVolume,
12    io::{Read, Seek},
13};
14use crate::error::Result;
15
16/// Types of verification issues that can be detected.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum VerificationIssue {
19    /// A cluster chain contains a loop (revisits a cluster).
20    ClusterLoop {
21        /// File or directory path containing the loop
22        path: String,
23        /// The cluster where the loop was detected
24        cluster: u32,
25    },
26
27    /// Two or more files share the same cluster (cross-linked).
28    CrossLinkedCluster {
29        /// The shared cluster number
30        cluster: u32,
31        /// Paths of files sharing this cluster
32        paths: Vec<String>,
33    },
34
35    /// A cluster chain exists in the FAT but is not referenced by any file.
36    OrphanedChain {
37        /// Starting cluster of the orphaned chain
38        start_cluster: u32,
39        /// Length of the chain in clusters
40        chain_length: u32,
41    },
42
43    /// The recorded file size doesn't match the cluster chain length.
44    SizeMismatch {
45        /// File path
46        path: String,
47        /// Size recorded in the directory entry
48        recorded_size: usize,
49        /// Size implied by the cluster chain
50        chain_size: usize,
51    },
52
53    /// A directory entry points to an invalid cluster.
54    InvalidFirstCluster {
55        /// File or directory path
56        path: String,
57        /// The invalid cluster number
58        cluster: u32,
59    },
60
61    /// The cluster chain contains a bad cluster marker.
62    BadClusterInChain {
63        /// File or directory path
64        path: String,
65        /// Position in the chain where the bad cluster was found
66        position: u32,
67        /// The bad cluster number
68        cluster: u32,
69    },
70
71    /// A directory entry has an invalid name.
72    InvalidEntryName {
73        /// Parent directory path
74        parent_path: String,
75        /// Raw bytes of the invalid name
76        raw_name: [u8; 11],
77    },
78
79    /// Lost clusters (used in FAT but not referenced by any file).
80    LostClusters {
81        /// Number of lost clusters
82        count: u32,
83    },
84}
85
86impl core::fmt::Display for VerificationIssue {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        match self {
89            Self::ClusterLoop { path, cluster } => {
90                write!(f, "Cluster loop detected at cluster {cluster} in '{path}'")
91            }
92            Self::CrossLinkedCluster { cluster, paths } => {
93                write!(
94                    f,
95                    "Cross-linked cluster {}: shared by {}",
96                    cluster,
97                    paths.join(", ")
98                )
99            }
100            Self::OrphanedChain {
101                start_cluster,
102                chain_length,
103            } => {
104                write!(
105                    f,
106                    "Orphaned cluster chain starting at {start_cluster} ({chain_length} clusters)"
107                )
108            }
109            Self::SizeMismatch {
110                path,
111                recorded_size,
112                chain_size,
113            } => {
114                write!(
115                    f,
116                    "Size mismatch for '{path}': recorded {recorded_size} bytes, chain suggests {chain_size} bytes"
117                )
118            }
119            Self::InvalidFirstCluster { path, cluster } => {
120                write!(f, "Invalid first cluster {cluster} for '{path}'")
121            }
122            Self::BadClusterInChain {
123                path,
124                position,
125                cluster,
126            } => {
127                write!(
128                    f,
129                    "Bad cluster {cluster} at position {position} in chain for '{path}'"
130                )
131            }
132            Self::InvalidEntryName {
133                parent_path,
134                raw_name: _,
135            } => {
136                write!(f, "Invalid entry name in directory '{parent_path}'")
137            }
138            Self::LostClusters { count } => {
139                write!(f, "{count} lost clusters (not referenced by any file)")
140            }
141        }
142    }
143}
144
145/// Report from filesystem verification.
146#[derive(Debug, Clone)]
147pub struct VerificationReport {
148    /// List of issues found
149    pub issues: Vec<VerificationIssue>,
150    /// Total files checked
151    pub files_checked: u32,
152    /// Total directories checked
153    pub directories_checked: u32,
154    /// Total clusters verified
155    pub clusters_verified: u32,
156}
157
158impl VerificationReport {
159    /// Check if the filesystem passed verification (no issues found).
160    pub fn is_valid(&self) -> bool {
161        self.issues.is_empty()
162    }
163
164    /// Get the number of issues found.
165    pub fn issue_count(&self) -> usize {
166        self.issues.len()
167    }
168
169    /// Get issues filtered by type.
170    pub fn issues_of_type<F>(&self, predicate: F) -> Vec<&VerificationIssue>
171    where
172        F: Fn(&VerificationIssue) -> bool,
173    {
174        self.issues.iter().filter(|i| predicate(i)).collect()
175    }
176}
177
178/// Extension trait for FatVolume providing verification operations.
179pub trait FatVerifyExt<DATA: Read + Seek> {
180    /// Verify filesystem integrity.
181    ///
182    /// This performs comprehensive checks including:
183    /// - Cluster chain validation (loops, bad clusters)
184    /// - Cross-link detection (multiple files sharing clusters)
185    /// - Orphaned cluster detection
186    /// - File size validation
187    /// - Directory entry validation
188    fn verify(&self) -> Result<VerificationReport>;
189}
190
191impl<DATA: Read + Seek> FatVerifyExt<DATA> for FatVolume<DATA> {
192    fn verify(&self) -> Result<VerificationReport> {
193        let mut issues = Vec::new();
194        let mut files_checked = 0u32;
195        let mut directories_checked = 0u32;
196        let cluster_size = self.info.cluster_size;
197        let max_cluster = self.info.max_cluster;
198
199        // Map of cluster -> list of paths that reference it
200        let mut cluster_usage: BTreeMap<u32, Vec<String>> = BTreeMap::new();
201
202        // Track which clusters are used by files
203        let mut used_by_files = alloc::vec![false; max_cluster as usize + 1];
204
205        // Verify all files and directories
206        self.verify_directory_recursive(
207            &self.root_dir(),
208            String::new(),
209            &mut issues,
210            &mut files_checked,
211            &mut directories_checked,
212            &mut cluster_usage,
213            &mut used_by_files,
214            cluster_size,
215            max_cluster,
216        )?;
217
218        // Check for cross-linked clusters
219        for (cluster, paths) in &cluster_usage {
220            if paths.len() > 1 {
221                issues.push(VerificationIssue::CrossLinkedCluster {
222                    cluster: *cluster,
223                    paths: paths.clone(),
224                });
225            }
226        }
227
228        // Scan FAT for orphaned clusters (used in FAT but not by any file)
229        let mut data = self.data.lock();
230        let mut orphaned_count = 0u32;
231
232        for cluster in 2..=max_cluster {
233            if !used_by_files[cluster as usize] {
234                // Check if this cluster is marked as used in the FAT
235                if let Ok(Some(_)) = self.fat.next_cluster(data.deref_mut(), cluster as usize) {
236                    orphaned_count += 1;
237                }
238            }
239        }
240
241        drop(data);
242
243        if orphaned_count > 0 {
244            issues.push(VerificationIssue::LostClusters {
245                count: orphaned_count,
246            });
247        }
248
249        Ok(VerificationReport {
250            issues,
251            files_checked,
252            directories_checked,
253            clusters_verified: max_cluster,
254        })
255    }
256}
257
258// Helper methods
259impl<DATA: Read + Seek> FatVolume<DATA> {
260    #[allow(clippy::too_many_arguments)]
261    fn verify_directory_recursive<'a>(
262        &'a self,
263        dir: &FatDir<'a, DATA>,
264        path_prefix: String,
265        issues: &mut Vec<VerificationIssue>,
266        files_checked: &mut u32,
267        directories_checked: &mut u32,
268        cluster_usage: &mut BTreeMap<u32, Vec<String>>,
269        used_by_files: &mut [bool],
270        cluster_size: usize,
271        max_cluster: u32,
272    ) -> Result<()> {
273        for entry in dir.entries() {
274            let entry = entry?;
275            let DirectoryEntry::Entry(file_entry) = entry;
276
277            let name = file_entry.name();
278            if name == "." || name == ".." {
279                continue;
280            }
281
282            let full_path = if path_prefix.is_empty() {
283                format!("/{name}")
284            } else {
285                format!("{path_prefix}/{name}")
286            };
287
288            let first_cluster = file_entry.cluster().0 as u32;
289
290            // Validate first cluster
291            if first_cluster != 0 && (first_cluster < 2 || first_cluster > max_cluster) {
292                issues.push(VerificationIssue::InvalidFirstCluster {
293                    path: full_path.clone(),
294                    cluster: first_cluster,
295                });
296                continue;
297            }
298
299            if file_entry.is_directory() {
300                *directories_checked += 1;
301
302                // Verify directory cluster chain
303                if first_cluster >= 2 {
304                    self.verify_cluster_chain(
305                        first_cluster,
306                        &full_path,
307                        issues,
308                        cluster_usage,
309                        used_by_files,
310                        max_cluster,
311                    )?;
312                }
313
314                // Recurse into subdirectory
315                let subdir = FatDir {
316                    data: self,
317                    cluster: file_entry.cluster(),
318                    fixed_root: None,
319                };
320                self.verify_directory_recursive(
321                    &subdir,
322                    full_path,
323                    issues,
324                    files_checked,
325                    directories_checked,
326                    cluster_usage,
327                    used_by_files,
328                    cluster_size,
329                    max_cluster,
330                )?;
331            } else {
332                *files_checked += 1;
333
334                // Verify file cluster chain
335                if first_cluster >= 2 {
336                    let chain_length = self.verify_cluster_chain(
337                        first_cluster,
338                        &full_path,
339                        issues,
340                        cluster_usage,
341                        used_by_files,
342                        max_cluster,
343                    )?;
344
345                    // Verify file size matches chain length
346                    let recorded_size = file_entry.len() as usize;
347                    let chain_size = chain_length as usize * cluster_size;
348                    let min_chain_size = if chain_length > 0 {
349                        (chain_length as usize - 1) * cluster_size + 1
350                    } else {
351                        0
352                    };
353
354                    if recorded_size > chain_size
355                        || (recorded_size > 0 && recorded_size < min_chain_size)
356                    {
357                        issues.push(VerificationIssue::SizeMismatch {
358                            path: full_path,
359                            recorded_size,
360                            chain_size,
361                        });
362                    }
363                } else if !file_entry.is_empty() {
364                    // Non-zero size but no cluster chain
365                    issues.push(VerificationIssue::SizeMismatch {
366                        path: full_path,
367                        recorded_size: file_entry.len() as usize,
368                        chain_size: 0,
369                    });
370                }
371            }
372        }
373
374        Ok(())
375    }
376
377    fn verify_cluster_chain(
378        &self,
379        start_cluster: u32,
380        path: &str,
381        issues: &mut Vec<VerificationIssue>,
382        cluster_usage: &mut BTreeMap<u32, Vec<String>>,
383        used_by_files: &mut [bool],
384        max_cluster: u32,
385    ) -> Result<u32> {
386        let mut chain_length = 0u32;
387        let mut current = start_cluster;
388        let mut data = self.data.lock();
389
390        // Track visited clusters to detect loops
391        let mut visited = alloc::vec![false; max_cluster as usize + 1];
392
393        let max_iterations = max_cluster as usize;
394        let mut iterations = 0;
395
396        loop {
397            if current < 2 || current > max_cluster {
398                break;
399            }
400
401            // Check for loop
402            if visited[current as usize] {
403                issues.push(VerificationIssue::ClusterLoop {
404                    path: path.to_string(),
405                    cluster: current,
406                });
407                break;
408            }
409
410            visited[current as usize] = true;
411            used_by_files[current as usize] = true;
412            chain_length += 1;
413
414            // Record cluster usage
415            cluster_usage
416                .entry(current)
417                .or_default()
418                .push(path.to_string());
419
420            iterations += 1;
421            if iterations > max_iterations {
422                // Safety limit to prevent infinite loops
423                break;
424            }
425
426            // Get next cluster
427            match self.fat.next_cluster(data.deref_mut(), current as usize)? {
428                Some(next) => {
429                    current = next;
430                }
431                None => break, // End of chain
432            }
433        }
434
435        Ok(chain_length)
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn test_verification_report_is_valid() {
445        let report = VerificationReport {
446            issues: Vec::new(),
447            files_checked: 10,
448            directories_checked: 5,
449            clusters_verified: 1000,
450        };
451        assert!(report.is_valid());
452
453        let report_with_issues = VerificationReport {
454            issues: alloc::vec![VerificationIssue::LostClusters { count: 5 }],
455            files_checked: 10,
456            directories_checked: 5,
457            clusters_verified: 1000,
458        };
459        assert!(!report_with_issues.is_valid());
460    }
461}