ext4fs/forensic/
carving.rs1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::forensic::recovery::BlockRange;
5use crate::inode::InodeReader;
6use crate::ondisk::extent::EXTENT_MAGIC;
7use std::io::{Read, Seek};
8
9#[derive(Debug, Clone)]
11pub struct CarvedInode {
12 pub block: u64,
13 pub offset_in_block: usize,
14}
15
16pub fn unallocated_blocks<R: Read + Seek>(reader: &mut InodeReader<R>) -> Result<Vec<BlockRange>> {
18 let sb = reader.block_reader().superblock();
19 let group_count = reader.block_reader().group_count();
20 let bpg = u64::from(sb.blocks_per_group);
21 let blocks_count = sb.blocks_count;
22 let mut ranges = Vec::new();
23
24 for g in 0..group_count {
25 let bitmap_block = reader.block_reader_mut().block_bitmap_block(g)?;
26 let bitmap = reader.block_reader_mut().read_block(bitmap_block)?;
27 let base_block = u64::from(g) * bpg;
28
29 let mut run_start: Option<u64> = None;
30 let blocks_in_group = bpg.min(blocks_count.saturating_sub(base_block));
31
32 for bit in 0..blocks_in_group as usize {
33 let byte = bit / 8;
34 let bit_pos = bit % 8;
35 let allocated = if byte < bitmap.len() {
36 (bitmap[byte] >> bit_pos) & 1 == 1
37 } else {
38 false
39 };
40
41 if !allocated {
42 if run_start.is_none() {
43 run_start = Some(base_block + bit as u64);
44 }
45 } else if let Some(start) = run_start {
46 let current = base_block + bit as u64;
47 ranges.push(BlockRange {
48 start,
49 length: current - start,
50 });
51 run_start = None;
52 }
53 }
54 if let Some(start) = run_start {
55 let end = base_block + blocks_in_group;
56 ranges.push(BlockRange {
57 start,
58 length: end - start,
59 });
60 }
61 }
62
63 Ok(ranges)
64}
65
66pub fn read_unallocated<R: Read + Seek>(
68 reader: &mut InodeReader<R>,
69 range: &BlockRange,
70) -> Result<Vec<u8>> {
71 reader
72 .block_reader_mut()
73 .read_blocks(range.start, range.length)
74}
75
76pub fn find_extent_signatures<R: Read + Seek>(
78 reader: &mut InodeReader<R>,
79 ranges: &[BlockRange],
80) -> Result<Vec<CarvedInode>> {
81 let mut found = Vec::new();
82
83 for range in ranges {
84 for i in 0..range.length {
85 let block = range.start + i;
86 let data = match reader.block_reader_mut().read_block(block) {
87 Ok(d) => d,
88 Err(_) => continue,
89 };
90 let mut offset = 0;
91 while offset + 12 <= data.len() {
92 if data.len() >= offset + 2 {
93 let magic = u16::from_le_bytes([data[offset], data[offset + 1]]);
94 if magic == EXTENT_MAGIC {
95 found.push(CarvedInode {
96 block,
97 offset_in_block: offset,
98 });
99 }
100 }
101 offset += 12;
102 }
103 }
104 }
105
106 Ok(found)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::block::BlockReader;
113 use crate::inode::InodeReader;
114 use std::io::Cursor;
115
116 fn open_minimal() -> Option<InodeReader<Cursor<Vec<u8>>>> {
117 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
118 let data = std::fs::read(path).ok()?;
119 let br = BlockReader::open(Cursor::new(data)).ok()?;
120 Some(InodeReader::new(br))
121 }
122
123 #[test]
124 fn find_unallocated_blocks() {
125 let mut reader = if let Some(r) = open_minimal() {
126 r
127 } else {
128 eprintln!("skip: minimal.img not found");
129 return;
130 };
131 let ranges = unallocated_blocks(&mut reader).unwrap();
132 assert!(!ranges.is_empty());
133 for range in &ranges {
134 assert!(range.length > 0);
135 }
136 }
137
138 fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
139 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
140 let data = std::fs::read(path).ok()?;
141 let br = BlockReader::open(Cursor::new(data)).ok()?;
142 Some(InodeReader::new(br))
143 }
144
145 #[test]
146 fn forensic_image_has_unallocated_blocks() {
147 let mut reader = if let Some(r) = open_forensic() {
148 r
149 } else {
150 eprintln!("skip: forensic.img not found");
151 return;
152 };
153 let ranges = unallocated_blocks(&mut reader).unwrap();
154 assert!(!ranges.is_empty(), "expected unallocated block ranges");
155 let total_free: u64 = ranges.iter().map(|r| r.length).sum();
156 assert!(
157 total_free > 100,
158 "expected > 100 free blocks in 32MB image, got {total_free}"
159 );
160 }
161
162 #[test]
163 fn read_unallocated_returns_data() {
164 let mut reader = if let Some(r) = open_forensic() {
165 r
166 } else {
167 eprintln!("skip: forensic.img not found");
168 return;
169 };
170 let ranges = unallocated_blocks(&mut reader).unwrap();
171 assert!(!ranges.is_empty(), "need at least one unallocated range");
172 let block_size = u64::from(reader.block_reader().superblock().block_size);
173 let first = &ranges[0];
174 let data = read_unallocated(&mut reader, first).unwrap();
175 assert_eq!(
176 data.len() as u64,
177 first.length * block_size,
178 "data length should equal range.length * block_size"
179 );
180 }
181
182 #[test]
183 fn find_extent_signatures_runs_without_error() {
184 let mut reader = if let Some(r) = open_forensic() {
185 r
186 } else {
187 eprintln!("skip: forensic.img not found");
188 return;
189 };
190 let ranges = unallocated_blocks(&mut reader).unwrap();
191 let carved = find_extent_signatures(&mut reader, &ranges).unwrap();
192 for c in &carved {
193 assert_eq!(
194 c.offset_in_block % 12,
195 0,
196 "offset {} is not 12-byte aligned",
197 c.offset_in_block
198 );
199 }
200 }
201}