ext4fs/forensic/
search.rs1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::inode::InodeReader;
5use std::io::{Read, Seek};
6
7#[derive(Debug, Clone)]
9pub struct SearchHit {
10 pub block: u64,
12 pub offset: usize,
14 pub context: Vec<u8>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SearchScope {
21 Allocated,
23 Unallocated,
25 All,
27}
28
29pub fn search_blocks<R: Read + Seek>(
34 reader: &mut InodeReader<R>,
35 pattern: &[u8],
36 scope: SearchScope,
37 context_bytes: usize,
38) -> Result<Vec<SearchHit>> {
39 if pattern.is_empty() {
40 return Ok(Vec::new());
41 }
42
43 let sb = reader.block_reader().superblock();
44 let blocks_count = sb.blocks_count;
45 let mut hits = Vec::new();
46
47 for block_num in 0..blocks_count {
48 let allocated = reader.is_block_allocated(block_num).unwrap_or(true);
50 match scope {
51 SearchScope::Allocated => {
52 if !allocated {
53 continue;
54 }
55 }
56 SearchScope::Unallocated => {
57 if allocated {
58 continue;
59 }
60 }
61 SearchScope::All => {}
62 }
63
64 let data = match reader.block_reader_mut().read_block(block_num) {
65 Ok(d) => d,
66 Err(_) => continue,
67 };
68
69 let mut pos = 0;
71 while pos + pattern.len() <= data.len() {
72 if &data[pos..pos + pattern.len()] == pattern {
73 let ctx_start = pos.saturating_sub(context_bytes);
75 let ctx_end = (pos + pattern.len() + context_bytes).min(data.len());
76 let context = data[ctx_start..ctx_end].to_vec();
77
78 hits.push(SearchHit {
79 block: block_num,
80 offset: pos,
81 context,
82 });
83 pos += pattern.len(); } else {
85 pos += 1;
86 }
87 }
88 }
89
90 Ok(hits)
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::block::BlockReader;
97 use std::io::Cursor;
98
99 fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
100 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
101 let data = std::fs::read(path).ok()?;
102 let br = BlockReader::open(Cursor::new(data)).ok()?;
103 Some(InodeReader::new(br))
104 }
105
106 #[test]
107 fn search_hello_in_allocated() {
108 let mut reader = if let Some(r) = open_forensic() {
109 r
110 } else {
111 eprintln!("skip");
112 return;
113 };
114 let hits = search_blocks(&mut reader, b"Hello", SearchScope::Allocated, 16).unwrap();
115 assert!(!hits.is_empty(), "should find 'Hello' in allocated blocks");
116 for h in &hits {
117 assert!(h.context.windows(5).any(|w| w == b"Hello"));
118 }
119 }
120
121 #[test]
122 fn search_nonexistent_pattern() {
123 let mut reader = if let Some(r) = open_forensic() {
124 r
125 } else {
126 eprintln!("skip");
127 return;
128 };
129 let hits = search_blocks(
130 &mut reader,
131 b"ZZZZZ_DEFINITELY_NOT_HERE_12345",
132 SearchScope::All,
133 16,
134 )
135 .unwrap();
136 assert!(hits.is_empty());
137 }
138
139 #[test]
140 fn search_empty_pattern_returns_empty() {
141 let mut reader = if let Some(r) = open_forensic() {
142 r
143 } else {
144 eprintln!("skip");
145 return;
146 };
147 let hits = search_blocks(&mut reader, b"", SearchScope::All, 16).unwrap();
148 assert!(hits.is_empty());
149 }
150
151 #[test]
152 fn search_ext4_magic_in_all() {
153 let mut reader = if let Some(r) = open_forensic() {
154 r
155 } else {
156 eprintln!("skip");
157 return;
158 };
159 let hits = search_blocks(&mut reader, b"forensic-test", SearchScope::All, 8).unwrap();
161 assert!(
162 !hits.is_empty(),
163 "should find 'forensic-test' label in superblock"
164 );
165 }
166
167 #[test]
168 fn search_unallocated_scope() {
169 let mut reader = if let Some(r) = open_forensic() {
170 r
171 } else {
172 eprintln!("skip");
173 return;
174 };
175 let hits = search_blocks(&mut reader, b"recover me", SearchScope::Unallocated, 16).unwrap();
178 let _ = hits; }
181
182 #[test]
183 fn search_hit_has_valid_context() {
184 let mut reader = if let Some(r) = open_forensic() {
185 r
186 } else {
187 eprintln!("skip");
188 return;
189 };
190 let hits = search_blocks(&mut reader, b"Hello", SearchScope::Allocated, 32).unwrap();
191 if let Some(hit) = hits.first() {
192 assert!(!hit.context.is_empty());
193 assert!(hit.context.len() >= 5);
195 }
196 }
197}