1use alloc::collections::{BTreeMap, BTreeSet};
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#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum VerificationIssue {
19 ClusterLoop {
21 path: String,
23 cluster: u32,
25 },
26
27 CrossLinkedCluster {
29 cluster: u32,
31 paths: Vec<String>,
33 },
34
35 OrphanedChain {
37 start_cluster: u32,
39 chain_length: u32,
41 },
42
43 SizeMismatch {
45 path: String,
47 recorded_size: usize,
49 chain_size: usize,
51 },
52
53 InvalidFirstCluster {
55 path: String,
57 cluster: u32,
59 },
60
61 BadClusterInChain {
63 path: String,
65 position: u32,
67 cluster: u32,
69 },
70
71 InvalidEntryName {
73 parent_path: String,
75 raw_name: [u8; 11],
77 },
78
79 LostClusters {
81 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#[derive(Debug, Clone)]
147pub struct VerificationReport {
148 pub issues: Vec<VerificationIssue>,
150 pub files_checked: u32,
152 pub directories_checked: u32,
154 pub clusters_verified: u32,
156}
157
158impl VerificationReport {
159 pub fn is_valid(&self) -> bool {
161 self.issues.is_empty()
162 }
163
164 pub fn issue_count(&self) -> usize {
166 self.issues.len()
167 }
168
169 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
178pub trait FatVerifyExt<DATA: Read + Seek> {
180 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 let mut cluster_usage: BTreeMap<u32, Vec<String>> = BTreeMap::new();
201
202 let mut used_by_files: BTreeSet<u32> = BTreeSet::new();
207
208 self.verify_directory_recursive(
210 &self.root_dir(),
211 String::new(),
212 0,
213 &mut issues,
214 &mut files_checked,
215 &mut directories_checked,
216 &mut cluster_usage,
217 &mut used_by_files,
218 cluster_size,
219 max_cluster,
220 )?;
221
222 for (cluster, paths) in &cluster_usage {
224 if paths.len() > 1 {
225 issues.push(VerificationIssue::CrossLinkedCluster {
226 cluster: *cluster,
227 paths: paths.clone(),
228 });
229 }
230 }
231
232 let mut data = self.data.lock();
234 let mut orphaned_count = 0u32;
235
236 for cluster in 2..=max_cluster {
237 if !used_by_files.contains(&cluster) {
238 if let Ok(Some(_)) = self.fat.next_cluster(data.deref_mut(), cluster as usize) {
240 orphaned_count += 1;
241 }
242 }
243 }
244
245 drop(data);
246
247 if orphaned_count > 0 {
248 issues.push(VerificationIssue::LostClusters {
249 count: orphaned_count,
250 });
251 }
252
253 Ok(VerificationReport {
254 issues,
255 files_checked,
256 directories_checked,
257 clusters_verified: max_cluster,
258 })
259 }
260}
261
262impl<DATA: Read + Seek> FatVolume<DATA> {
264 #[allow(clippy::too_many_arguments)]
265 fn verify_directory_recursive<'a>(
266 &'a self,
267 dir: &FatDir<'a, DATA>,
268 path_prefix: String,
269 depth: u32,
270 issues: &mut Vec<VerificationIssue>,
271 files_checked: &mut u32,
272 directories_checked: &mut u32,
273 cluster_usage: &mut BTreeMap<u32, Vec<String>>,
274 used_by_files: &mut BTreeSet<u32>,
275 cluster_size: usize,
276 max_cluster: u32,
277 ) -> Result<()> {
278 if depth > super::analysis::MAX_DIRECTORY_DEPTH {
282 return Err(crate::error::Error::CorruptFilesystem {
283 context: "directory nesting depth limit exceeded",
284 });
285 }
286 for entry in dir.entries() {
287 let entry = entry?;
288 let DirectoryEntry::Entry(file_entry) = entry;
289
290 let name = file_entry.name();
291 if name == "." || name == ".." {
292 continue;
293 }
294
295 let full_path = if path_prefix.is_empty() {
296 format!("/{name}")
297 } else {
298 format!("{path_prefix}/{name}")
299 };
300
301 let first_cluster = file_entry.cluster().0 as u32;
302
303 if first_cluster != 0 && (first_cluster < 2 || first_cluster > max_cluster) {
305 issues.push(VerificationIssue::InvalidFirstCluster {
306 path: full_path.clone(),
307 cluster: first_cluster,
308 });
309 continue;
310 }
311
312 if file_entry.is_directory() {
313 *directories_checked += 1;
314
315 if first_cluster >= 2 {
317 self.verify_cluster_chain(
318 first_cluster,
319 &full_path,
320 issues,
321 cluster_usage,
322 used_by_files,
323 max_cluster,
324 )?;
325 }
326
327 let subdir = FatDir {
329 data: self,
330 cluster: file_entry.cluster(),
331 fixed_root: None,
332 };
333 self.verify_directory_recursive(
334 &subdir,
335 full_path,
336 depth + 1,
337 issues,
338 files_checked,
339 directories_checked,
340 cluster_usage,
341 used_by_files,
342 cluster_size,
343 max_cluster,
344 )?;
345 } else {
346 *files_checked += 1;
347
348 if first_cluster >= 2 {
350 let chain_length = self.verify_cluster_chain(
351 first_cluster,
352 &full_path,
353 issues,
354 cluster_usage,
355 used_by_files,
356 max_cluster,
357 )?;
358
359 let recorded_size = file_entry.len() as usize;
361 let chain_size = chain_length as usize * cluster_size;
362 let min_chain_size = if chain_length > 0 {
363 (chain_length as usize - 1) * cluster_size + 1
364 } else {
365 0
366 };
367
368 if recorded_size > chain_size
369 || (recorded_size > 0 && recorded_size < min_chain_size)
370 {
371 issues.push(VerificationIssue::SizeMismatch {
372 path: full_path,
373 recorded_size,
374 chain_size,
375 });
376 }
377 } else if !file_entry.is_empty() {
378 issues.push(VerificationIssue::SizeMismatch {
380 path: full_path,
381 recorded_size: file_entry.len() as usize,
382 chain_size: 0,
383 });
384 }
385 }
386 }
387
388 Ok(())
389 }
390
391 fn verify_cluster_chain(
392 &self,
393 start_cluster: u32,
394 path: &str,
395 issues: &mut Vec<VerificationIssue>,
396 cluster_usage: &mut BTreeMap<u32, Vec<String>>,
397 used_by_files: &mut BTreeSet<u32>,
398 max_cluster: u32,
399 ) -> Result<u32> {
400 let mut chain_length = 0u32;
401 let mut current = start_cluster;
402 let mut data = self.data.lock();
403
404 let mut visited = alloc::vec![false; max_cluster as usize + 1];
406
407 let max_iterations = max_cluster as usize;
408 let mut iterations = 0;
409
410 loop {
411 if current < 2 || current > max_cluster {
412 break;
413 }
414
415 if visited[current as usize] {
417 issues.push(VerificationIssue::ClusterLoop {
418 path: path.to_string(),
419 cluster: current,
420 });
421 break;
422 }
423
424 visited[current as usize] = true;
425 used_by_files.insert(current);
426 chain_length += 1;
427
428 cluster_usage
430 .entry(current)
431 .or_default()
432 .push(path.to_string());
433
434 iterations += 1;
435 if iterations > max_iterations {
436 break;
438 }
439
440 match self.fat.next_cluster(data.deref_mut(), current as usize)? {
442 Some(next) => {
443 current = next;
444 }
445 None => break, }
447 }
448
449 Ok(chain_length)
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_verification_report_is_valid() {
459 let report = VerificationReport {
460 issues: Vec::new(),
461 files_checked: 10,
462 directories_checked: 5,
463 clusters_verified: 1000,
464 };
465 assert!(report.is_valid());
466
467 let report_with_issues = VerificationReport {
468 issues: alloc::vec![VerificationIssue::LostClusters { count: 5 }],
469 files_checked: 10,
470 directories_checked: 5,
471 clusters_verified: 1000,
472 };
473 assert!(!report_with_issues.is_valid());
474 }
475}