#![forbid(unsafe_code)]
use crate::error::{Ext4Error, Result};
use crate::inode::InodeReader;
use std::io::{Read, Seek};
#[derive(Debug, Clone)]
pub struct BlockRange {
pub start: u64,
pub length: u64,
}
#[derive(Debug, Clone)]
pub struct RecoveryResult {
pub data: Vec<u8>,
pub expected_size: u64,
pub recovered_size: u64,
pub overwritten_ranges: Vec<BlockRange>,
}
impl RecoveryResult {
pub fn recovery_percentage(&self) -> f64 {
if self.expected_size == 0 {
return 0.0;
}
(self.recovered_size as f64 / self.expected_size as f64) * 100.0
}
}
pub fn recover_file<R: Read + Seek>(
reader: &mut InodeReader<R>,
ino: u64,
) -> Result<RecoveryResult> {
let inode = reader.read_inode(ino)?;
let expected_size = inode.size;
if expected_size == 0 {
return Ok(RecoveryResult {
data: Vec::new(),
expected_size: 0,
recovered_size: 0,
overwritten_ranges: Vec::new(),
});
}
let mappings = match reader.inode_block_map(ino) {
Ok(m) => m,
Err(_) => {
return Err(Ext4Error::RecoveryFailed {
ino,
reason: "extent tree root is zeroed (common for small deleted ext4 files)".into(),
});
}
};
if mappings.is_empty() {
return Err(Ext4Error::RecoveryFailed {
ino,
reason: "no block mappings found".into(),
});
}
let block_size = u64::from(reader.block_reader().block_size());
let mut data = Vec::with_capacity(expected_size as usize);
let mut overwritten_ranges = Vec::new();
let mut overwritten_bytes: u64 = 0;
for mapping in &mappings {
for i in 0..mapping.length {
let block = mapping.physical_block + i;
let is_allocated = reader.is_block_allocated(block).unwrap_or(true);
if is_allocated {
overwritten_ranges.push(BlockRange {
start: (mapping.logical_block + i) * block_size,
length: block_size,
});
let remaining = (expected_size as usize).saturating_sub(data.len());
let fill = remaining.min(block_size as usize);
data.extend(vec![0u8; fill]);
overwritten_bytes += fill as u64;
} else if let Ok(block_data) = reader.block_reader_mut().read_block(block) {
let remaining = (expected_size as usize).saturating_sub(data.len());
let to_copy = remaining.min(block_data.len());
data.extend_from_slice(&block_data[..to_copy]);
} else {
let remaining = (expected_size as usize).saturating_sub(data.len());
let fill = remaining.min(block_size as usize);
data.extend(vec![0u8; fill]);
overwritten_bytes += fill as u64;
}
if data.len() >= expected_size as usize {
break;
}
}
if data.len() >= expected_size as usize {
break;
}
}
data.truncate(expected_size as usize);
let recovered_size = expected_size.saturating_sub(overwritten_bytes);
Ok(RecoveryResult {
data,
expected_size,
recovered_size,
overwritten_ranges,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::BlockReader;
use crate::inode::InodeReader;
use std::io::Cursor;
fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
let data = std::fs::read(path).ok()?;
let br = BlockReader::open(Cursor::new(data)).ok()?;
Some(InodeReader::new(br))
}
fn forensic_raw() -> Option<Vec<u8>> {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
std::fs::read(path).ok()
}
fn inode_byte_offset(reader: &InodeReader<Cursor<Vec<u8>>>, ino: u64) -> (usize, usize) {
let sb = reader.block_reader().superblock();
let ipg = u64::from(sb.inodes_per_group);
let inode_size = u64::from(sb.inode_size);
let block_size = u64::from(sb.block_size);
let group = ((ino - 1) / ipg) as u32;
let index = (ino - 1) % ipg;
let inode_table = reader.block_reader().inode_table_block(group).unwrap();
let offset = inode_table * block_size + index * inode_size;
(offset as usize, inode_size as usize)
}
#[test]
fn recovery_result_fields() {
let result = RecoveryResult {
data: vec![1, 2, 3],
expected_size: 100,
recovered_size: 3,
overwritten_ranges: vec![BlockRange {
start: 10,
length: 97,
}],
};
assert_eq!(result.recovery_percentage(), 3.0);
}
#[test]
fn recovery_percentage_zero_expected() {
let result = RecoveryResult {
data: Vec::new(),
expected_size: 0,
recovered_size: 0,
overwritten_ranges: Vec::new(),
};
assert_eq!(result.recovery_percentage(), 0.0);
}
#[test]
fn recover_deleted_inode_21_hits_zero_size_path() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let result = recover_file(&mut reader, 21).unwrap();
assert_eq!(result.expected_size, 0);
assert_eq!(result.recovered_size, 0);
assert!(result.data.is_empty());
assert!(result.overwritten_ranges.is_empty());
assert_eq!(result.recovery_percentage(), 0.0);
}
#[test]
fn recover_deleted_inode_22_hits_zero_size_path() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let result = recover_file(&mut reader, 22).unwrap();
assert_eq!(result.expected_size, 0);
assert_eq!(result.recovered_size, 0);
}
#[test]
fn recover_live_file_exercises_main_loop() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let result = recover_file(&mut reader, 12);
if let Ok(r) = result {
assert!(r.expected_size > 0, "live file should have size > 0");
assert_eq!(r.data.len(), r.expected_size as usize);
assert!(r.recovery_percentage() >= 0.0);
assert!(r.recovery_percentage() <= 100.0);
} else {
}
}
#[test]
fn recover_zeroed_extent_tree_error() {
let reader_orig = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let mut data = forensic_raw().unwrap();
let (off, _isz) = inode_byte_offset(&reader_orig, 12);
let size_lo = u32::from_le_bytes([
data[off + 0x04],
data[off + 0x05],
data[off + 0x06],
data[off + 0x07],
]);
assert!(size_lo > 0, "inode 12 should have nonzero size");
for b in &mut data[off + 0x28..off + 0x64] {
*b = 0;
}
let br = BlockReader::open(Cursor::new(data)).unwrap();
let mut reader = InodeReader::new(br);
let result = recover_file(&mut reader, 12);
match result {
Err(Ext4Error::RecoveryFailed { ino, reason }) => {
assert_eq!(ino, 12);
assert!(reason.contains("zeroed"), "reason: {reason}");
}
other => panic!("expected RecoveryFailed, got: {other:?}"),
}
}
#[test]
fn recover_empty_mappings_error() {
let reader_orig = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let mut data = forensic_raw().unwrap();
let (off, _isz) = inode_byte_offset(&reader_orig, 12);
let iblock_off = off + 0x28;
data[iblock_off] = 0x0A;
data[iblock_off + 1] = 0xF3; data[iblock_off + 2] = 0x00;
data[iblock_off + 3] = 0x00; data[iblock_off + 4] = 0x04;
data[iblock_off + 5] = 0x00; data[iblock_off + 6] = 0x00;
data[iblock_off + 7] = 0x00; for b in &mut data[iblock_off + 8..iblock_off + 60] {
*b = 0;
}
let br = BlockReader::open(Cursor::new(data)).unwrap();
let mut reader = InodeReader::new(br);
let result = recover_file(&mut reader, 12);
match result {
Err(Ext4Error::RecoveryFailed { ino, reason }) => {
assert_eq!(ino, 12);
assert!(reason.contains("no block mappings"), "reason: {reason}");
}
other => panic!("expected RecoveryFailed(no block mappings), got: {other:?}"),
}
}
#[test]
fn recover_with_unallocated_blocks() {
let reader_orig = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let mut data = forensic_raw().unwrap();
let reader_tmp = open_forensic().unwrap();
let mappings = reader_tmp.inode_block_map(12).unwrap();
if mappings.is_empty() {
eprintln!("skip: inode 12 has no block mappings");
return;
}
let sb = reader_orig.block_reader().superblock();
let bitmap_block = reader_orig.block_reader().block_bitmap_block(0).unwrap();
let block_size = sb.block_size as usize;
let bitmap_offset = bitmap_block as usize * block_size;
for mapping in &mappings {
for i in 0..mapping.length {
let block = mapping.physical_block + i;
let byte_idx = block as usize / 8;
let bit_idx = block as usize % 8;
if bitmap_offset + byte_idx < data.len() {
data[bitmap_offset + byte_idx] &= !(1 << bit_idx);
}
}
}
let br = BlockReader::open(Cursor::new(data)).unwrap();
let mut reader = InodeReader::new(br);
let result = recover_file(&mut reader, 12).unwrap();
assert!(result.expected_size > 0);
assert_eq!(result.data.len(), result.expected_size as usize);
assert_eq!(result.recovered_size, result.expected_size);
assert!(result.overwritten_ranges.is_empty());
assert_eq!(result.recovery_percentage(), 100.0);
}
#[test]
fn recover_with_read_block_error() {
let reader_orig = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let mut data = forensic_raw().unwrap();
let (off, _isz) = inode_byte_offset(&reader_orig, 12);
let reader_tmp = open_forensic().unwrap();
let mappings = reader_tmp.inode_block_map(12).unwrap();
if mappings.is_empty() {
eprintln!("skip: inode 12 has no block mappings");
return;
}
let iblock_off = off + 0x28;
let leaf_off = iblock_off + 12;
let huge_block: u32 = 0x00FF_FFFF;
data[leaf_off + 8] = (huge_block & 0xFF) as u8;
data[leaf_off + 9] = ((huge_block >> 8) & 0xFF) as u8;
data[leaf_off + 10] = ((huge_block >> 16) & 0xFF) as u8;
data[leaf_off + 11] = ((huge_block >> 24) & 0xFF) as u8;
data[leaf_off + 6] = 0;
data[leaf_off + 7] = 0;
let sb = reader_orig.block_reader().superblock();
let total_blocks = sb.blocks_count;
let target_block = (total_blocks - 1) as u32;
data[leaf_off + 8] = (target_block & 0xFF) as u8;
data[leaf_off + 9] = ((target_block >> 8) & 0xFF) as u8;
data[leaf_off + 10] = ((target_block >> 16) & 0xFF) as u8;
data[leaf_off + 11] = ((target_block >> 24) & 0xFF) as u8;
let block_size = sb.block_size as usize;
let bitmap_block = reader_orig.block_reader().block_bitmap_block(0).unwrap();
let bitmap_offset = bitmap_block as usize * block_size;
let byte_idx = target_block as usize / 8;
let bit_idx = target_block as usize % 8;
if bitmap_offset + byte_idx < data.len() {
data[bitmap_offset + byte_idx] &= !(1 << bit_idx);
}
let trunc_at = target_block as usize * block_size;
if trunc_at < data.len() {
data.truncate(trunc_at);
}
let br = BlockReader::open(Cursor::new(data)).unwrap();
let mut reader = InodeReader::new(br);
let result = recover_file(&mut reader, 12);
if let Ok(r) = result {
assert_eq!(r.data.len(), r.expected_size as usize);
} else {
}
}
#[test]
fn recover_inode_out_of_range() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let result = recover_file(&mut reader, 999_999);
assert!(result.is_err());
}
}