use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::ops::DerefMut;
use super::super::{
dir::{DirectoryEntry, FatDir},
fs::FatVolume,
io::{Read, Seek},
};
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerificationIssue {
ClusterLoop {
path: String,
cluster: u32,
},
CrossLinkedCluster {
cluster: u32,
paths: Vec<String>,
},
OrphanedChain {
start_cluster: u32,
chain_length: u32,
},
SizeMismatch {
path: String,
recorded_size: usize,
chain_size: usize,
},
InvalidFirstCluster {
path: String,
cluster: u32,
},
BadClusterInChain {
path: String,
position: u32,
cluster: u32,
},
InvalidEntryName {
parent_path: String,
raw_name: [u8; 11],
},
LostClusters {
count: u32,
},
}
impl core::fmt::Display for VerificationIssue {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::ClusterLoop { path, cluster } => {
write!(f, "Cluster loop detected at cluster {cluster} in '{path}'")
}
Self::CrossLinkedCluster { cluster, paths } => {
write!(
f,
"Cross-linked cluster {}: shared by {}",
cluster,
paths.join(", ")
)
}
Self::OrphanedChain {
start_cluster,
chain_length,
} => {
write!(
f,
"Orphaned cluster chain starting at {start_cluster} ({chain_length} clusters)"
)
}
Self::SizeMismatch {
path,
recorded_size,
chain_size,
} => {
write!(
f,
"Size mismatch for '{path}': recorded {recorded_size} bytes, chain suggests {chain_size} bytes"
)
}
Self::InvalidFirstCluster { path, cluster } => {
write!(f, "Invalid first cluster {cluster} for '{path}'")
}
Self::BadClusterInChain {
path,
position,
cluster,
} => {
write!(
f,
"Bad cluster {cluster} at position {position} in chain for '{path}'"
)
}
Self::InvalidEntryName {
parent_path,
raw_name: _,
} => {
write!(f, "Invalid entry name in directory '{parent_path}'")
}
Self::LostClusters { count } => {
write!(f, "{count} lost clusters (not referenced by any file)")
}
}
}
}
#[derive(Debug, Clone)]
pub struct VerificationReport {
pub issues: Vec<VerificationIssue>,
pub files_checked: u32,
pub directories_checked: u32,
pub clusters_verified: u32,
}
impl VerificationReport {
pub fn is_valid(&self) -> bool {
self.issues.is_empty()
}
pub fn issue_count(&self) -> usize {
self.issues.len()
}
pub fn issues_of_type<F>(&self, predicate: F) -> Vec<&VerificationIssue>
where
F: Fn(&VerificationIssue) -> bool,
{
self.issues.iter().filter(|i| predicate(i)).collect()
}
}
pub trait FatVerifyExt<DATA: Read + Seek> {
fn verify(&self) -> Result<VerificationReport>;
}
impl<DATA: Read + Seek> FatVerifyExt<DATA> for FatVolume<DATA> {
fn verify(&self) -> Result<VerificationReport> {
let mut issues = Vec::new();
let mut files_checked = 0u32;
let mut directories_checked = 0u32;
let cluster_size = self.info.cluster_size;
let max_cluster = self.info.max_cluster;
let mut cluster_usage: BTreeMap<u32, Vec<String>> = BTreeMap::new();
let mut used_by_files = alloc::vec![false; max_cluster as usize + 1];
self.verify_directory_recursive(
&self.root_dir(),
String::new(),
&mut issues,
&mut files_checked,
&mut directories_checked,
&mut cluster_usage,
&mut used_by_files,
cluster_size,
max_cluster,
)?;
for (cluster, paths) in &cluster_usage {
if paths.len() > 1 {
issues.push(VerificationIssue::CrossLinkedCluster {
cluster: *cluster,
paths: paths.clone(),
});
}
}
let mut data = self.data.lock();
let mut orphaned_count = 0u32;
for cluster in 2..=max_cluster {
if !used_by_files[cluster as usize] {
if let Ok(Some(_)) = self.fat.next_cluster(data.deref_mut(), cluster as usize) {
orphaned_count += 1;
}
}
}
drop(data);
if orphaned_count > 0 {
issues.push(VerificationIssue::LostClusters {
count: orphaned_count,
});
}
Ok(VerificationReport {
issues,
files_checked,
directories_checked,
clusters_verified: max_cluster,
})
}
}
impl<DATA: Read + Seek> FatVolume<DATA> {
#[allow(clippy::too_many_arguments)]
fn verify_directory_recursive<'a>(
&'a self,
dir: &FatDir<'a, DATA>,
path_prefix: String,
issues: &mut Vec<VerificationIssue>,
files_checked: &mut u32,
directories_checked: &mut u32,
cluster_usage: &mut BTreeMap<u32, Vec<String>>,
used_by_files: &mut [bool],
cluster_size: usize,
max_cluster: u32,
) -> Result<()> {
for entry in dir.entries() {
let entry = entry?;
let DirectoryEntry::Entry(file_entry) = entry;
let name = file_entry.name();
if name == "." || name == ".." {
continue;
}
let full_path = if path_prefix.is_empty() {
format!("/{name}")
} else {
format!("{path_prefix}/{name}")
};
let first_cluster = file_entry.cluster().0 as u32;
if first_cluster != 0 && (first_cluster < 2 || first_cluster > max_cluster) {
issues.push(VerificationIssue::InvalidFirstCluster {
path: full_path.clone(),
cluster: first_cluster,
});
continue;
}
if file_entry.is_directory() {
*directories_checked += 1;
if first_cluster >= 2 {
self.verify_cluster_chain(
first_cluster,
&full_path,
issues,
cluster_usage,
used_by_files,
max_cluster,
)?;
}
let subdir = FatDir {
data: self,
cluster: file_entry.cluster(),
fixed_root: None,
};
self.verify_directory_recursive(
&subdir,
full_path,
issues,
files_checked,
directories_checked,
cluster_usage,
used_by_files,
cluster_size,
max_cluster,
)?;
} else {
*files_checked += 1;
if first_cluster >= 2 {
let chain_length = self.verify_cluster_chain(
first_cluster,
&full_path,
issues,
cluster_usage,
used_by_files,
max_cluster,
)?;
let recorded_size = file_entry.len() as usize;
let chain_size = chain_length as usize * cluster_size;
let min_chain_size = if chain_length > 0 {
(chain_length as usize - 1) * cluster_size + 1
} else {
0
};
if recorded_size > chain_size
|| (recorded_size > 0 && recorded_size < min_chain_size)
{
issues.push(VerificationIssue::SizeMismatch {
path: full_path,
recorded_size,
chain_size,
});
}
} else if !file_entry.is_empty() {
issues.push(VerificationIssue::SizeMismatch {
path: full_path,
recorded_size: file_entry.len() as usize,
chain_size: 0,
});
}
}
}
Ok(())
}
fn verify_cluster_chain(
&self,
start_cluster: u32,
path: &str,
issues: &mut Vec<VerificationIssue>,
cluster_usage: &mut BTreeMap<u32, Vec<String>>,
used_by_files: &mut [bool],
max_cluster: u32,
) -> Result<u32> {
let mut chain_length = 0u32;
let mut current = start_cluster;
let mut data = self.data.lock();
let mut visited = alloc::vec![false; max_cluster as usize + 1];
let max_iterations = max_cluster as usize;
let mut iterations = 0;
loop {
if current < 2 || current > max_cluster {
break;
}
if visited[current as usize] {
issues.push(VerificationIssue::ClusterLoop {
path: path.to_string(),
cluster: current,
});
break;
}
visited[current as usize] = true;
used_by_files[current as usize] = true;
chain_length += 1;
cluster_usage
.entry(current)
.or_default()
.push(path.to_string());
iterations += 1;
if iterations > max_iterations {
break;
}
match self.fat.next_cluster(data.deref_mut(), current as usize)? {
Some(next) => {
current = next;
}
None => break, }
}
Ok(chain_length)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verification_report_is_valid() {
let report = VerificationReport {
issues: Vec::new(),
files_checked: 10,
directories_checked: 5,
clusters_verified: 1000,
};
assert!(report.is_valid());
let report_with_issues = VerificationReport {
issues: alloc::vec![VerificationIssue::LostClusters { count: 5 }],
files_checked: 10,
directories_checked: 5,
clusters_verified: 1000,
};
assert!(!report_with_issues.is_valid());
}
}