Skip to content
API Documentation
BastionGuard™
recovery_exporter.cpp
Go to the documentation of this file.
2#include "disk_reader.hpp"
3#include "file_carver.hpp"
5#include "rescue_imager.hpp"
6#include "i18n.hpp"
7
8#include <algorithm>
9#include <filesystem>
10#include <fstream>
11#include <iomanip>
12#include <sstream>
13#include <utility>
14#include <unordered_set>
15
16namespace fs = std::filesystem;
17
18namespace {
19
21 uint64_t offset,
22 uint64_t size,
23 const fs::path& out_path,
24 std::string& error,
25 std::function<bool()> cancel_cb)
26 {
27 std::ofstream out(out_path, std::ios::binary);
28 if (!out) {
29 error = trs("Unable to create output file") + std::string(": ") + out_path.string();
30 return false;
31 }
32
33 std::vector<unsigned char> buffer(1024 * 1024);
34
35 uint64_t remaining = size;
36 uint64_t cursor = offset;
37
38 while (remaining > 0)
39 {
40 if (cancel_cb && cancel_cb())
41 {
42 error = trs("Recovery cancelled by user");
43 return false;
44 }
45
46 const size_t chunk = static_cast<size_t>(
47 std::min<uint64_t>(buffer.size(), remaining));
48
49 if (!reader.read_at(cursor, buffer.data(), chunk))
50 {
51 error = trs("Read error while exporting partition image");
52 return false;
53 }
54
55 out.write(reinterpret_cast<const char*>(buffer.data()),
56 static_cast<std::streamsize>(chunk));
57
58 if (!out)
59 {
60 error = trs("Write error while exporting partition image");
61 return false;
62 }
63
64 cursor += chunk;
65 remaining -= chunk;
66 }
67
68 return true;
69 }
70
72 {
73 return r.kind == ResultKind::Partition ||
76 r.kind == ResultKind::NtfsVolume;
77 }
78
79 std::string sanitize(const std::string& s)
80 {
81 std::string out;
82
83 for (char c : s)
84 {
85 if (isalnum(static_cast<unsigned char>(c)))
86 out.push_back(c);
87 else if (c == ' ' || c == '-' || c == '_')
88 out.push_back('_');
89 }
90
91 if (out.empty())
92 out = "item";
93
94 return out;
95 }
96
98 const fs::path& output_dir,
99 const ScanReport& report,
100 const std::unordered_set<size_t>& selected_indices,
101 bool filter_results,
102 const RecoverySummary& summary,
103 const std::vector<RecoveredFileRecord>& fs_recovered,
104 const std::vector<CarvedFile>& carved,
105 std::string& error)
106 {
107 std::ofstream out(output_dir / "recovery_manifest.txt", std::ios::binary);
108
109 if (!out)
110 {
111 error = trs("Unable to create output file") + std::string(": ") +
112 (output_dir / "recovery_manifest.txt").string();
113 return;
114 }
115
116 out << "input=" << report.input_path << "\n";
117 out << "selected_filter=" << (filter_results ? 1 : 0) << "\n";
118 out << "selected_results=" << selected_indices.size() << "\n";
119 out << "exported_images=" << summary.exported_images.size() << "\n";
120 out << "recovered_files=" << summary.recovered_files.size() << "\n\n";
121
122 out << "[filesystem_recovery]\n";
123
124 for (const auto& rf : fs_recovered)
125 {
126 out << rf.filesystem << '\t'
127 << rf.integrity << '\t'
128 << rf.size_bytes << '\t'
129 << rf.original_path << '\t'
130 << rf.output_path;
131
132 if (!rf.note.empty())
133 out << '\t' << rf.note;
134
135 out << '\n';
136 }
137
138 out << "\n[carved]\n";
139
140 for (const auto& cf : carved)
141 {
142 out << cf.type << '\t'
143 << cf.size << '\t'
144 << cf.output_path << '\n';
145 }
146 }
147
148 std::vector<std::pair<uint64_t, uint64_t>> selected_carve_ranges(
149 const ScanReport& report,
150 const std::unordered_set<size_t>& selected_indices,
151 bool filter_results,
152 uint64_t source_size)
153 {
154 std::vector<std::pair<uint64_t, uint64_t>> ranges;
155
156 auto add_range = [&](uint64_t offset, uint64_t size)
157 {
158 if (size == 0 || offset >= source_size)
159 return;
160
161 size = std::min<uint64_t>(size, source_size - offset);
162
163 if (ranges.empty())
164 {
165 ranges.emplace_back(offset, size);
166 return;
167 }
168
169 auto& last = ranges.back();
170
171 const uint64_t last_end = last.first + last.second;
172
173 if (offset <= last_end)
174 {
175 const uint64_t new_end = std::max<uint64_t>(last_end, offset + size);
176 last.second = new_end - last.first;
177 }
178 else
179 {
180 ranges.emplace_back(offset, size);
181 }
182 };
183
184 if (!filter_results)
185 {
186 ranges.emplace_back(0, source_size);
187 return ranges;
188 }
189
190 std::vector<std::pair<uint64_t, uint64_t>> temp;
191
192 for (size_t idx = 0; idx < report.results.size(); ++idx)
193 {
194 if (!selected_indices.count(idx))
195 continue;
196
197 const auto& r = report.results[idx];
198
199 if (r.offset_bytes == 0 && r.size_bytes == 0)
200 continue;
201
202 switch (r.kind)
203 {
210 temp.emplace_back(r.offset_bytes, r.size_bytes);
211 break;
212 default:
213 break;
214 }
215 }
216
217 if (temp.empty())
218 return ranges;
219
220 std::sort(temp.begin(), temp.end(),
221 [](const auto& a, const auto& b)
222 {
223 return a.first < b.first;
224 });
225
226 for (const auto& r : temp)
227 add_range(r.first, r.second);
228
229 return ranges;
230 }
231
232} // namespace
233
234
235
237 const std::string& input_path,
238 const std::string& output_image_path,
239 RecoverySummary& summary,
240 std::vector<std::string>& logs,
241 std::string& error,
242 std::function<bool()> cancel_cb)
243{
244 RescueImageReport report;
245 if (!RescueImager::create_image(input_path, output_image_path, report, error, {}, cancel_cb))
246 {
247 return false;
248 }
249
250 summary.rescue_image_path = report.image_path;
251 summary.rescue_map_path = report.map_path;
252 logs.insert(logs.end(), report.logs.begin(), report.logs.end());
253 return true;
254}
255
256
258 const ScanReport& report,
259 const std::string& output_dir,
260 const std::vector<size_t>& selected_result_indices,
261 RecoverySummary& summary,
262 std::vector<std::string>& logs,
263 std::string& error,
264 std::function<bool()> cancel_cb)
265{
266 summary = {};
267
268 try
269 {
270 fs::create_directories(output_dir);
271 fs::create_directories(fs::path(output_dir) / "images");
272 fs::create_directories(fs::path(output_dir) / "carved");
273 }
274 catch (const std::exception& ex)
275 {
276 error = ex.what();
277 return false;
278 }
279
280 if (cancel_cb && cancel_cb())
281 {
282 logs.push_back(trs("Recovery cancelled by user."));
283 return false;
284 }
285
286 DiskReader reader;
287
288 std::string open_error;
289
290 if (!reader.open_readonly(report.input_path, open_error))
291 {
292 error = trs("Unable to open source");
293
294 if (!open_error.empty())
295 error += std::string(": ") + open_error;
296
297 logs.push_back(trs("Recovery open error:") + std::string(" ") + error);
298
299 return false;
300 }
301
302 std::unordered_set<size_t> selected_indices(
303 selected_result_indices.begin(),
304 selected_result_indices.end());
305
306 const bool filter_results = !selected_indices.empty();
307
308 size_t index = 0;
309
310 for (size_t result_index = 0;
311 result_index < report.results.size();
312 ++result_index)
313 {
314 if (cancel_cb && cancel_cb())
315 {
316 logs.push_back(trs("Recovery cancelled during image export."));
317 return false;
318 }
319
320 const auto& r = report.results[result_index];
321
322 if (filter_results && !selected_indices.count(result_index))
323 continue;
324
325 if (!should_export(r) || r.size_bytes == 0)
326 continue;
327
328 if (r.offset_bytes + r.size_bytes > reader.size())
329 continue;
330
331 std::ostringstream name;
332
333 name << std::setw(3)
334 << std::setfill('0')
335 << index++
336 << '_'
337 << sanitize(r.type)
338 << '_'
339 << r.offset_bytes
340 << ".img";
341
342 fs::path out_path =
343 fs::path(output_dir) / "images" / name.str();
344
345 if (!export_range(
346 reader,
347 r.offset_bytes,
348 r.size_bytes,
349 out_path,
350 error,
351 cancel_cb))
352 {
353 return false;
354 }
355
356 summary.exported_images.push_back(out_path.string());
357
358 logs.push_back(
359 trs("Exported volume image:") +
360 std::string(" ") +
361 out_path.string());
362 }
363
364 if (cancel_cb && cancel_cb())
365 {
366 logs.push_back(trs("Recovery cancelled before filesystem recovery."));
367 return false;
368 }
369
370 ScanReport filtered_report = report;
371
372 if (filter_results)
373 {
374 filtered_report.results.clear();
375
376 for (size_t idx = 0; idx < report.results.size(); ++idx)
377 {
378 if (selected_indices.count(idx))
379 filtered_report.results.push_back(report.results[idx]);
380 }
381 }
382
383 std::vector<RecoveredFileRecord> fs_recovered;
384
386 reader,
387 filtered_report,
388 output_dir,
389 fs_recovered,
390 logs,
391 error))
392 {
393 return false;
394 }
395
396 for (const auto& rf : fs_recovered)
397 summary.recovered_files.push_back(rf.output_path);
398
399 if (cancel_cb && cancel_cb())
400 {
401 logs.push_back(trs("Recovery cancelled during filesystem recovery."));
402 return false;
403 }
404
405 const auto carve_ranges =
406 selected_carve_ranges(
407 report,
408 selected_indices,
409 filter_results,
410 reader.size());
411
412 std::vector<CarvedFile> carved;
413
414 if (!carve_ranges.empty())
415 {
417 reader,
418 carve_ranges,
419 (fs::path(output_dir) / "carved").string(),
420 carved,
421 logs,
422 error))
423 {
424 return false;
425 }
426 }
427
428 for (const auto& cf : carved)
429 summary.recovered_files.push_back(cf.output_path);
430
431 if (cancel_cb && cancel_cb())
432 {
433 logs.push_back(trs("Recovery cancelled during carving."));
434 return false;
435 }
436
437 write_recovery_manifest(
438 fs::path(output_dir),
439 report,
440 selected_indices,
441 filter_results,
442 summary,
443 fs_recovered,
444 carved,
445 error);
446
447 if (!error.empty())
448 return false;
449
450 logs.push_back(trs("Recovery export completed."));
451
452 return true;
453}
bool read_at(uint64_t offset, void *buffer, size_t len)
bool open_readonly(const std::string &path, std::string &error)
uint64_t size() const
static bool carve_ranges_to_directory(DiskReader &reader, const std::vector< std::pair< uint64_t, uint64_t > > &ranges, const std::string &output_dir, std::vector< CarvedFile > &carved, std::vector< std::string > &logs, std::string &error)
static bool recover_supported_filesystems(DiskReader &reader, const ScanReport &report, const std::string &output_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
static bool create_rescue_image(const std::string &input_path, const std::string &output_image_path, RecoverySummary &summary, std::vector< std::string > &logs, std::string &error, CancelCallback cancel_cb)
static bool export_results_and_carve(const ScanReport &report, const std::string &output_dir, const std::vector< size_t > &selected_result_indices, RecoverySummary &summary, std::vector< std::string > &logs, std::string &error, CancelCallback cancel_cb)
static bool create_image(const std::string &device_path, const std::string &image_path, RescueImageReport &report, std::string &error, ProgressCallback progress_cb={}, CancelCallback cancel_cb={})
std::string trs(const char *msgid)
Definition i18n.cpp:22
@ Partition
Definition models.hpp:8
@ LostPartitionCandidate
Definition models.hpp:9
@ NtfsMetadata
Definition models.hpp:13
@ FilesystemCandidate
Definition models.hpp:10
@ FilesystemVolume
Definition models.hpp:11
bool export_range(DiskReader &reader, uint64_t offset, uint64_t size, const fs::path &out_path, std::string &error, std::function< bool()> cancel_cb)
void write_recovery_manifest(const fs::path &output_dir, const ScanReport &report, const std::unordered_set< size_t > &selected_indices, bool filter_results, const RecoverySummary &summary, const std::vector< RecoveredFileRecord > &fs_recovered, const std::vector< CarvedFile > &carved, std::string &error)
std::string sanitize(const std::string &s)
std::vector< std::pair< uint64_t, uint64_t > > selected_carve_ranges(const ScanReport &report, const std::unordered_set< size_t > &selected_indices, bool filter_results, uint64_t source_size)
std::vector< std::string > exported_images
std::string rescue_map_path
std::string rescue_image_path
std::vector< std::string > recovered_files
std::string image_path
std::vector< std::string > logs
std::vector< ScanResult > results
Definition models.hpp:40
std::string input_path
Definition models.hpp:38