ext4fs/forensic/
history.rs1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::forensic::journal::Journal;
5use crate::inode::InodeReader;
6use crate::ondisk::Inode;
7use std::io::{Read, Seek};
8
9#[derive(Debug, Clone)]
11pub struct InodeVersion {
12 pub sequence: u64,
14 pub commit_time: u64,
16 pub inode: Inode,
18}
19
20pub fn inode_history<R: Read + Seek>(
22 reader: &mut InodeReader<R>,
23 journal: &Journal,
24 ino: u64,
25) -> Result<Vec<InodeVersion>> {
26 let (ipg, inode_size_u64, inode_size_u16, block_size, journal_ino) = {
27 let sb = reader.block_reader().superblock();
28 (
29 u64::from(sb.inodes_per_group),
30 u64::from(sb.inode_size),
31 sb.inode_size,
32 u64::from(sb.block_size),
33 u64::from(sb.journal_inum),
34 )
35 };
36
37 let group = ((ino - 1) / ipg) as u32;
38 let index = (ino - 1) % ipg;
39
40 let inode_table = match reader.block_reader().inode_table_block(group) {
41 Ok(t) => t,
42 Err(_) => return Ok(Vec::new()),
43 };
44
45 let inode_offset_in_table = index * inode_size_u64;
46 let target_block = inode_table + inode_offset_in_table / block_size;
47 let offset_in_block = (inode_offset_in_table % block_size) as usize;
48
49 let journal_data = reader.read_inode_data(journal_ino)?;
50 let j_block_size = journal.block_size as usize;
51
52 let mut versions = Vec::new();
53 for txn in &journal.transactions {
54 for mapping in &txn.mappings {
55 if mapping.filesystem_block == target_block {
56 let j_offset = mapping.journal_block as usize * j_block_size;
57 if j_offset + j_block_size <= journal_data.len() {
58 let block_data = &journal_data[j_offset..j_offset + j_block_size];
59 let end = offset_in_block + inode_size_u64 as usize;
60 if end <= block_data.len() {
61 if let Ok(inode) =
62 Inode::parse(&block_data[offset_in_block..end], inode_size_u16)
63 {
64 if inode.mode != 0 {
65 versions.push(InodeVersion {
66 sequence: u64::from(txn.sequence),
67 commit_time: txn.commit_seconds as u64,
68 inode,
69 });
70 }
71 }
72 }
73 }
74 }
75 }
76 }
77
78 versions.sort_by_key(|v| v.sequence);
79 Ok(versions)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::block::BlockReader;
86 use crate::forensic::journal::parse_journal;
87 use std::io::Cursor;
88
89 fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
90 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
91 let data = std::fs::read(path).ok()?;
92 let br = BlockReader::open(Cursor::new(data)).ok()?;
93 Some(InodeReader::new(br))
94 }
95
96 #[test]
97 fn inode_history_for_hello_txt() {
98 let mut reader = if let Some(r) = open_forensic() {
99 r
100 } else {
101 eprintln!("skip");
102 return;
103 };
104 let journal = parse_journal(&mut reader).unwrap();
105 let versions = inode_history(&mut reader, &journal, 12).unwrap();
106 eprintln!("hello.txt versions: {}", versions.len());
108 for v in &versions {
109 assert!(v.sequence > 0);
110 assert!(v.commit_time > 0);
111 assert!(v.inode.mode != 0);
112 }
113 }
114
115 #[test]
116 fn inode_history_for_deleted_file() {
117 let mut reader = if let Some(r) = open_forensic() {
118 r
119 } else {
120 eprintln!("skip");
121 return;
122 };
123 let journal = parse_journal(&mut reader).unwrap();
124 let versions = inode_history(&mut reader, &journal, 21).unwrap();
126 eprintln!("deleted file (ino 21) versions: {}", versions.len());
127 }
129
130 #[test]
131 fn inode_history_nonexistent() {
132 let mut reader = if let Some(r) = open_forensic() {
133 r
134 } else {
135 eprintln!("skip");
136 return;
137 };
138 let journal = parse_journal(&mut reader).unwrap();
139 let versions = inode_history(&mut reader, &journal, 999_999).unwrap();
140 assert!(versions.is_empty());
141 }
142
143 #[test]
144 fn inode_history_sorted_by_sequence() {
145 let mut reader = if let Some(r) = open_forensic() {
146 r
147 } else {
148 eprintln!("skip");
149 return;
150 };
151 let journal = parse_journal(&mut reader).unwrap();
152 let versions = inode_history(&mut reader, &journal, 12).unwrap();
153 for w in versions.windows(2) {
154 assert!(w[0].sequence <= w[1].sequence);
155 }
156 }
157}