use crate::error::Result;
use crate::extent;
use crate::fs::Filesystem;
use crate::indirect;
use crate::inode::{Inode, InodeFlags};
#[derive(Debug, Clone, Default)]
pub struct VerifyReport {
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub inodes_walked: u32,
pub blocks_claimed: u64,
}
impl VerifyReport {
pub fn is_clean(&self) -> bool {
self.errors.is_empty()
}
pub fn summary(&self) -> String {
format!(
"verify: {} errors, {} warnings, {} inodes, {} blocks claimed",
self.errors.len(),
self.warnings.len(),
self.inodes_walked,
self.blocks_claimed,
)
}
}
pub fn verify(fs: &Filesystem) -> Result<VerifyReport> {
let mut report = VerifyReport::default();
verify_superblock(fs, &mut report);
verify_bgds(fs, &mut report);
let bs = fs.sb.block_size() as u64;
let total_blocks = fs.sb.blocks_count;
if total_blocks == 0 || bs == 0 {
return Ok(report);
}
if total_blocks > u32::MAX as u64 {
report.errors.push(
"verify: volume exceeds u32 blocks (multi-group triple-indirect tier untested)".into(),
);
return Ok(report);
}
let mut claimed = ClaimedBitmap::new(total_blocks as u32);
mark_metadata_blocks(fs, &mut claimed);
walk_all_allocated_inodes(fs, &mut claimed, &mut report)?;
reconcile_with_bitmaps(fs, &claimed, &mut report)?;
report.blocks_claimed = claimed.count_set();
Ok(report)
}
fn verify_superblock(fs: &Filesystem, r: &mut VerifyReport) {
let sb = &fs.sb;
if sb.magic != crate::superblock::EXT4_MAGIC {
r.errors.push(format!(
"superblock: magic 0x{:04X} != 0x{:04X}",
sb.magic,
crate::superblock::EXT4_MAGIC
));
}
let bs = sb.block_size() as u64;
if bs == 0 || !bs.is_power_of_two() || !(1024..=65536).contains(&bs) {
r.errors.push(format!(
"superblock: block_size {} out of range [1024..=65536, power of 2]",
bs
));
}
if sb.blocks_count == 0 {
r.errors.push("superblock: blocks_count == 0".into());
}
if sb.free_blocks_count > sb.blocks_count {
r.errors.push(format!(
"superblock: free_blocks_count {} > blocks_count {}",
sb.free_blocks_count, sb.blocks_count
));
}
if sb.free_inodes_count > sb.inodes_count {
r.errors.push(format!(
"superblock: free_inodes_count {} > inodes_count {}",
sb.free_inodes_count, sb.inodes_count
));
}
}
fn verify_bgds(fs: &Filesystem, r: &mut VerifyReport) {
let total = fs.sb.blocks_count;
let bpg = fs.sb.blocks_per_group as u64;
let ipg = fs.sb.inodes_per_group;
for (gi, bgd) in fs.groups.iter().enumerate() {
if bgd.block_bitmap >= total {
r.errors.push(format!(
"bgd[{}]: block_bitmap {} >= blocks_count {}",
gi, bgd.block_bitmap, total
));
}
if bgd.inode_bitmap >= total {
r.errors.push(format!(
"bgd[{}]: inode_bitmap {} >= blocks_count {}",
gi, bgd.inode_bitmap, total
));
}
if bgd.inode_table >= total {
r.errors.push(format!(
"bgd[{}]: inode_table {} >= blocks_count {}",
gi, bgd.inode_table, total
));
}
if bgd.free_blocks_count as u64 > bpg {
r.errors.push(format!(
"bgd[{}]: free_blocks_count {} > blocks_per_group {}",
gi, bgd.free_blocks_count, bpg
));
}
if bgd.free_inodes_count > ipg {
r.errors.push(format!(
"bgd[{}]: free_inodes_count {} > inodes_per_group {}",
gi, bgd.free_inodes_count, ipg
));
}
}
}
struct ClaimedBitmap {
bits: Vec<u8>,
total: u32,
}
impl ClaimedBitmap {
fn new(total_blocks: u32) -> Self {
Self {
bits: vec![0u8; total_blocks.div_ceil(8) as usize],
total: total_blocks,
}
}
fn set(&mut self, block: u32) -> bool {
debug_assert!(block < self.total);
let byte = (block / 8) as usize;
let mask = 1u8 << (block % 8);
let was = self.bits[byte] & mask != 0;
self.bits[byte] |= mask;
was
}
fn get(&self, block: u32) -> bool {
debug_assert!(block < self.total);
let byte = (block / 8) as usize;
let mask = 1u8 << (block % 8);
self.bits[byte] & mask != 0
}
fn count_set(&self) -> u64 {
self.bits.iter().map(|b| b.count_ones() as u64).sum()
}
}
fn mark_metadata_blocks(fs: &Filesystem, claimed: &mut ClaimedBitmap) {
let bs = fs.sb.block_size();
let sb_logical_block = 1024 / bs;
if sb_logical_block < claimed.total {
claimed.set(sb_logical_block);
}
if 0 < claimed.total {
claimed.set(0);
}
for bgd in &fs.groups {
if bgd.block_bitmap < claimed.total as u64 {
claimed.set(bgd.block_bitmap as u32);
}
if bgd.inode_bitmap < claimed.total as u64 {
claimed.set(bgd.inode_bitmap as u32);
}
let inodes_per_group = fs.sb.inodes_per_group as u64;
let inode_size = fs.sb.inode_size as u64;
let it_blocks = (inodes_per_group * inode_size).div_ceil(bs as u64);
for i in 0..it_blocks {
let b = bgd.inode_table + i;
if b < claimed.total as u64 {
claimed.set(b as u32);
}
}
}
let first_data = fs.sb.first_data_block as u64;
let bgt = first_data + 1;
if bgt < claimed.total as u64 {
claimed.set(bgt as u32);
}
}
fn walk_all_allocated_inodes(
fs: &Filesystem,
claimed: &mut ClaimedBitmap,
r: &mut VerifyReport,
) -> Result<()> {
let bs = fs.sb.block_size() as u64;
let ipg = fs.sb.inodes_per_group;
for (gi, bgd) in fs.groups.iter().enumerate() {
let mut bm = vec![0u8; bs as usize];
fs.dev.read_at(bgd.inode_bitmap * bs, &mut bm)?;
for bit in 0..ipg {
let byte = (bit / 8) as usize;
let mask = 1u8 << (bit % 8);
if byte >= bm.len() || bm[byte] & mask == 0 {
continue;
}
let ino = (gi as u32) * ipg + bit + 1;
if ino < fs.sb.first_inode && ino != 2 {
continue;
}
r.inodes_walked += 1;
let raw = match fs.read_inode_raw(ino) {
Ok(b) => b,
Err(e) => {
r.errors
.push(format!("inode {}: read failed: {:?}", ino, e));
continue;
}
};
let inode = match Inode::parse(&raw) {
Ok(i) => i,
Err(e) => {
r.errors
.push(format!("inode {}: parse failed: {:?}", ino, e));
continue;
}
};
if (inode.flags & InodeFlags::INLINE_DATA.bits()) != 0 {
continue;
}
if inode.blocks == 0 {
continue;
}
walk_inode_block_tree(fs, ino, &inode, claimed, r)?;
}
}
Ok(())
}
fn walk_inode_block_tree(
fs: &Filesystem,
ino: u32,
inode: &Inode,
claimed: &mut ClaimedBitmap,
r: &mut VerifyReport,
) -> Result<()> {
let bs = fs.sb.block_size() as u64;
let bs32 = fs.sb.block_size();
let n_blocks = inode.size.div_ceil(bs);
let total = claimed.total as u64;
if (inode.flags & InodeFlags::EXTENTS.bits()) != 0 {
let extents = match extent::collect_all(&inode.block, fs.dev.as_ref(), bs32) {
Ok(es) => es,
Err(e) => {
r.errors
.push(format!("inode {}: extent walk failed: {:?}", ino, e));
return Ok(());
}
};
for ext in &extents {
for off in 0..ext.length as u64 {
let phys = ext.physical_block + off;
if phys >= total {
r.errors.push(format!(
"inode {}: extent block {} >= blocks_count {}",
ino, phys, total
));
continue;
}
if claimed.set(phys as u32) {
r.errors.push(format!(
"inode {}: block {} double-claimed (extent)",
ino, phys
));
}
}
}
} else {
for logical in 0..n_blocks {
let phys_opt = indirect::map_logical_any(
&inode.block,
inode.flags,
fs.dev.as_ref(),
bs32,
logical,
)?;
if let Some(phys) = phys_opt {
if phys >= total {
r.errors.push(format!(
"inode {}: indirect block {} >= blocks_count {}",
ino, phys, total
));
continue;
}
if claimed.set(phys as u32) {
r.errors.push(format!(
"inode {}: block {} double-claimed (indirect)",
ino, phys
));
}
}
}
for slot in [12usize, 13, 14] {
let off = slot * 4;
let p = u32::from_le_bytes(inode.block[off..off + 4].try_into().unwrap()) as u64;
if p != 0 && p < total {
claimed.set(p as u32);
let mut buf = vec![0u8; bs32 as usize];
if fs.dev.read_at(p * bs, &mut buf).is_ok() {
let ppb = (bs32 / 4) as usize;
for i in 0..ppb {
let off = i * 4;
let inner =
u32::from_le_bytes(buf[off..off + 4].try_into().unwrap()) as u64;
if inner != 0 && inner < total && slot >= 13 {
claimed.set(inner as u32);
}
}
}
}
}
}
Ok(())
}
fn reconcile_with_bitmaps(
fs: &Filesystem,
claimed: &ClaimedBitmap,
r: &mut VerifyReport,
) -> Result<()> {
let bs = fs.sb.block_size() as u64;
let bpg = fs.sb.blocks_per_group as u64;
let total = fs.sb.blocks_count;
let first_data = fs.sb.first_data_block as u64;
for (gi, bgd) in fs.groups.iter().enumerate() {
let mut bm = vec![0u8; bs as usize];
fs.dev.read_at(bgd.block_bitmap * bs, &mut bm)?;
let group_start = first_data + (gi as u64) * bpg;
let group_end = (group_start + bpg).min(total);
for b in group_start..group_end {
let in_bitmap = {
let bit_idx = b - group_start;
let byte = (bit_idx / 8) as usize;
let mask = 1u8 << (bit_idx % 8);
byte < bm.len() && bm[byte] & mask != 0
};
let in_claimed = claimed.get(b as u32);
match (in_bitmap, in_claimed) {
(true, true) => {} (false, false) => {} (false, true) => {
r.errors.push(format!(
"block {} (group {}): claimed by inode but marked free in bitmap",
b, gi
));
}
(true, false) => {
r.warnings.push(format!(
"block {} (group {}): allocated but unclaimed",
b, gi
));
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn report_default_is_clean() {
let r = VerifyReport::default();
assert!(r.is_clean());
assert_eq!(
r.summary(),
"verify: 0 errors, 0 warnings, 0 inodes, 0 blocks claimed"
);
}
#[test]
fn claimed_bitmap_set_get_count() {
let mut bm = ClaimedBitmap::new(100);
assert_eq!(bm.count_set(), 0);
assert!(!bm.set(50));
assert!(bm.set(50)); assert!(bm.get(50));
assert!(!bm.get(51));
assert!(!bm.set(99));
assert_eq!(bm.count_set(), 2);
}
}