1use 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#[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 = alloc::vec![false; max_cluster as usize + 1];
204
205 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 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 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 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
258impl<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 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 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 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 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 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 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 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 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 cluster_usage
416 .entry(current)
417 .or_default()
418 .push(path.to_string());
419
420 iterations += 1;
421 if iterations > max_iterations {
422 break;
424 }
425
426 match self.fat.next_cluster(data.deref_mut(), current as usize)? {
428 Some(next) => {
429 current = next;
430 }
431 None => break, }
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}