#![forbid(unsafe_code)]
use crate::error::Result;
use crate::inode::InodeReader;
use crate::ondisk::{DirEntryType, FileType};
use std::io::{Read, Seek};
#[derive(Debug, Clone)]
pub struct RecoveredDirEntry {
pub parent_ino: u64,
pub inode: u32,
pub name: String,
pub file_type: DirEntryType,
}
pub fn recover_dir_entries<R: Read + Seek>(
reader: &mut InodeReader<R>,
dir_ino: u64,
) -> Result<Vec<RecoveredDirEntry>> {
let block_size = reader.block_reader().superblock().block_size as usize;
let data = reader.read_inode_data(dir_ino)?;
let mut recovered = Vec::new();
let mut block_offset = 0;
while block_offset < data.len() {
let block_end = (block_offset + block_size).min(data.len());
let mut offset = block_offset;
while offset < block_end {
if offset + 8 > block_end {
break;
}
let rec_len = u16::from_le_bytes([data[offset + 4], data[offset + 5]]) as usize;
if rec_len == 0 || offset + rec_len > block_end {
break;
}
let name_len = data[offset + 6] as usize;
let actual_size = ((8 + name_len) + 3) & !3;
if rec_len > actual_size && actual_size > 0 {
let gap_start = offset + actual_size;
let gap_end = offset + rec_len;
if gap_end - gap_start >= 8 {
let gap = &data[gap_start..gap_end];
let del_inode = u32::from_le_bytes([gap[0], gap[1], gap[2], gap[3]]);
let del_name_len = gap[6] as usize;
let del_file_type_raw = gap[7];
if del_inode > 0
&& del_name_len > 0
&& del_name_len <= 255
&& gap_start + 8 + del_name_len <= gap_end
{
let del_name = String::from_utf8_lossy(
&data[gap_start + 8..gap_start + 8 + del_name_len],
)
.to_string();
if del_name.chars().all(|c| !c.is_control() || c == '\0')
&& !del_name.is_empty()
{
let file_type = DirEntryType::from(del_file_type_raw);
recovered.push(RecoveredDirEntry {
parent_ino: dir_ino,
inode: del_inode,
name: del_name,
file_type,
});
}
}
}
}
offset += rec_len;
}
block_offset += block_size;
}
Ok(recovered)
}
pub fn recover_all_dir_entries<R: Read + Seek>(
reader: &mut InodeReader<R>,
) -> Result<Vec<RecoveredDirEntry>> {
let all_inodes = reader.iter_all_inodes()?;
let mut all_recovered = Vec::new();
for (ino, inode) in &all_inodes {
if inode.file_type() != FileType::Directory || inode.mode == 0 {
continue;
}
if let Ok(entries) = recover_dir_entries(reader, *ino) {
all_recovered.extend(entries);
}
}
Ok(all_recovered)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::BlockReader;
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 open_minimal() -> Option<InodeReader<Cursor<Vec<u8>>>> {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
let data = std::fs::read(path).ok()?;
let br = BlockReader::open(Cursor::new(data)).ok()?;
Some(InodeReader::new(br))
}
#[test]
fn recover_deleted_entries_in_forensic_root() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip");
return;
};
let recovered = recover_dir_entries(&mut reader, 2).unwrap();
let names: Vec<&str> = recovered.iter().map(|e| e.name.as_str()).collect();
eprintln!("Recovered entries: {names:?}");
}
#[test]
fn no_deleted_entries_in_clean_dir() {
let mut reader = if let Some(r) = open_minimal() {
r
} else {
eprintln!("skip");
return;
};
let recovered = recover_dir_entries(&mut reader, 2).unwrap();
let _ = recovered;
}
#[test]
fn recover_all_returns_without_error() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip");
return;
};
let all = recover_all_dir_entries(&mut reader).unwrap();
eprintln!("Total recovered entries across all dirs: {}", all.len());
}
}