feat: PDB import via RawPDB, no msdia140.dll dependency

Replace DIA SDK COM-based PDB importer with RawPDB (MolecularMatters)
which reads PDB files directly via memory-mapped I/O. Adds File menu
"Import PDB..." dialog with type filtering, selection, and progress.

- Vendor raw_pdb into third_party/
- Two-phase API: enumeratePdbTypes() + importPdbSelected()
- Full recursive import of structs/unions/arrays/pointers/bitfields
- PDB import dialog with name filter, select-all, type count
- Benchmark: 1654 types from ntkrnlmp.pdb in 16ms
- Reorganize import/export files into src/imports/
This commit is contained in:
IChooseYou
2026-02-21 17:18:24 -07:00
parent 3a76b03c85
commit 1d7d384b93
100 changed files with 11627 additions and 17 deletions

View File

@@ -0,0 +1,82 @@
#include <QtTest/QtTest>
#include "core.h"
#include "imports/import_pdb.h"
using namespace rcx;
class BenchImportPdb : public QObject {
Q_OBJECT
private slots:
void benchEnumerateAll();
void benchImportAll();
};
static const QString kPdbPath = QStringLiteral(
"C:/Symbols/ntkrnlmp.pdb/0762CF42EF7F3E8116EF7329ADAA09A31/ntkrnlmp.pdb");
void BenchImportPdb::benchEnumerateAll() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
QElapsedTimer timer;
timer.start();
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
qint64 elapsed = timer.elapsed();
QVERIFY2(!types.isEmpty(), qPrintable(err));
qDebug() << "enumeratePdbTypes:" << types.size() << "types in" << elapsed << "ms";
}
void BenchImportPdb::benchImportAll() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
// Phase 1: enumerate
QString err;
QElapsedTimer timer;
timer.start();
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
qint64 enumerateMs = timer.elapsed();
QVERIFY2(!types.isEmpty(), qPrintable(err));
// Collect all type indices
QVector<uint32_t> indices;
indices.reserve(types.size());
for (const auto& t : types)
indices.append(t.typeIndex);
// Phase 2: import all
timer.restart();
int lastProgress = 0;
NodeTree tree = importPdbSelected(kPdbPath, indices, &err,
[&](int cur, int total) -> bool {
// Report progress at 25% intervals
int pct = (cur * 100) / total;
if (pct >= lastProgress + 25) {
qDebug() << " progress:" << cur << "/" << total
<< "(" << pct << "%)";
lastProgress = pct;
}
return true;
});
qint64 importMs = timer.elapsed();
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
// Count root structs
int rootCount = 0;
for (const auto& n : tree.nodes)
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
qDebug() << "";
qDebug() << "=== PDB Import Benchmark (ntkrnlmp.pdb) ===";
qDebug() << " Enumerate:" << types.size() << "types in" << enumerateMs << "ms";
qDebug() << " Import all:" << rootCount << "root structs,"
<< tree.nodes.size() << "total nodes in" << importMs << "ms";
qDebug() << " Total:" << (enumerateMs + importMs) << "ms";
qDebug() << "============================================";
}
QTEST_MAIN(BenchImportPdb)
#include "bench_import_pdb.moc"

View File

@@ -1,8 +1,8 @@
#include <QtTest/QtTest>
#include <QTemporaryFile>
#include "core.h"
#include "export_reclass_xml.h"
#include "import_reclass_xml.h"
#include "imports/export_reclass_xml.h"
#include "imports/import_reclass_xml.h"
using namespace rcx;

237
tests/test_import_pdb.cpp Normal file
View File

@@ -0,0 +1,237 @@
#include <QtTest/QtTest>
#include "core.h"
#include "imports/import_pdb.h"
using namespace rcx;
class TestImportPdb : public QObject {
Q_OBJECT
private slots:
void missingFileReturnsError();
void importKProcess();
void verifyDispatcherHeader();
void verifyListEntry();
void importFilteredStruct();
void enumerateTypes();
void importSelected();
};
static const QString kPdbPath = QStringLiteral(
"C:/Symbols/ntkrnlmp.pdb/0762CF42EF7F3E8116EF7329ADAA09A31/ntkrnlmp.pdb");
// Find a root struct by structTypeName
static int findRootStruct(const NodeTree& tree, const QString& name) {
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].parentId == 0 &&
tree.nodes[i].kind == NodeKind::Struct &&
tree.nodes[i].structTypeName == name)
return i;
}
return -1;
}
// Find a child of parentId by name
static int findChildNode(const NodeTree& tree, uint64_t parentId, const QString& name) {
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].parentId == parentId && tree.nodes[i].name == name)
return i;
}
return -1;
}
void TestImportPdb::missingFileReturnsError() {
QString err;
NodeTree tree = importPdb(QStringLiteral("C:/nonexistent.pdb"), {}, &err);
QVERIFY(tree.nodes.isEmpty());
QVERIFY(!err.isEmpty());
}
void TestImportPdb::importKProcess() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
// Find _KPROCESS root struct
int kpIdx = findRootStruct(tree, QStringLiteral("_KPROCESS"));
QVERIFY2(kpIdx >= 0, "Expected _KPROCESS root struct");
uint64_t kpId = tree.nodes[kpIdx].id;
// Verify Header field at offset 0 → embedded _DISPATCHER_HEADER
int headerIdx = findChildNode(tree, kpId, QStringLiteral("Header"));
QVERIFY2(headerIdx >= 0, "Expected 'Header' child of _KPROCESS");
QCOMPARE(tree.nodes[headerIdx].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[headerIdx].structTypeName, QStringLiteral("_DISPATCHER_HEADER"));
QCOMPARE(tree.nodes[headerIdx].offset, 0);
// Verify ProfileListHead at offset 0x18 → embedded _LIST_ENTRY
int profileIdx = findChildNode(tree, kpId, QStringLiteral("ProfileListHead"));
QVERIFY2(profileIdx >= 0, "Expected 'ProfileListHead' child of _KPROCESS");
QCOMPARE(tree.nodes[profileIdx].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[profileIdx].structTypeName, QStringLiteral("_LIST_ENTRY"));
QCOMPARE(tree.nodes[profileIdx].offset, 0x18);
}
void TestImportPdb::verifyDispatcherHeader() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
// _DISPATCHER_HEADER should be imported as a transitive dependency
int dhIdx = findRootStruct(tree, QStringLiteral("_DISPATCHER_HEADER"));
QVERIFY2(dhIdx >= 0, "_DISPATCHER_HEADER should be imported as a dependency");
uint64_t dhId = tree.nodes[dhIdx].id;
auto kids = tree.childrenOf(dhId);
QVERIFY2(!kids.isEmpty(), "_DISPATCHER_HEADER should have children (fields)");
// Look for WaitListHead — a _LIST_ENTRY at offset 0x10 in most builds
int waitIdx = findChildNode(tree, dhId, QStringLiteral("WaitListHead"));
QVERIFY2(waitIdx >= 0, "Expected 'WaitListHead' in _DISPATCHER_HEADER");
QCOMPARE(tree.nodes[waitIdx].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[waitIdx].structTypeName, QStringLiteral("_LIST_ENTRY"));
}
void TestImportPdb::verifyListEntry() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
// _LIST_ENTRY should be imported (used by ProfileListHead and others)
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
QVERIFY2(leIdx >= 0, "_LIST_ENTRY should be imported");
uint64_t leId = tree.nodes[leIdx].id;
// Flink at offset 0 — pointer to _LIST_ENTRY
int flinkIdx = findChildNode(tree, leId, QStringLiteral("Flink"));
QVERIFY2(flinkIdx >= 0, "Expected 'Flink' in _LIST_ENTRY");
QCOMPARE(tree.nodes[flinkIdx].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[flinkIdx].offset, 0);
// Blink at offset 8 — pointer to _LIST_ENTRY
int blinkIdx = findChildNode(tree, leId, QStringLiteral("Blink"));
QVERIFY2(blinkIdx >= 0, "Expected 'Blink' in _LIST_ENTRY");
QCOMPARE(tree.nodes[blinkIdx].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[blinkIdx].offset, 8);
// Both should point back to _LIST_ENTRY (self-referencing)
QCOMPARE(tree.nodes[flinkIdx].refId, leId);
QCOMPARE(tree.nodes[blinkIdx].refId, leId);
}
void TestImportPdb::importFilteredStruct() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_LIST_ENTRY"), &err);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
QVERIFY(leIdx >= 0);
// _LIST_ENTRY only references itself, so exactly 1 root struct
int rootCount = 0;
for (const auto& n : tree.nodes)
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
QCOMPARE(rootCount, 1);
}
void TestImportPdb::enumerateTypes() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
QVERIFY2(!types.isEmpty(), qPrintable(err));
// Should have hundreds of types in ntkrnlmp
QVERIFY2(types.size() > 100,
qPrintable(QStringLiteral("Expected >100 types, got %1").arg(types.size())));
// Verify _KPROCESS is present
bool foundKProcess = false;
bool foundListEntry = false;
for (const auto& t : types) {
if (t.name == QStringLiteral("_KPROCESS")) {
foundKProcess = true;
QVERIFY2(t.childCount > 0, "_KPROCESS should have children");
QVERIFY2(t.size > 0, "_KPROCESS should have non-zero size");
}
if (t.name == QStringLiteral("_LIST_ENTRY")) {
foundListEntry = true;
}
}
QVERIFY2(foundKProcess, "_KPROCESS not found in enumerated types");
QVERIFY2(foundListEntry, "_LIST_ENTRY not found in enumerated types");
}
void TestImportPdb::importSelected() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
// First enumerate to find _LIST_ENTRY's type index
QString err;
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
QVERIFY2(!types.isEmpty(), qPrintable(err));
uint32_t listEntryIdx = 0;
bool found = false;
for (const auto& t : types) {
if (t.name == QStringLiteral("_LIST_ENTRY")) {
listEntryIdx = t.typeIndex;
found = true;
break;
}
}
QVERIFY2(found, "_LIST_ENTRY not found in enumeration");
// Import just _LIST_ENTRY
QVector<uint32_t> indices = { listEntryIdx };
int progressCalls = 0;
NodeTree tree = importPdbSelected(kPdbPath, indices, &err,
[&](int cur, int total) -> bool {
progressCalls++;
Q_UNUSED(total);
Q_ASSERT(cur <= total);
return true; // don't cancel
});
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
QVERIFY(progressCalls > 0);
// Verify _LIST_ENTRY root struct
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
QVERIFY2(leIdx >= 0, "_LIST_ENTRY should be imported");
// Flink and Blink
uint64_t leId = tree.nodes[leIdx].id;
int flinkIdx = findChildNode(tree, leId, QStringLiteral("Flink"));
QVERIFY2(flinkIdx >= 0, "Expected 'Flink' in _LIST_ENTRY");
QCOMPARE(tree.nodes[flinkIdx].kind, NodeKind::Pointer64);
int blinkIdx = findChildNode(tree, leId, QStringLiteral("Blink"));
QVERIFY2(blinkIdx >= 0, "Expected 'Blink' in _LIST_ENTRY");
QCOMPARE(tree.nodes[blinkIdx].kind, NodeKind::Pointer64);
// Self-referencing pointers
QCOMPARE(tree.nodes[flinkIdx].refId, leId);
QCOMPARE(tree.nodes[blinkIdx].refId, leId);
// Only 1 root struct
int rootCount = 0;
for (const auto& n : tree.nodes)
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
QCOMPARE(rootCount, 1);
}
QTEST_MAIN(TestImportPdb)
#include "test_import_pdb.moc"

View File

@@ -1,6 +1,6 @@
#include <QtTest/QtTest>
#include "core.h"
#include "import_source.h"
#include "imports/import_source.h"
using namespace rcx;

View File

@@ -1,6 +1,6 @@
#include <QtTest/QtTest>
#include "core.h"
#include "import_reclass_xml.h"
#include "imports/import_reclass_xml.h"
using namespace rcx;