Skip to content
API Documentation
BastionGuard™
filesystem_probes.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 <array>
7#include <set>
8#include <sstream>
9#include <string>
10#include <vector>
11
12namespace {
13constexpr uint64_t SECTOR_SIZE = 512ull;
14
15uint16_t le16(const unsigned char* p) {
16 return static_cast<uint16_t>(p[0]) | (static_cast<uint16_t>(p[1]) << 8);
17}
18
19uint32_t le32(const unsigned char* p) {
20 return static_cast<uint32_t>(p[0]) |
21 (static_cast<uint32_t>(p[1]) << 8) |
22 (static_cast<uint32_t>(p[2]) << 16) |
23 (static_cast<uint32_t>(p[3]) << 24);
24}
25
26uint64_t le64(const unsigned char* p) {
27 return static_cast<uint64_t>(p[0]) |
28 (static_cast<uint64_t>(p[1]) << 8) |
29 (static_cast<uint64_t>(p[2]) << 16) |
30 (static_cast<uint64_t>(p[3]) << 24) |
31 (static_cast<uint64_t>(p[4]) << 32) |
32 (static_cast<uint64_t>(p[5]) << 40) |
33 (static_cast<uint64_t>(p[6]) << 48) |
34 (static_cast<uint64_t>(p[7]) << 56);
35}
36
37bool read_sector(DiskReader& reader, uint64_t offset, std::array<unsigned char, 512>& sector) {
38 return reader.read_at(offset, sector.data(), sector.size());
39}
40
41bool is_fat32(const std::array<unsigned char, 512>& sector) {
42 return std::string(reinterpret_cast<const char*>(sector.data() + 82), 8) == "FAT32 " &&
43 sector[510] == 0x55 && sector[511] == 0xAA;
44}
45
46bool is_exfat(const std::array<unsigned char, 512>& sector) {
47 return std::string(reinterpret_cast<const char*>(sector.data() + 3), 8) == "EXFAT " &&
48 sector[510] == 0x55 && sector[511] == 0xAA;
49}
50
51bool ext4_has_magic(DiskReader& reader, uint64_t offset) {
52 std::array<unsigned char, 2> sig{};
53 return reader.read_at(offset + 1024 + 56, sig.data(), sig.size()) && sig[0] == 0x53 && sig[1] == 0xEF;
54}
55
56bool btrfs_has_magic(DiskReader& reader, uint64_t offset) {
57 std::array<char, 8> sig{};
58 return reader.read_at(offset + 0x10040, sig.data(), sig.size()) && std::string(sig.data(), sig.size()) == "_BHRfS_M";
59}
60
61ScanResult fat32_probe(DiskReader& reader, uint64_t offset) {
62 std::array<unsigned char, 512> sector{};
63 if (!read_sector(reader, offset, sector) || !is_fat32(sector)) {
64 return {};
65 }
66
67 const uint16_t bytes_per_sector = le16(sector.data() + 11);
68 const uint8_t sectors_per_cluster = sector[13];
69 const uint16_t reserved_sectors = le16(sector.data() + 14);
70 const uint8_t fats = sector[16];
71 const uint32_t total_sectors = le32(sector.data() + 32);
72 const uint32_t sectors_per_fat = le32(sector.data() + 36);
73 const uint32_t root_cluster = le32(sector.data() + 44);
74 const uint16_t fsinfo_sector = le16(sector.data() + 48);
75 const uint16_t backup_boot_sector = le16(sector.data() + 50);
76
77 if (bytes_per_sector == 0 || sectors_per_cluster == 0 || total_sectors == 0) {
78 return {};
79 }
80
81 const uint64_t volume_size = static_cast<uint64_t>(bytes_per_sector) * total_sectors;
82 std::ostringstream detail;
83 detail << trs("Bytes/sector") << ": " << bytes_per_sector
84 << ", " << trs("Sectors/cluster") << ": " << static_cast<unsigned>(sectors_per_cluster)
85 << trs(", FATs: ") << static_cast<unsigned>(fats)
86 << trs(", Reserved: ") << reserved_sectors
87 << trs(", Sectors/FAT: ") << sectors_per_fat
88 << trs(", Root cluster: ") << root_cluster
89 << trs(", FSInfo: ") << fsinfo_sector
90 << trs(", Backup boot: ") << backup_boot_sector;
91
93 offset,
94 volume_size,
95 "FAT32 volume",
96 "FAT32 probe",
97 detail.str(),
98 0.93};
99}
100
101ScanResult exfat_probe(DiskReader& reader, uint64_t offset) {
102 std::array<unsigned char, 512> sector{};
103 if (!read_sector(reader, offset, sector) || !is_exfat(sector)) {
104 return {};
105 }
106
107 const uint64_t partition_offset = le64(sector.data() + 64);
108 const uint64_t volume_length = le64(sector.data() + 72);
109 const uint32_t fat_offset = le32(sector.data() + 80);
110 const uint32_t fat_length = le32(sector.data() + 84);
111 const uint32_t cluster_heap_offset = le32(sector.data() + 88);
112 const uint32_t cluster_count = le32(sector.data() + 92);
113 const uint32_t root_dir_cluster = le32(sector.data() + 96);
114 const uint8_t bytes_per_sector_shift = sector[108];
115 const uint8_t sectors_per_cluster_shift = sector[109];
116
117 const uint64_t bytes_per_sector = 1ull << bytes_per_sector_shift;
118 const uint64_t sectors_per_cluster = 1ull << sectors_per_cluster_shift;
119 const uint64_t volume_size = volume_length * bytes_per_sector;
120
121 if (volume_length == 0 || bytes_per_sector < 512 || sectors_per_cluster == 0) {
122 return {};
123 }
124
125 std::ostringstream detail;
126 detail << trs("Partition offset: ") << partition_offset
127 << ", " << trs("Bytes/sector") << ": " << bytes_per_sector
128 << ", " << trs("Sectors/cluster") << ": " << sectors_per_cluster
129 << trs(", FAT offset: ") << fat_offset
130 << trs(", FAT length: ") << fat_length
131 << trs(", Cluster heap: ") << cluster_heap_offset
132 << trs(", Cluster count: ") << cluster_count
133 << trs(", Root dir cluster: ") << root_dir_cluster;
134
136 offset,
137 volume_size,
138 "exFAT volume",
139 "exFAT probe",
140 detail.str(),
141 0.94};
142}
143
144ScanResult ext4_probe(DiskReader& reader, uint64_t offset) {
145 if (!ext4_has_magic(reader, offset)) {
146 return {};
147 }
148
149 std::array<unsigned char, 1024> super{};
150 if (!reader.read_at(offset + 1024, super.data(), super.size())) {
151 return {};
152 }
153
154 const uint32_t inodes_count = le32(super.data() + 0x00);
155 const uint32_t blocks_count_lo = le32(super.data() + 0x04);
156 const uint32_t log_block_size = le32(super.data() + 0x18);
157 const uint32_t blocks_per_group = le32(super.data() + 0x20);
158 const uint32_t inodes_per_group = le32(super.data() + 0x28);
159 const uint16_t magic = le16(super.data() + 0x38);
160 const uint32_t feature_compat = le32(super.data() + 0x5C);
161 const uint32_t feature_incompat = le32(super.data() + 0x60);
162 const uint32_t feature_ro_compat = le32(super.data() + 0x64);
163 const uint16_t inode_size = le16(super.data() + 0x58);
164
165 if (magic != 0xEF53) {
166 return {};
167 }
168
169 const uint64_t block_size = 1024ull << log_block_size;
170 const uint64_t volume_size = static_cast<uint64_t>(blocks_count_lo) * block_size;
171
172 std::ostringstream detail;
173 detail << trs("Inodes: ") << inodes_count
174 << trs(", Blocks: ") << blocks_count_lo
175 << trs(", Block size: ") << block_size
176 << trs(", Blocks/group: ") << blocks_per_group
177 << trs(", Inodes/group: ") << inodes_per_group
178 << trs(", Inode size: ") << inode_size
179 << ", Compat: 0x" << std::hex << feature_compat
180 << ", Incompat: 0x" << feature_incompat
181 << ", RO compat: 0x" << feature_ro_compat;
182
184 offset,
185 volume_size,
186 "ext4 volume",
187 "ext4 probe",
188 detail.str(),
189 0.91};
190}
191
192ScanResult btrfs_probe(DiskReader& reader, uint64_t offset) {
193 if (!btrfs_has_magic(reader, offset)) {
194 return {};
195 }
196
197 std::array<unsigned char, 0x2000> sb{};
198 if (!reader.read_at(offset + 0x10000, sb.data(), sb.size())) {
199 return {};
200 }
201
202 const uint64_t bytenr = le64(sb.data() + 0x00);
203 const uint64_t flags = le64(sb.data() + 0x30);
204 const uint64_t generation = le64(sb.data() + 0x40);
205 const uint64_t root = le64(sb.data() + 0x50);
206 const uint64_t chunk_root = le64(sb.data() + 0x58);
207 const uint64_t total_bytes = le64(sb.data() + 0x70);
208 const uint64_t bytes_used = le64(sb.data() + 0x78);
209 const uint32_t sectorsize = le32(sb.data() + 0x90);
210 const uint32_t nodesize = le32(sb.data() + 0x94);
211 const uint32_t leafsize = le32(sb.data() + 0x98);
212 const uint32_t stripesize = le32(sb.data() + 0x9C);
213 const uint32_t num_devices = le32(sb.data() + 0xA0);
214
215 if (sectorsize == 0 || nodesize == 0 || total_bytes == 0) {
216 return {};
217 }
218
219 std::ostringstream detail;
220 detail << trs("Bytenr: ") << bytenr
221 << trs(", Generation: ") << generation
222 << ", Flags: 0x" << std::hex << flags << std::dec
223 << ", Root: " << root
224 << ", Chunk root: " << chunk_root
225 << ", Total bytes: " << total_bytes
226 << ", Used bytes: " << bytes_used
227 << ", Sector size: " << sectorsize
228 << ", Node size: " << nodesize
229 << ", Leaf size: " << leafsize
230 << ", Stripe size: " << stripesize
231 << ", Devices: " << num_devices;
232
234 offset,
235 total_bytes,
236 "Btrfs volume",
237 "Btrfs probe",
238 detail.str(),
239 0.92};
240}
241
242// XFS probe: delegates the heavy lifting to xfs::parse_superblock(), which
243// validates magic, geometry, version (4/5), and feature consistency. This
244// produces a high-quality identification that won't match on garbage.
245ScanResult xfs_probe(DiskReader& reader, uint64_t offset) {
246 // The XFS superblock is at byte 0 of the volume. We always read 512 bytes
247 // because that's what parse_superblock expects (one sector worth).
248 std::array<unsigned char, 512> sector{};
249 if (!reader.read_at(offset, sector.data(), sector.size())) {
250 return {};
251 }
253 if (!xfs::parse_superblock(sector.data(), sb)) {
254 return {};
255 }
256
257 std::ostringstream detail;
258 detail << trs("Format version") << ": " << sb.format_version()
259 << ", " << trs("Block size") << ": " << sb.blocksize
260 << ", " << trs("Sector size") << ": " << sb.sectsize
261 << ", " << trs("Inode size") << ": " << sb.inodesize
262 << ", " << trs("Inodes/block") << ": " << sb.inopblock
263 << ", " << trs("Allocation groups") << ": " << sb.agcount
264 << ", " << trs("Blocks/AG") << ": " << sb.agblocks
265 << ", " << trs("Total blocks") << ": " << sb.dblocks;
266 if (sb.is_v5()) {
267 detail << ", " << trs("CRC: V5");
268 if (sb.has_ftype()) detail << ", FTYPE";
270 detail << ", FINOBT";
271 }
273 detail << ", REFLINK";
274 }
275 }
276 const std::string lbl = sb.label();
277 if (!lbl.empty()) {
278 detail << ", " << trs("Label") << ": " << lbl;
279 }
280
281 // Confidence: V5 has CRC and stricter validation, so very high.
282 // V4 is also reliable when it parses, but slightly lower.
283 const double confidence = sb.is_v5() ? 0.96 : 0.92;
284
286 offset,
288 "XFS volume",
289 "XFS probe",
290 detail.str(),
291 confidence};
292}
293
294// HFS+ / HFSX probe. The volume header lives at +1024 from the volume
295// start. Delegates field validation to hfs::parse_volume_header.
296ScanResult hfs_probe(DiskReader& reader, uint64_t offset) {
297 std::array<unsigned char, 512> sector{};
298 if (!reader.read_at(offset + hfs::HFSPLUS_VOLUME_HEADER_OFFSET,
299 sector.data(), sector.size())) {
300 return {};
301 }
303 if (!hfs::parse_volume_header(sector.data(), vh)) {
304 return {};
305 }
306
307 std::ostringstream detail;
308 detail << trs("Variant") << ": " << vh.variant_name()
309 << ", " << trs("Version") << ": " << vh.version
310 << ", " << trs("Block size") << ": " << vh.block_size
311 << ", " << trs("Total blocks") << ": " << vh.total_blocks
312 << ", " << trs("Free blocks") << ": " << vh.free_blocks
313 << ", " << trs("Files") << ": " << vh.file_count
314 << ", " << trs("Folders") << ": " << vh.folder_count;
315 if (vh.is_journaled()) {
316 detail << ", " << trs("Journaled");
317 }
318
319 // Confidence: high. The volume header is a strong magic + structured
320 // sanity (block_size power of 2, fork extents in range, etc.).
321 const double confidence = vh.is_hfsx() ? 0.96 : 0.95;
322
324 offset,
326 vh.is_hfsx() ? "HFSX volume" : "HFS+ volume",
327 "HFS+ probe",
328 detail.str(),
329 confidence};
330}
331}
332
333std::vector<ScanResult> FilesystemProbes::analyze(DiskReader& reader,
334 const std::vector<ScanResult>& seed_results,
335 std::vector<std::string>& logs) {
336 std::vector<ScanResult> out;
337 std::set<uint64_t> seen_offsets;
338 logs.push_back(trs("Filesystem probes: analysis started."));
339
340 for (const auto& seed : seed_results) {
341 const bool interesting_seed =
342 seed.kind == ResultKind::Partition ||
344 seed.kind == ResultKind::FilesystemCandidate ||
345 seed.kind == ResultKind::NtfsVolume;
346 if (!interesting_seed || seen_offsets.count(seed.offset_bytes) != 0) {
347 continue;
348 }
349 seen_offsets.insert(seed.offset_bytes);
350
351 if (auto r = fat32_probe(reader, seed.offset_bytes); !r.type.empty()) out.push_back(r);
352 if (auto r = exfat_probe(reader, seed.offset_bytes); !r.type.empty()) out.push_back(r);
353 if (auto r = ext4_probe(reader, seed.offset_bytes); !r.type.empty()) out.push_back(r);
354 if (auto r = btrfs_probe(reader, seed.offset_bytes); !r.type.empty()) out.push_back(r);
355 if (auto r = xfs_probe(reader, seed.offset_bytes); !r.type.empty()) out.push_back(r);
356 if (auto r = hfs_probe(reader, seed.offset_bytes); !r.type.empty()) out.push_back(r);
357 }
358
359 logs.push_back(trs("Filesystem probes: analysis completed."));
360 return out;
361}
bool read_at(uint64_t offset, void *buffer, size_t len)
static std::vector< ScanResult > analyze(DiskReader &reader, const std::vector< ScanResult > &seed_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
@ FilesystemCandidate
Definition models.hpp:10
@ FilesystemVolume
Definition models.hpp:11
bool is_exfat(const std::array< unsigned char, 512 > &sector)
bool is_fat32(const std::array< unsigned char, 512 > &sector)
ScanResult fat32_probe(DiskReader &reader, uint64_t offset)
uint64_t le64(const unsigned char *p)
bool read_sector(DiskReader &reader, uint64_t offset, std::array< unsigned char, 512 > &sector)
ScanResult exfat_probe(DiskReader &reader, uint64_t offset)
bool ext4_has_magic(DiskReader &reader, uint64_t offset)
bool btrfs_has_magic(DiskReader &reader, uint64_t offset)
uint16_t le16(const unsigned char *p)
ScanResult hfs_probe(DiskReader &reader, uint64_t offset)
uint32_t le32(const unsigned char *p)
ScanResult xfs_probe(DiskReader &reader, uint64_t offset)
ScanResult ext4_probe(DiskReader &reader, uint64_t offset)
ScanResult btrfs_probe(DiskReader &reader, uint64_t offset)
bool parse_volume_header(const unsigned char *buf, VolumeHeader &out)
constexpr uint32_t HFSPLUS_VOLUME_HEADER_OFFSET
bool parse_superblock(const unsigned char *buf, Superblock &out)
constexpr uint32_t XFS_SB_FEAT_RO_COMPAT_FINOBT
constexpr uint32_t XFS_SB_FEAT_RO_COMPAT_REFLINK
bool is_hfsx() const
uint64_t volume_size_bytes() const
bool is_journaled() const
const char * variant_name() const
uint64_t volume_size_bytes() const
std::string label() const
unsigned format_version() const
uint32_t features_ro_compat
bool is_v5() const
bool has_ftype() const