ext4fs/forensic/
recovery.rs1#![forbid(unsafe_code)]
2
3use crate::error::{Ext4Error, Result};
4use crate::inode::InodeReader;
5use std::io::{Read, Seek};
6
7#[derive(Debug, Clone)]
9pub struct BlockRange {
10 pub start: u64,
11 pub length: u64,
12}
13
14#[derive(Debug, Clone)]
16pub struct RecoveryResult {
17 pub data: Vec<u8>,
18 pub expected_size: u64,
19 pub recovered_size: u64,
20 pub overwritten_ranges: Vec<BlockRange>,
21}
22
23impl RecoveryResult {
24 pub fn recovery_percentage(&self) -> f64 {
25 if self.expected_size == 0 {
26 return 0.0;
27 }
28 (self.recovered_size as f64 / self.expected_size as f64) * 100.0
29 }
30}
31
32pub fn recover_file<R: Read + Seek>(
34 reader: &mut InodeReader<R>,
35 ino: u64,
36) -> Result<RecoveryResult> {
37 let inode = reader.read_inode(ino)?;
38 let expected_size = inode.size;
39
40 if expected_size == 0 {
41 return Ok(RecoveryResult {
42 data: Vec::new(),
43 expected_size: 0,
44 recovered_size: 0,
45 overwritten_ranges: Vec::new(),
46 });
47 }
48
49 let mappings = match reader.inode_block_map(ino) {
50 Ok(m) => m,
51 Err(_) => {
52 return Err(Ext4Error::RecoveryFailed {
53 ino,
54 reason: "extent tree root is zeroed (common for small deleted ext4 files)".into(),
55 });
56 }
57 };
58
59 if mappings.is_empty() {
60 return Err(Ext4Error::RecoveryFailed {
61 ino,
62 reason: "no block mappings found".into(),
63 });
64 }
65
66 let block_size = u64::from(reader.block_reader().block_size());
67 let mut data = Vec::with_capacity(expected_size as usize);
68 let mut overwritten_ranges = Vec::new();
69 let mut overwritten_bytes: u64 = 0;
70
71 for mapping in &mappings {
72 for i in 0..mapping.length {
73 let block = mapping.physical_block + i;
74 let is_allocated = reader.is_block_allocated(block).unwrap_or(true);
75
76 if is_allocated {
77 overwritten_ranges.push(BlockRange {
78 start: (mapping.logical_block + i) * block_size,
79 length: block_size,
80 });
81 let remaining = (expected_size as usize).saturating_sub(data.len());
82 let fill = remaining.min(block_size as usize);
83 data.extend(vec![0u8; fill]);
84 overwritten_bytes += fill as u64;
85 } else if let Ok(block_data) = reader.block_reader_mut().read_block(block) {
86 let remaining = (expected_size as usize).saturating_sub(data.len());
87 let to_copy = remaining.min(block_data.len());
88 data.extend_from_slice(&block_data[..to_copy]);
89 } else {
90 let remaining = (expected_size as usize).saturating_sub(data.len());
91 let fill = remaining.min(block_size as usize);
92 data.extend(vec![0u8; fill]);
93 overwritten_bytes += fill as u64;
94 }
95 if data.len() >= expected_size as usize {
96 break;
97 }
98 }
99 if data.len() >= expected_size as usize {
100 break;
101 }
102 }
103
104 data.truncate(expected_size as usize);
105 let recovered_size = expected_size.saturating_sub(overwritten_bytes);
106
107 Ok(RecoveryResult {
108 data,
109 expected_size,
110 recovered_size,
111 overwritten_ranges,
112 })
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::block::BlockReader;
119 use crate::inode::InodeReader;
120 use std::io::Cursor;
121
122 fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
123 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
124 let data = std::fs::read(path).ok()?;
125 let br = BlockReader::open(Cursor::new(data)).ok()?;
126 Some(InodeReader::new(br))
127 }
128
129 fn forensic_raw() -> Option<Vec<u8>> {
130 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
131 std::fs::read(path).ok()
132 }
133
134 fn inode_byte_offset(reader: &InodeReader<Cursor<Vec<u8>>>, ino: u64) -> (usize, usize) {
137 let sb = reader.block_reader().superblock();
138 let ipg = u64::from(sb.inodes_per_group);
139 let inode_size = u64::from(sb.inode_size);
140 let block_size = u64::from(sb.block_size);
141 let group = ((ino - 1) / ipg) as u32;
142 let index = (ino - 1) % ipg;
143 let inode_table = reader.block_reader().inode_table_block(group).unwrap();
144 let offset = inode_table * block_size + index * inode_size;
145 (offset as usize, inode_size as usize)
146 }
147
148 #[test]
149 fn recovery_result_fields() {
150 let result = RecoveryResult {
151 data: vec![1, 2, 3],
152 expected_size: 100,
153 recovered_size: 3,
154 overwritten_ranges: vec![BlockRange {
155 start: 10,
156 length: 97,
157 }],
158 };
159 assert!((result.recovery_percentage() - 3.0).abs() < f64::EPSILON);
160 }
161
162 #[test]
163 fn recovery_percentage_zero_expected() {
164 let result = RecoveryResult {
165 data: Vec::new(),
166 expected_size: 0,
167 recovered_size: 0,
168 overwritten_ranges: Vec::new(),
169 };
170 assert!((result.recovery_percentage() - 0.0).abs() < f64::EPSILON);
171 }
172
173 #[test]
176 fn recover_deleted_inode_21_hits_zero_size_path() {
177 let mut reader = if let Some(r) = open_forensic() {
178 r
179 } else {
180 eprintln!("skip: forensic.img not found");
181 return;
182 };
183 let result = recover_file(&mut reader, 21).unwrap();
184 assert_eq!(result.expected_size, 0);
185 assert_eq!(result.recovered_size, 0);
186 assert!(result.data.is_empty());
187 assert!(result.overwritten_ranges.is_empty());
188 assert!((result.recovery_percentage() - 0.0).abs() < f64::EPSILON);
189 }
190
191 #[test]
193 fn recover_deleted_inode_22_hits_zero_size_path() {
194 let mut reader = if let Some(r) = open_forensic() {
195 r
196 } else {
197 eprintln!("skip: forensic.img not found");
198 return;
199 };
200 let result = recover_file(&mut reader, 22).unwrap();
201 assert_eq!(result.expected_size, 0);
202 assert_eq!(result.recovered_size, 0);
203 }
204
205 #[test]
208 fn recover_live_file_exercises_main_loop() {
209 let mut reader = if let Some(r) = open_forensic() {
210 r
211 } else {
212 eprintln!("skip: forensic.img not found");
213 return;
214 };
215 let result = recover_file(&mut reader, 12);
216 if let Ok(r) = result {
217 assert!(r.expected_size > 0, "live file should have size > 0");
218 assert_eq!(r.data.len(), r.expected_size as usize);
219 assert!(r.recovery_percentage() >= 0.0);
220 assert!(r.recovery_percentage() <= 100.0);
221 } else {
222 }
224 }
225
226 #[test]
229 fn recover_zeroed_extent_tree_error() {
230 let reader_orig = if let Some(r) = open_forensic() {
231 r
232 } else {
233 eprintln!("skip: forensic.img not found");
234 return;
235 };
236 let mut data = forensic_raw().unwrap();
237 let (off, _isz) = inode_byte_offset(&reader_orig, 12);
239
240 let size_lo = u32::from_le_bytes([
242 data[off + 0x04],
243 data[off + 0x05],
244 data[off + 0x06],
245 data[off + 0x07],
246 ]);
247 assert!(size_lo > 0, "inode 12 should have nonzero size");
248
249 for b in &mut data[off + 0x28..off + 0x64] {
251 *b = 0;
252 }
253
254 let br = BlockReader::open(Cursor::new(data)).unwrap();
255 let mut reader = InodeReader::new(br);
256 let result = recover_file(&mut reader, 12);
257 match result {
258 Err(Ext4Error::RecoveryFailed { ino, reason }) => {
259 assert_eq!(ino, 12);
260 assert!(reason.contains("zeroed"), "reason: {reason}");
261 }
262 other => panic!("expected RecoveryFailed, got: {other:?}"),
263 }
264 }
265
266 #[test]
269 fn recover_empty_mappings_error() {
270 let reader_orig = if let Some(r) = open_forensic() {
271 r
272 } else {
273 eprintln!("skip: forensic.img not found");
274 return;
275 };
276 let mut data = forensic_raw().unwrap();
277 let (off, _isz) = inode_byte_offset(&reader_orig, 12);
278
279 let iblock_off = off + 0x28;
283 data[iblock_off] = 0x0A;
284 data[iblock_off + 1] = 0xF3; data[iblock_off + 2] = 0x00;
286 data[iblock_off + 3] = 0x00; data[iblock_off + 4] = 0x04;
288 data[iblock_off + 5] = 0x00; data[iblock_off + 6] = 0x00;
290 data[iblock_off + 7] = 0x00; for b in &mut data[iblock_off + 8..iblock_off + 60] {
293 *b = 0;
294 }
295
296 let br = BlockReader::open(Cursor::new(data)).unwrap();
297 let mut reader = InodeReader::new(br);
298 let result = recover_file(&mut reader, 12);
299 match result {
300 Err(Ext4Error::RecoveryFailed { ino, reason }) => {
301 assert_eq!(ino, 12);
302 assert!(reason.contains("no block mappings"), "reason: {reason}");
303 }
304 other => panic!("expected RecoveryFailed(no block mappings), got: {other:?}"),
305 }
306 }
307
308 #[test]
311 fn recover_with_unallocated_blocks() {
312 let reader_orig = if let Some(r) = open_forensic() {
313 r
314 } else {
315 eprintln!("skip: forensic.img not found");
316 return;
317 };
318 let mut data = forensic_raw().unwrap();
319
320 let reader_tmp = open_forensic().unwrap();
322 let mappings = reader_tmp.inode_block_map(12).unwrap();
323 if mappings.is_empty() {
324 eprintln!("skip: inode 12 has no block mappings");
325 return;
326 }
327
328 let sb = reader_orig.block_reader().superblock();
330 let bitmap_block = reader_orig.block_reader().block_bitmap_block(0).unwrap();
331 let block_size = sb.block_size as usize;
332 let bitmap_offset = bitmap_block as usize * block_size;
333
334 for mapping in &mappings {
336 for i in 0..mapping.length {
337 let block = mapping.physical_block + i;
338 let byte_idx = block as usize / 8;
339 let bit_idx = block as usize % 8;
340 if bitmap_offset + byte_idx < data.len() {
341 data[bitmap_offset + byte_idx] &= !(1 << bit_idx);
342 }
343 }
344 }
345
346 let br = BlockReader::open(Cursor::new(data)).unwrap();
347 let mut reader = InodeReader::new(br);
348 let result = recover_file(&mut reader, 12).unwrap();
349 assert!(result.expected_size > 0);
350 assert_eq!(result.data.len(), result.expected_size as usize);
351 assert_eq!(result.recovered_size, result.expected_size);
354 assert!(result.overwritten_ranges.is_empty());
355 assert!((result.recovery_percentage() - 100.0).abs() < f64::EPSILON);
356 }
357
358 #[test]
361 fn recover_with_read_block_error() {
362 let reader_orig = if let Some(r) = open_forensic() {
363 r
364 } else {
365 eprintln!("skip: forensic.img not found");
366 return;
367 };
368 let mut data = forensic_raw().unwrap();
369 let (off, _isz) = inode_byte_offset(&reader_orig, 12);
370
371 let reader_tmp = open_forensic().unwrap();
373 let mappings = reader_tmp.inode_block_map(12).unwrap();
374 if mappings.is_empty() {
375 eprintln!("skip: inode 12 has no block mappings");
376 return;
377 }
378
379 let iblock_off = off + 0x28;
383 let leaf_off = iblock_off + 12;
384 let huge_block: u32 = 0x00FF_FFFF;
386 data[leaf_off + 8] = (huge_block & 0xFF) as u8;
387 data[leaf_off + 9] = ((huge_block >> 8) & 0xFF) as u8;
388 data[leaf_off + 10] = ((huge_block >> 16) & 0xFF) as u8;
389 data[leaf_off + 11] = ((huge_block >> 24) & 0xFF) as u8;
390 data[leaf_off + 6] = 0;
392 data[leaf_off + 7] = 0;
393
394 let sb = reader_orig.block_reader().superblock();
411 let total_blocks = sb.blocks_count;
412 let target_block = (total_blocks - 1) as u32;
414 data[leaf_off + 8] = (target_block & 0xFF) as u8;
415 data[leaf_off + 9] = ((target_block >> 8) & 0xFF) as u8;
416 data[leaf_off + 10] = ((target_block >> 16) & 0xFF) as u8;
417 data[leaf_off + 11] = ((target_block >> 24) & 0xFF) as u8;
418
419 let block_size = sb.block_size as usize;
421 let bitmap_block = reader_orig.block_reader().block_bitmap_block(0).unwrap();
422 let bitmap_offset = bitmap_block as usize * block_size;
423 let byte_idx = target_block as usize / 8;
424 let bit_idx = target_block as usize % 8;
425 if bitmap_offset + byte_idx < data.len() {
426 data[bitmap_offset + byte_idx] &= !(1 << bit_idx);
427 }
428
429 let trunc_at = target_block as usize * block_size;
431 if trunc_at < data.len() {
432 data.truncate(trunc_at);
433 }
434
435 let br = BlockReader::open(Cursor::new(data)).unwrap();
436 let mut reader = InodeReader::new(br);
437 let result = recover_file(&mut reader, 12);
438 if let Ok(r) = result {
441 assert_eq!(r.data.len(), r.expected_size as usize);
442 } else {
443 }
445 }
446
447 #[test]
448 fn recover_inode_out_of_range() {
449 let mut reader = if let Some(r) = open_forensic() {
450 r
451 } else {
452 eprintln!("skip: forensic.img not found");
453 return;
454 };
455 let result = recover_file(&mut reader, 999_999);
456 assert!(result.is_err());
457 }
458}