#![forbid(unsafe_code)]
use crate::error::Result;
use crate::inode::InodeReader;
use crate::ondisk::{FileType, Inode, Timestamp};
use std::io::{Read, Seek};
#[derive(Debug, Clone)]
pub struct DeletedInode {
pub ino: u64,
pub file_type: FileType,
pub mode: u16,
pub uid: u32,
pub gid: u32,
pub size: u64,
pub atime: Timestamp,
pub mtime: Timestamp,
pub ctime: Timestamp,
pub crtime: Timestamp,
pub dtime: u32,
pub recoverability: f64,
}
pub fn find_deleted_inodes<R: Read + Seek>(
reader: &mut InodeReader<R>,
) -> Result<Vec<DeletedInode>> {
let all_inodes = reader.iter_all_inodes()?;
let mut deleted = Vec::new();
for (ino, inode) in &all_inodes {
if !inode.is_deleted() {
continue;
}
let recoverability = estimate_recoverability(reader, *ino, inode);
deleted.push(DeletedInode {
ino: *ino,
file_type: inode.file_type(),
mode: inode.mode,
uid: inode.uid,
gid: inode.gid,
size: inode.size,
atime: inode.atime,
mtime: inode.mtime,
ctime: inode.ctime,
crtime: inode.crtime,
dtime: inode.dtime,
recoverability,
});
}
Ok(deleted)
}
pub fn find_orphan_inodes<R: Read + Seek>(
reader: &mut InodeReader<R>,
) -> Result<Vec<DeletedInode>> {
let all_inodes = reader.iter_all_inodes()?;
let mut orphans = Vec::new();
for (ino, inode) in &all_inodes {
if !inode.is_orphan() {
continue;
}
let recoverability = estimate_recoverability(reader, *ino, inode);
orphans.push(DeletedInode {
ino: *ino,
file_type: inode.file_type(),
mode: inode.mode,
uid: inode.uid,
gid: inode.gid,
size: inode.size,
atime: inode.atime,
mtime: inode.mtime,
ctime: inode.ctime,
crtime: inode.crtime,
dtime: inode.dtime,
recoverability,
});
}
Ok(orphans)
}
fn estimate_recoverability<R: Read + Seek>(
reader: &mut InodeReader<R>,
ino: u64,
inode: &Inode,
) -> f64 {
if inode.size == 0 {
return 0.0;
}
let mappings = match reader.inode_block_map(ino) {
Ok(m) => m,
Err(_) => return 0.0,
};
if mappings.is_empty() {
return 0.0;
}
let total_blocks: u64 = mappings.iter().map(|m| m.length).sum();
if total_blocks == 0 {
return 0.0;
}
let mut free_blocks = 0u64;
for mapping in &mappings {
for i in 0..mapping.length {
let block = mapping.physical_block + i;
if let Ok(false) = reader.is_block_allocated(block) {
free_blocks += 1;
}
}
}
free_blocks as f64 / total_blocks as f64
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::BlockReader;
use crate::inode::InodeReader;
use std::io::Cursor;
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 block_reader = BlockReader::open(Cursor::new(data)).ok()?;
Some(InodeReader::new(block_reader))
}
#[test]
fn no_deleted_inodes_in_fresh_image() {
let mut reader = if let Some(r) = open_minimal() {
r
} else {
eprintln!("skip: minimal.img not found");
return;
};
let deleted = find_deleted_inodes(&mut reader).unwrap();
assert_eq!(deleted.len(), 0);
}
#[test]
fn deleted_inode_has_dtime_set() {
use crate::ondisk::Inode;
let mut buf = vec![0u8; 256];
buf[0x00] = 0x80;
buf[0x01] = 0x81; buf[0x04] = 100; buf[0x14] = 0x01; buf[0x1A] = 0; let inode = Inode::parse(&buf, 256).unwrap();
assert!(inode.is_deleted(), "dtime != 0 means deleted");
assert!(!inode.is_orphan(), "dtime set means not orphan");
assert_eq!(inode.dtime, 1);
}
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 block_reader = BlockReader::open(Cursor::new(data)).ok()?;
Some(InodeReader::new(block_reader))
}
#[test]
fn forensic_image_has_deleted_inodes() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let deleted = find_deleted_inodes(&mut reader).unwrap();
assert!(
deleted.len() >= 2,
"expected at least 2 deleted inodes, got {}",
deleted.len()
);
let inos: Vec<u64> = deleted.iter().map(|d| d.ino).collect();
assert!(
inos.contains(&21),
"expected inode 21 in deleted list, got {inos:?}"
);
assert!(
inos.contains(&22),
"expected inode 22 in deleted list, got {inos:?}"
);
}
#[test]
fn deleted_inode_has_nonzero_dtime() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let deleted = find_deleted_inodes(&mut reader).unwrap();
assert!(!deleted.is_empty(), "expected deleted inodes");
for d in &deleted {
assert!(d.dtime > 0, "inode {} has dtime=0 but should be >0", d.ino);
}
}
#[test]
fn deleted_inode_recoverability_is_valid() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let deleted = find_deleted_inodes(&mut reader).unwrap();
assert!(!deleted.is_empty(), "expected deleted inodes");
for d in &deleted {
assert!(
(0.0..=1.0).contains(&d.recoverability),
"inode {} recoverability {} not in [0.0, 1.0]",
d.ino,
d.recoverability
);
}
}
#[test]
fn deleted_inode_21_is_regular_file() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip: forensic.img not found");
return;
};
let deleted = find_deleted_inodes(&mut reader).unwrap();
let ino21 = deleted
.iter()
.find(|d| d.ino == 21)
.expect("inode 21 should be in deleted list");
assert_eq!(
ino21.file_type,
FileType::RegularFile,
"inode 21 should be RegularFile, got {:?}",
ino21.file_type
);
}
#[test]
fn find_orphan_inodes_on_forensic_img() {
let mut reader = if let Some(r) = open_forensic() {
r
} else {
eprintln!("skip");
return;
};
let orphans = find_orphan_inodes(&mut reader).unwrap();
for o in &orphans {
assert_eq!(o.dtime, 0, "orphan should have dtime=0");
assert!((0.0..=1.0).contains(&o.recoverability));
}
}
#[test]
fn orphan_inode_has_no_dtime() {
use crate::ondisk::Inode;
let mut buf = vec![0u8; 256];
buf[0x00] = 0x80;
buf[0x01] = 0x81; buf[0x04] = 100; buf[0x1A] = 0; let inode = Inode::parse(&buf, 256).unwrap();
assert!(!inode.is_deleted(), "no dtime means not deleted");
assert!(
inode.is_orphan(),
"links_count=0, dtime=0, mode!=0 is orphan"
);
}
}