Skip to content
API Documentation
BastionGuard™
lost_partition_finder.cpp
Go to the documentation of this file.
2#include "i18n.hpp"
3#include "xfs_format.hpp"
4#include "hfs_format.hpp"
5
6#include <algorithm>
7#include <array>
8#include <string>
9#include <vector>
10
11namespace {
12constexpr uint64_t MiB = 1024ull * 1024ull;
13constexpr uint64_t GiB = 1024ull * 1024ull * 1024ull;
14constexpr uint64_t SECTOR_SIZE = 512ull;
15
16// Bug #13 fix: modern partitions are 1 MiB aligned (Windows Vista+, Linux ≥2010).
17// A coarser stride misses partitions that happen to live on a 1 MiB boundary
18// not divisible by the chosen stride. The cost is a few extra reads, which are
19// negligible compared to producing a usable result.
20constexpr uint64_t SCAN_STRIDE = 1ull * MiB;
21
22// Bug #10 fix: the previous 16 GiB cap made the lost-partition finder useless
23// on any modern disk. We now scan up to 4 TiB by default (covers most consumer
24// drives) and let larger drives be scanned in full if the user has time.
25constexpr uint64_t DEFAULT_MAX_SCAN = 4ull * 1024ull * GiB; // 4 TiB
26constexpr uint64_t HARD_MAX_SCAN = 64ull * 1024ull * GiB; // 64 TiB safety cap
27
28struct Range {
29 uint64_t start;
30 uint64_t end;
31};
32
33bool overlaps(const std::vector<Range>& ranges, uint64_t start, uint64_t end) {
34 for (const auto& r : ranges) {
35 if (start < r.end && end > r.start) {
36 return true;
37 }
38 }
39 return false;
40}
41
42bool is_ntfs(const std::array<unsigned char, 512>& b) {
43 return std::string(reinterpret_cast<const char*>(b.data() + 3), 8) == "NTFS " &&
44 b[510] == 0x55 && b[511] == 0xAA;
45}
46
47bool is_fat32(const std::array<unsigned char, 512>& b) {
48 return std::string(reinterpret_cast<const char*>(b.data() + 82), 8) == "FAT32 " &&
49 b[510] == 0x55 && b[511] == 0xAA;
50}
51
52bool is_exfat(const std::array<unsigned char, 512>& b) {
53 return std::string(reinterpret_cast<const char*>(b.data() + 3), 8) == "EXFAT " &&
54 b[510] == 0x55 && b[511] == 0xAA;
55}
56
57bool looks_ext4(DiskReader& reader, uint64_t offset) {
58 std::array<unsigned char, 2> sig{};
59 return reader.read_at(offset + 1024 + 56, sig.data(), sig.size()) && sig[0] == 0x53 && sig[1] == 0xEF;
60}
61
62bool looks_btrfs(DiskReader& reader, uint64_t offset) {
63 std::array<char, 8> sig{};
64 return reader.read_at(offset + 0x10040, sig.data(), sig.size()) && std::string(sig.data(), sig.size()) == "_BHRfS_M";
65}
66
67// XFS quick check (magic only). For full validation we use parse_superblock
68// in xfs_size_bytes() below.
69bool looks_xfs(DiskReader& reader, uint64_t offset) {
70 std::array<unsigned char, 4> sig{};
71 if (!reader.read_at(offset, sig.data(), sig.size())) return false;
72 return sig[0] == 'X' && sig[1] == 'F' && sig[2] == 'S' && sig[3] == 'B';
73}
74
75// Returns 0 if the sector at `offset` does not actually contain a valid
76// XFS superblock (e.g. magic was a coincidence), otherwise volume size.
77uint64_t xfs_size_bytes(DiskReader& reader, uint64_t offset) {
78 std::array<unsigned char, 512> sector{};
79 if (!reader.read_at(offset, sector.data(), sector.size())) return 0;
81 if (!xfs::parse_superblock(sector.data(), sb)) return 0;
82 return sb.volume_size_bytes();
83}
84
85// HFS+ / HFSX: volume header at +1024 from volume start. Magic in the
86// first 2 bytes is "H+" or "HX".
87bool looks_hfs(DiskReader& reader, uint64_t offset) {
88 std::array<unsigned char, 4> sig{};
89 if (!reader.read_at(offset + hfs::HFSPLUS_VOLUME_HEADER_OFFSET,
90 sig.data(), sig.size())) {
91 return false;
92 }
93 const uint16_t s = static_cast<uint16_t>((sig[0] << 8) | sig[1]);
95}
96
97uint64_t hfs_size_bytes(DiskReader& reader, uint64_t offset) {
98 std::array<unsigned char, 512> sector{};
99 if (!reader.read_at(offset + hfs::HFSPLUS_VOLUME_HEADER_OFFSET,
100 sector.data(), sector.size())) {
101 return 0;
102 }
104 if (!hfs::parse_volume_header(sector.data(), vh)) return 0;
105 return vh.volume_size_bytes();
106}
107
108uint64_t le64(const unsigned char* p) {
109 return static_cast<uint64_t>(p[0]) |
110 (static_cast<uint64_t>(p[1]) << 8) |
111 (static_cast<uint64_t>(p[2]) << 16) |
112 (static_cast<uint64_t>(p[3]) << 24) |
113 (static_cast<uint64_t>(p[4]) << 32) |
114 (static_cast<uint64_t>(p[5]) << 40) |
115 (static_cast<uint64_t>(p[6]) << 48) |
116 (static_cast<uint64_t>(p[7]) << 56);
117}
118
119uint32_t le32(const unsigned char* p) {
120 return static_cast<uint32_t>(p[0]) |
121 (static_cast<uint32_t>(p[1]) << 8) |
122 (static_cast<uint32_t>(p[2]) << 16) |
123 (static_cast<uint32_t>(p[3]) << 24);
124}
125
126uint64_t ntfs_size_bytes(const std::array<unsigned char, 512>& b) {
127 return le64(b.data() + 40) * SECTOR_SIZE;
128}
129
130uint64_t fat32_size_bytes(const std::array<unsigned char, 512>& b) {
131 const uint32_t sectors32 = le32(b.data() + 32);
132 return static_cast<uint64_t>(sectors32) * SECTOR_SIZE;
133}
134
135uint64_t exfat_size_bytes(const std::array<unsigned char, 512>& b) {
136 return le64(b.data() + 72) * SECTOR_SIZE;
137}
138
139// Bug #20 fix: read the ext4 superblock and compute real volume size.
140uint64_t ext4_size_bytes(DiskReader& reader, uint64_t offset) {
141 std::array<unsigned char, 1024> sb{};
142 if (!reader.read_at(offset + 1024, sb.data(), sb.size())) return 0;
143 const uint32_t blocks_lo = le32(sb.data() + 0x04);
144 const uint32_t log_block_size = le32(sb.data() + 0x18);
145 const uint64_t block_size = 1024ull << log_block_size;
146 if (block_size < 1024 || block_size > 65536) return 0;
147 return static_cast<uint64_t>(blocks_lo) * block_size;
148}
149
150// Bug #20 fix: read the btrfs superblock total_bytes
151uint64_t btrfs_size_bytes(DiskReader& reader, uint64_t offset) {
152 std::array<unsigned char, 8> total{};
153 // btrfs superblock at +0x10000, total_bytes is at offset 0x70 within sb
154 if (!reader.read_at(offset + 0x10000 + 0x70, total.data(), total.size())) return 0;
155 return le64(total.data());
156}
157}
158
159std::vector<ScanResult> LostPartitionFinder::find(DiskReader& reader,
160 const std::vector<ScanResult>& known_results,
161 std::vector<std::string>& logs) {
162 std::vector<ScanResult> out;
163 std::vector<Range> claimed_ranges;
164 claimed_ranges.reserve(known_results.size());
165
166 for (const auto& r : known_results) {
167 if (r.kind == ResultKind::Partition && r.size_bytes > 0) {
168 claimed_ranges.push_back({r.offset_bytes, r.offset_bytes + r.size_bytes});
169 }
170 }
171
172 const uint64_t disk_size = reader.size();
173 const uint64_t max_scan = std::min<uint64_t>(disk_size, std::min<uint64_t>(DEFAULT_MAX_SCAN, HARD_MAX_SCAN));
174
175 logs.push_back(trs("Lost partition finder: deep scan started."));
176 logs.push_back(trs("Lost partition scan stride:") + std::string(" ") +
177 std::to_string(SCAN_STRIDE / MiB) + " MiB");
178 logs.push_back(trs("Lost partition scan range:") + std::string(" ") +
179 std::to_string(max_scan / GiB) + " GiB");
180
181 // Bug #10/#13 fix: scan with fixed 1 MiB stride from offset SCAN_STRIDE
182 // up to max_scan. Partitions are 1 MiB aligned on every modern OS, and the
183 // first MiB is reserved for MBR/GPT, never a partition boundary.
184 for (uint64_t offset = SCAN_STRIDE; offset + 4096 < max_scan; offset += SCAN_STRIDE) {
185 std::array<unsigned char, 512> sector{};
186 if (!reader.read_at(offset, sector.data(), sector.size())) {
187 continue;
188 }
189
190 if (is_ntfs(sector)) {
191 const uint64_t size_bytes = ntfs_size_bytes(sector);
192 const uint64_t end = size_bytes > 0 ? offset + size_bytes : offset + MiB;
193 if (!overlaps(claimed_ranges, offset, end)) {
195 offset,
196 size_bytes,
197 "Lost NTFS partition candidate",
198 trs("Lost partition finder"),
199 trs("Unallocated NTFS boot sector candidate"),
200 0.88});
201 claimed_ranges.push_back({offset, end});
202 }
203 } else if (is_fat32(sector)) {
204 const uint64_t size_bytes = fat32_size_bytes(sector);
205 const uint64_t end = size_bytes > 0 ? offset + size_bytes : offset + MiB;
206 if (!overlaps(claimed_ranges, offset, end)) {
208 offset,
209 size_bytes,
210 "Lost FAT32 partition candidate",
211 trs("Lost partition finder"),
212 trs("Unallocated FAT32 boot sector candidate"),
213 0.84});
214 claimed_ranges.push_back({offset, end});
215 }
216 } else if (is_exfat(sector)) {
217 const uint64_t size_bytes = exfat_size_bytes(sector);
218 const uint64_t end = size_bytes > 0 ? offset + size_bytes : offset + MiB;
219 if (!overlaps(claimed_ranges, offset, end)) {
221 offset,
222 size_bytes,
223 "Lost exFAT partition candidate",
224 trs("Lost partition finder"),
225 trs("Unallocated exFAT boot sector candidate"),
226 0.86});
227 claimed_ranges.push_back({offset, end});
228 }
229 } else if (looks_ext4(reader, offset)) {
230 // Bug #20 fix: compute real ext4 volume size instead of leaving 0
231 const uint64_t size_bytes = ext4_size_bytes(reader, offset);
232 const uint64_t end = size_bytes > 0 ? offset + size_bytes : offset + MiB;
233 if (!overlaps(claimed_ranges, offset, end)) {
235 offset,
236 size_bytes,
237 "Lost ext4 partition candidate",
238 trs("Lost partition finder"),
239 trs("Unallocated ext4 superblock candidate"),
240 0.80});
241 claimed_ranges.push_back({offset, end});
242 }
243 } else if (looks_btrfs(reader, offset)) {
244 // Bug #20 fix: compute real btrfs volume size instead of leaving 0
245 const uint64_t size_bytes = btrfs_size_bytes(reader, offset);
246 const uint64_t end = size_bytes > 0 ? offset + size_bytes : offset + MiB;
247 if (!overlaps(claimed_ranges, offset, end)) {
249 offset,
250 size_bytes,
251 trs("Lost Btrfs partition candidate"),
252 trs("Lost partition finder"),
253 trs("Unallocated Btrfs superblock candidate"),
254 0.81});
255 claimed_ranges.push_back({offset, end});
256 }
257 } else if (looks_xfs(reader, offset)) {
258 // Use parse_superblock to also validate geometry (rejects false
259 // positives where 'XFSB' appears as random bytes).
260 const uint64_t size_bytes = xfs_size_bytes(reader, offset);
261 if (size_bytes > 0) {
262 const uint64_t end = offset + size_bytes;
263 if (!overlaps(claimed_ranges, offset, end)) {
265 offset,
266 size_bytes,
267 "Lost XFS partition candidate",
268 trs("Lost partition finder"),
269 trs("Unallocated XFS superblock candidate"),
270 0.86});
271 claimed_ranges.push_back({offset, end});
272 }
273 }
274 } else if (looks_hfs(reader, offset)) {
275 // HFS+/HFSX: validate geometry through parse_volume_header.
276 const uint64_t size_bytes = hfs_size_bytes(reader, offset);
277 if (size_bytes > 0) {
278 const uint64_t end = offset + size_bytes;
279 if (!overlaps(claimed_ranges, offset, end)) {
281 offset,
282 size_bytes,
283 "Lost HFS+ partition candidate",
284 trs("Lost partition finder"),
285 trs("Unallocated HFS+/HFSX volume header candidate"),
286 0.85});
287 claimed_ranges.push_back({offset, end});
288 }
289 }
290 }
291 }
292
293 if (max_scan < disk_size) {
294 logs.push_back(trs("Lost partition finder: scan limited to first ") +
295 std::to_string(max_scan / GiB) + " GiB " +
296 trs("(disk is larger; remaining area not scanned)."));
297 }
298 logs.push_back(trs("Lost partition finder: deep scan completed."));
299 return out;
300}
bool read_at(uint64_t offset, void *buffer, size_t len)
uint64_t size() const
static std::vector< ScanResult > find(DiskReader &reader, const std::vector< ScanResult > &known_results, std::vector< std::string > &logs)
std::string trs(const char *msgid)
Definition i18n.cpp:22
@ Partition
Definition models.hpp:8
@ LostPartitionCandidate
Definition models.hpp:9
bool is_fat32(const std::array< unsigned char, 512 > &b)
bool looks_xfs(DiskReader &reader, uint64_t offset)
bool looks_hfs(DiskReader &reader, uint64_t offset)
uint64_t xfs_size_bytes(DiskReader &reader, uint64_t offset)
uint64_t hfs_size_bytes(DiskReader &reader, uint64_t offset)
bool looks_btrfs(DiskReader &reader, uint64_t offset)
uint64_t ntfs_size_bytes(const std::array< unsigned char, 512 > &b)
bool is_exfat(const std::array< unsigned char, 512 > &b)
bool is_ntfs(const std::array< unsigned char, 512 > &b)
uint64_t exfat_size_bytes(const std::array< unsigned char, 512 > &b)
uint64_t ext4_size_bytes(DiskReader &reader, uint64_t offset)
uint64_t btrfs_size_bytes(DiskReader &reader, uint64_t offset)
bool overlaps(const std::vector< Range > &ranges, uint64_t start, uint64_t end)
uint64_t fat32_size_bytes(const std::array< unsigned char, 512 > &b)
bool looks_ext4(DiskReader &reader, uint64_t offset)
bool parse_volume_header(const unsigned char *buf, VolumeHeader &out)
constexpr uint16_t HFSX_SIGNATURE
constexpr uint32_t HFSPLUS_VOLUME_HEADER_OFFSET
constexpr uint16_t HFSPLUS_SIGNATURE
bool parse_superblock(const unsigned char *buf, Superblock &out)
uint64_t volume_size_bytes() const
uint64_t volume_size_bytes() const