///|
/// One verifier finding. `cluster` is 0 when the issue is volume-wide.
pub(all) struct VerifyIssue {
code : String
message : String
cluster : Int
} derive(Eq, Debug)
///|
fn issue(code : String, message : String, cluster : Int) -> VerifyIssue {
{ code, message, cluster }
}
///|
pub fn Volume::verify(self : Volume) -> Array[VerifyIssue] {
let issues : Array[VerifyIssue] = []
match parse_layout(self.data) {
Err(err) => {
issues.push(issue("VFY001", err.message(), 0))
return issues
}
Ok(_) => ()
}
if self.data.length() != self.total_bytes() {
issues.push(
issue(
"VFY002",
"image length \{self.data.length()} does not match \{self.total_bytes()} byte layout",
0,
),
)
}
match self.layout.kind {
Fat32 =>
if self.layout.root_cluster < 2 {
issues.push(
issue(
"VFY003",
"FAT32 root cluster is invalid",
self.layout.root_cluster,
),
)
}
_ => ()
}
let used = Array::make(self.layout.cluster_count + 2, 0)
self.verify_fat(issues, used)
self.verify_tree(root_cluster_of(self.layout), "/", issues, used)
self.verify_lost_clusters(issues, used)
issues
}
///|
fn Volume::verify_fat(
self : Volume,
issues : Array[VerifyIssue],
used : Array[Int],
) -> Unit {
let media_entry = match read_fat_raw(self.data, self.layout, 0) {
Err(err) => {
issues.push(issue("VFY010", err.message(), 0))
return
}
Ok(value) => value
}
if (media_entry & 0xFF) != self.layout.media.to_int() {
issues.push(
issue(
"VFY011",
"FAT[0] media nibble \{media_entry & 0xFF} != BPB media \{self.layout.media.to_int()}",
0,
),
)
}
match read_fat_raw(self.data, self.layout, 1) {
Err(err) => issues.push(issue("VFY012", err.message(), 1))
Ok(_) => ()
}
for cluster = 2
cluster <= self.layout.cluster_count + 1
cluster = cluster + 1 {
match read_fat_raw(self.data, self.layout, cluster) {
Err(err) => issues.push(issue("VFY013", err.message(), cluster))
Ok(value) =>
if self.layout.kind.is_bad(value) {
used[cluster] = 2
} else if value == 0 {
used[cluster] = 0
} else if self.layout.kind.is_eoc(value) ||
(value >= 2 && value <= self.layout.cluster_count + 1) {
used[cluster] = 1
} else {
issues.push(
issue(
"VFY014",
"FAT[\{cluster}] has out-of-range next \{value}",
cluster,
),
)
}
}
}
if self.layout.fat_count >= 2 {
let len = self.layout.fat_bytes()
let a = fat_base(self.layout, 0)
let b = fat_base(self.layout, 1)
let mut differ = 0
for i = 0; i < len; i = i + 1 {
if self.data[a + i] != self.data[b + i] {
differ = differ + 1
}
}
if differ > 0 {
issues.push(
issue("VFY015", "FAT copy 0 and 1 differ in \{differ} bytes", 0),
)
}
}
}
///|
fn Volume::verify_tree(
self : Volume,
dir_cluster : Int,
path : String,
issues : Array[VerifyIssue],
used : Array[Int],
) -> Unit {
match read_directory(self.data, self.layout, dir_cluster) {
Err(err) =>
issues.push(issue("VFY020", "\{path}: \{err.message()}", dir_cluster))
Ok(records) =>
for i = 0; i < records.length(); i = i + 1 {
let record = records[i]
if record.is_dot() {
continue
}
if record.lfn_offsets.length() > 0 {
let checksum = lfn_checksum(record.short_name)
for j = 0; j < record.lfn_offsets.length(); j = j + 1 {
match u8_at(self.data, record.lfn_offsets[j] + 13) {
Err(_) =>
issues.push(
issue("VFY021", "truncated LFN at \{path}", dir_cluster),
)
Ok(found) =>
if found != checksum {
issues.push(
issue(
"VFY022",
"\{path}/\{record.name} LFN checksum mismatch",
dir_cluster,
),
)
}
}
}
}
if record.cluster == 0 &&
record.size > 0 &&
!record.is_dir() &&
!record.is_volume_id() {
issues.push(
issue(
"VFY023",
"\{path}/\{record.name} has size \{record.size} but cluster 0",
0,
),
)
}
if record.cluster != 0 && !record.is_volume_id() {
match follow_chain(self.data, self.layout, record.cluster) {
Err(err) =>
issues.push(
issue(
"VFY024",
"\{path}/\{record.name}: \{err.message()}",
record.cluster,
),
)
Ok(chain) => {
self.mark_chain(chain, path + "/" + record.name, issues, used)
if !record.is_dir() {
let capacity = chain_byte_length(self.layout, chain)
if record.size > capacity {
issues.push(
issue(
"VFY025",
"\{path}/\{record.name} size \{record.size} exceeds chain \{capacity}",
record.cluster,
),
)
}
}
if record.is_dir() {
self.verify_tree(
record.cluster,
join_path(path, record.name),
issues,
used,
)
}
}
}
}
}
}
}
///|
fn Volume::mark_chain(
self : Volume,
chain : Array[Int],
path : String,
issues : Array[VerifyIssue],
used : Array[Int],
) -> Unit {
let _ = self
for i = 0; i < chain.length(); i = i + 1 {
let cluster = chain[i]
if cluster < 2 || cluster >= used.length() {
issues.push(
issue("VFY030", "\{path} uses invalid cluster \{cluster}", cluster),
)
continue
}
if used[cluster] == 3 {
issues.push(
issue("VFY031", "\{path} cross-links cluster \{cluster}", cluster),
)
} else if used[cluster] == 2 {
issues.push(
issue("VFY032", "\{path} uses a bad cluster \{cluster}", cluster),
)
} else {
used[cluster] = 3
}
}
}
///|
fn Volume::verify_lost_clusters(
self : Volume,
issues : Array[VerifyIssue],
used : Array[Int],
) -> Unit {
let mut lost = 0
for cluster = 2
cluster <= self.layout.cluster_count + 1
cluster = cluster + 1 {
if used[cluster] == 1 {
lost = lost + 1
}
}
if lost > 0 {
issues.push(
issue(
"VFY040",
"\{lost} allocated clusters are not reachable from directories",
0,
),
)
}
}
///|
pub fn Volume::issue_codes(self : Volume) -> Array[String] {
let issues = self.verify()
let codes : Array[String] = []
for i = 0; i < issues.length(); i = i + 1 {
codes.push(issues[i].code)
}
codes
}