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,204 @@
#include "export_reclass_xml.h"
#include <QFile>
#include <QXmlStreamWriter>
#include <QHash>
#include <QVector>
#include <algorithm>
namespace rcx {
// Reverse type map: NodeKind -> ReClassEx V2016 XML Type integer
static int xmlTypeForKind(NodeKind kind) {
switch (kind) {
case NodeKind::Struct: return 1; // ClassInstance
case NodeKind::Hex32: return 4;
case NodeKind::Hex64: return 5;
case NodeKind::Hex16: return 6;
case NodeKind::Hex8: return 7;
case NodeKind::Pointer64: return 8; // ClassPointer
case NodeKind::Pointer32: return 8;
case NodeKind::Int64: return 9;
case NodeKind::Int32: return 10;
case NodeKind::Int16: return 11;
case NodeKind::Int8: return 12;
case NodeKind::Float: return 13;
case NodeKind::Double: return 14;
case NodeKind::UInt32: return 15;
case NodeKind::UInt16: return 16;
case NodeKind::UInt8: return 17;
case NodeKind::UInt64: return 32;
case NodeKind::UTF8: return 18;
case NodeKind::UTF16: return 19;
case NodeKind::Bool: return 17; // No native bool in ReClass, map to UInt8
case NodeKind::Vec2: return 22;
case NodeKind::Vec3: return 23;
case NodeKind::Vec4: return 24;
case NodeKind::Mat4x4: return 25;
case NodeKind::Array: return 27; // ClassInstanceArray
}
return 7; // fallback to Hex8
}
static int nodeSizeForExport(const Node& node) {
switch (node.kind) {
case NodeKind::UTF8: return node.strLen;
case NodeKind::UTF16: return node.strLen * 2;
case NodeKind::Array: {
int elemSz = sizeForKind(node.elementKind);
return node.arrayLen * (elemSz > 0 ? elemSz : 0);
}
default: return sizeForKind(node.kind);
}
}
// Resolve a struct type name from a node ID
static QString resolveStructName(const NodeTree& tree, uint64_t refId) {
int idx = tree.indexOfId(refId);
if (idx < 0) return {};
const Node& ref = tree.nodes[idx];
if (!ref.structTypeName.isEmpty()) return ref.structTypeName;
return ref.name;
}
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg) {
if (tree.nodes.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("No nodes to export");
return false;
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file for writing: ") + filePath;
return false;
}
// Build child map
QHash<uint64_t, QVector<int>> childMap;
for (int i = 0; i < tree.nodes.size(); i++)
childMap[tree.nodes[i].parentId].append(i);
QXmlStreamWriter xml(&file);
xml.setAutoFormatting(true);
xml.setAutoFormattingIndent(4);
xml.writeStartDocument();
xml.writeStartElement(QStringLiteral("ReClass"));
xml.writeComment(QStringLiteral("ReClassEx"));
// Get root structs
QVector<int> roots = childMap.value(0);
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
int classCount = 0;
for (int ri : roots) {
const Node& root = tree.nodes[ri];
if (root.kind != NodeKind::Struct) continue;
xml.writeStartElement(QStringLiteral("Class"));
xml.writeAttribute(QStringLiteral("Name"), root.name.isEmpty() ? root.structTypeName : root.name);
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("28"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
xml.writeAttribute(QStringLiteral("Offset"), QStringLiteral("0"));
xml.writeAttribute(QStringLiteral("strOffset"), QStringLiteral("0"));
xml.writeAttribute(QStringLiteral("Code"), QString());
// Get children sorted by offset
QVector<int> children = childMap.value(root.id);
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
int i = 0;
while (i < children.size()) {
const Node& child = tree.nodes[children[i]];
// Collapse consecutive hex nodes into a single Custom node (Type=21)
if (isHexNode(child.kind)) {
int runStart = child.offset;
int runEnd = child.offset + child.byteSize();
int j = i + 1;
while (j < children.size()) {
const Node& next = tree.nodes[children[j]];
if (!isHexNode(next.kind)) break;
if (next.offset < runEnd) break; // overlap
runEnd = next.offset + next.byteSize();
j++;
}
int totalSize = runEnd - runStart;
xml.writeStartElement(QStringLiteral("Node"));
// Use first hex node's name if it's a single node, otherwise generate
QString hexName = (j - i == 1 && !child.name.isEmpty()) ? child.name : QString();
xml.writeAttribute(QStringLiteral("Name"), hexName);
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("21")); // Custom
xml.writeAttribute(QStringLiteral("Size"), QString::number(totalSize));
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
xml.writeEndElement(); // Node
i = j;
continue;
}
xml.writeStartElement(QStringLiteral("Node"));
xml.writeAttribute(QStringLiteral("Name"), child.name);
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(child.kind)));
xml.writeAttribute(QStringLiteral("Size"), QString::number(nodeSizeForExport(child)));
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
// Pointer with target
if ((child.kind == NodeKind::Pointer64 || child.kind == NodeKind::Pointer32) && child.refId != 0) {
QString target = resolveStructName(tree, child.refId);
if (!target.isEmpty())
xml.writeAttribute(QStringLiteral("Pointer"), target);
}
// Embedded struct instance
if (child.kind == NodeKind::Struct) {
QString instName = child.structTypeName.isEmpty() ? child.name : child.structTypeName;
xml.writeAttribute(QStringLiteral("Instance"), instName);
}
// Array: Total attribute and child <Array> element
if (child.kind == NodeKind::Array) {
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
// Resolve element type name
QString elemName;
if (child.elementKind == NodeKind::Struct && !child.structTypeName.isEmpty()) {
elemName = child.structTypeName;
} else if (child.refId != 0) {
elemName = resolveStructName(tree, child.refId);
}
if (elemName.isEmpty())
elemName = kindToString(child.elementKind);
xml.writeStartElement(QStringLiteral("Array"));
xml.writeAttribute(QStringLiteral("Name"), elemName);
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
xml.writeEndElement(); // Array
}
xml.writeEndElement(); // Node
i++;
}
xml.writeEndElement(); // Class
classCount++;
}
xml.writeEndElement(); // ReClass
xml.writeEndDocument();
file.close();
if (classCount == 0) {
if (errorMsg) *errorMsg = QStringLiteral("No struct classes found to export");
return false;
}
return true;
}
} // namespace rcx

View File

@@ -0,0 +1,10 @@
#pragma once
#include "core.h"
namespace rcx {
// Export a NodeTree to ReClass .NET / ReClassEx compatible XML format.
// Returns true on success; populates errorMsg on failure if non-null.
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg = nullptr);
} // namespace rcx

971
src/imports/import_pdb.cpp Normal file
View File

@@ -0,0 +1,971 @@
#include "import_pdb.h"
#ifdef _WIN32
#include <windows.h>
#include <QFile>
#include <QHash>
#include <QPair>
#include <QSet>
// ── RawPDB headers ──
#include "PDB.h"
#include "PDB_RawFile.h"
#include "PDB_TPIStream.h"
#include "PDB_TPITypes.h"
#include "PDB_DBIStream.h"
#include "PDB_InfoStream.h"
#include "PDB_CoalescedMSFStream.h"
#include "Foundation/PDB_Memory.h"
namespace rcx {
// ── Memory-mapped file (mirrors ExampleMemoryMappedFile) ──
struct MappedFile {
HANDLE hFile = INVALID_HANDLE_VALUE;
HANDLE hMapping = nullptr;
const void* base = nullptr;
size_t size = 0;
bool open(const QString& path) {
hFile = CreateFileW(reinterpret_cast<const wchar_t*>(path.utf16()),
GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
if (hFile == INVALID_HANDLE_VALUE) return false;
hMapping = CreateFileMappingW(hFile, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!hMapping) { close(); return false; }
base = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (!base) { close(); return false; }
BY_HANDLE_FILE_INFORMATION info;
if (!GetFileInformationByHandle(hFile, &info)) { close(); return false; }
size = (static_cast<size_t>(info.nFileSizeHigh) << 32) | info.nFileSizeLow;
return true;
}
void close() {
if (base) { UnmapViewOfFile(base); base = nullptr; }
if (hMapping) { CloseHandle(hMapping); hMapping = nullptr; }
if (hFile != INVALID_HANDLE_VALUE) { CloseHandle(hFile); hFile = INVALID_HANDLE_VALUE; }
size = 0;
}
~MappedFile() { close(); }
MappedFile() = default;
MappedFile(const MappedFile&) = delete;
MappedFile& operator=(const MappedFile&) = delete;
};
// ── TypeTable (mirrors ExampleTypeTable) ──
// Builds an O(1) lookup table from type index → record pointer.
class TypeTable {
public:
explicit TypeTable(const PDB::TPIStream& tpiStream) {
m_firstIndex = tpiStream.GetFirstTypeIndex();
m_lastIndex = tpiStream.GetLastTypeIndex();
m_count = tpiStream.GetTypeRecordCount();
const PDB::DirectMSFStream& ds = tpiStream.GetDirectMSFStream();
m_stream = PDB::CoalescedMSFStream(ds, ds.GetSize(), 0u);
m_records = PDB_NEW_ARRAY(const PDB::CodeView::TPI::Record*, m_count);
uint32_t idx = 0;
tpiStream.ForEachTypeRecordHeaderAndOffset(
[this, &idx](const PDB::CodeView::TPI::RecordHeader&, size_t offset) {
m_records[idx++] = m_stream.GetDataAtOffset<const PDB::CodeView::TPI::Record>(offset);
});
}
~TypeTable() { PDB_DELETE_ARRAY(m_records); }
uint32_t firstIndex() const { return m_firstIndex; }
uint32_t lastIndex() const { return m_lastIndex; }
size_t count() const { return m_count; }
const PDB::CodeView::TPI::Record* get(uint32_t typeIndex) const {
if (typeIndex < m_firstIndex || typeIndex >= m_lastIndex) return nullptr;
return m_records[typeIndex - m_firstIndex];
}
private:
uint32_t m_firstIndex = 0;
uint32_t m_lastIndex = 0;
size_t m_count = 0;
const PDB::CodeView::TPI::Record** m_records = nullptr;
PDB::CoalescedMSFStream m_stream;
TypeTable(const TypeTable&) = delete;
TypeTable& operator=(const TypeTable&) = delete;
};
// ── Leaf numeric helpers (variable-length integer encoding) ──
using TRK = PDB::CodeView::TPI::TypeRecordKind;
static uint8_t leafSize(TRK kind) {
if (kind < TRK::LF_NUMERIC) return sizeof(TRK); // value is the kind itself
switch (kind) {
case TRK::LF_CHAR: return sizeof(TRK) + sizeof(uint8_t);
case TRK::LF_SHORT:
case TRK::LF_USHORT: return sizeof(TRK) + sizeof(uint16_t);
case TRK::LF_LONG:
case TRK::LF_ULONG: return sizeof(TRK) + sizeof(uint32_t);
case TRK::LF_QUADWORD:
case TRK::LF_UQUADWORD: return sizeof(TRK) + sizeof(uint64_t);
default: return sizeof(TRK);
}
}
static const char* leafName(const char* data, TRK kind) {
return data + leafSize(kind);
}
static uint64_t leafValue(const char* data, TRK kind) {
if (kind < TRK::LF_NUMERIC) {
return static_cast<uint16_t>(kind);
}
const char* p = data + sizeof(TRK);
switch (kind) {
case TRK::LF_CHAR: return *reinterpret_cast<const uint8_t*>(p);
case TRK::LF_SHORT: return *reinterpret_cast<const int16_t*>(p);
case TRK::LF_USHORT: return *reinterpret_cast<const uint16_t*>(p);
case TRK::LF_LONG: return *reinterpret_cast<const int32_t*>(p);
case TRK::LF_ULONG: return *reinterpret_cast<const uint32_t*>(p);
case TRK::LF_QUADWORD: return *reinterpret_cast<const int64_t*>(p);
case TRK::LF_UQUADWORD: return *reinterpret_cast<const uint64_t*>(p);
default: return 0;
}
}
// ── Primitive type index mapping (< 0x1000) ──
static NodeKind mapPrimitiveType(uint32_t typeIndex) {
uint32_t base = typeIndex & 0xFF;
switch (base) {
// void
case 0x03: return NodeKind::Hex8;
// signed char
case 0x10: return NodeKind::Int8;
// unsigned char
case 0x20: return NodeKind::UInt8;
// real char
case 0x70: return NodeKind::Int8;
// wchar
case 0x71: return NodeKind::UInt16;
// char8
case 0x7c: return NodeKind::UInt8;
// char16
case 0x7a: return NodeKind::UInt16;
// char32
case 0x7b: return NodeKind::UInt32;
// short
case 0x11: return NodeKind::Int16;
// ushort
case 0x21: return NodeKind::UInt16;
// long
case 0x12: return NodeKind::Int32;
// ulong
case 0x22: return NodeKind::UInt32;
// int8
case 0x68: return NodeKind::Int8;
// uint8
case 0x69: return NodeKind::UInt8;
// int16
case 0x72: return NodeKind::Int16;
// uint16
case 0x73: return NodeKind::UInt16;
// int32
case 0x74: return NodeKind::Int32;
// uint32
case 0x75: return NodeKind::UInt32;
// quad (int64)
case 0x13: return NodeKind::Int64;
// uquad (uint64)
case 0x23: return NodeKind::UInt64;
// int64
case 0x76: return NodeKind::Int64;
// uint64
case 0x77: return NodeKind::UInt64;
// float
case 0x40: return NodeKind::Float;
// double
case 0x41: return NodeKind::Double;
// bool
case 0x30: return NodeKind::Bool;
case 0x31: return NodeKind::UInt16; // bool16
case 0x32: return NodeKind::UInt32; // bool32
case 0x33: return NodeKind::UInt64; // bool64
// HRESULT
case 0x08: return NodeKind::UInt32;
// bit
case 0x60: return NodeKind::UInt8;
// int128 / uint128 approximation
case 0x78: return NodeKind::Hex64; // int128 → Hex64 (best we can do)
case 0x79: return NodeKind::Hex64; // uint128
default: return NodeKind::Hex32;
}
}
static NodeKind hexForSize(uint64_t len) {
switch (len) {
case 1: return NodeKind::Hex8;
case 2: return NodeKind::Hex16;
case 4: return NodeKind::Hex32;
case 8: return NodeKind::Hex64;
default: return NodeKind::Hex32;
}
}
// ── Helper: read the leaf kind from the start of LF_UNION.data ──
// (LF_UNION lacks the lfEasy member that LF_CLASS has)
static TRK unionLeafKind(const char* data) {
return *reinterpret_cast<const TRK*>(data);
}
// ── Import context ──
struct PdbCtx {
NodeTree tree;
const TypeTable* tt = nullptr;
QHash<uint32_t, uint64_t> typeCache; // typeIndex → nodeId
uint64_t importUDT(uint32_t typeIndex);
void importFieldList(uint32_t fieldListIndex, uint64_t parentId);
void importMemberType(uint32_t typeIndex, int offset, const QString& name, uint64_t parentId);
// Resolve LF_MODIFIER chain to underlying type index
uint32_t unwrapModifier(uint32_t typeIndex) const {
if (typeIndex < tt->firstIndex()) return typeIndex;
const auto* rec = tt->get(typeIndex);
if (!rec) return typeIndex;
if (rec->header.kind == TRK::LF_MODIFIER)
return rec->data.LF_MODIFIER.type;
return typeIndex;
}
};
uint64_t PdbCtx::importUDT(uint32_t typeIndex) {
if (typeIndex < tt->firstIndex()) return 0;
auto it = typeCache.find(typeIndex);
if (it != typeCache.end()) return it.value();
const auto* rec = tt->get(typeIndex);
if (!rec) return 0;
const char* name = nullptr;
uint32_t fieldListIndex = 0;
uint16_t fieldCount = 0;
bool isUnion = false;
const char* sizeData = nullptr;
if (rec->header.kind == TRK::LF_STRUCTURE || rec->header.kind == TRK::LF_CLASS) {
// Skip forward references — find the definition
if (rec->data.LF_CLASS.property.fwdref) return 0;
fieldCount = rec->data.LF_CLASS.count;
fieldListIndex = rec->data.LF_CLASS.field;
sizeData = rec->data.LF_CLASS.data;
name = leafName(sizeData, rec->data.LF_CLASS.lfEasy.kind);
} else if (rec->header.kind == TRK::LF_UNION) {
if (rec->data.LF_UNION.property.fwdref) return 0;
isUnion = true;
fieldCount = rec->data.LF_UNION.count;
fieldListIndex = rec->data.LF_UNION.field;
sizeData = rec->data.LF_UNION.data;
name = leafName(sizeData, unionLeafKind(sizeData));
} else {
return 0;
}
(void)fieldCount;
QString qname = name ? QString::fromUtf8(name) : QStringLiteral("<anon>");
Node s;
s.kind = NodeKind::Struct;
s.name = qname;
s.structTypeName = qname;
s.classKeyword = isUnion ? QStringLiteral("union") : QStringLiteral("struct");
s.parentId = 0;
s.collapsed = true;
int idx = tree.addNode(s);
uint64_t nodeId = tree.nodes[idx].id;
typeCache[typeIndex] = nodeId;
importFieldList(fieldListIndex, nodeId);
return nodeId;
}
void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
const auto* rec = tt->get(fieldListIndex);
if (!rec || rec->header.kind != TRK::LF_FIELDLIST) return;
auto maximumSize = rec->header.size - sizeof(uint16_t);
QSet<QPair<int,int>> bitfieldSlots;
for (size_t i = 0; i < maximumSize; ) {
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
reinterpret_cast<const uint8_t*>(&rec->data.LF_FIELD.list) + i);
if (field->kind == TRK::LF_MEMBER) {
// Extract offset from variable-length leaf
uint16_t offset = 0;
if (field->data.LF_MEMBER.lfEasy.kind < TRK::LF_NUMERIC)
offset = *reinterpret_cast<const uint16_t*>(field->data.LF_MEMBER.offset);
else
offset = static_cast<uint16_t>(leafValue(field->data.LF_MEMBER.offset,
field->data.LF_MEMBER.lfEasy.kind));
const char* memberName = leafName(field->data.LF_MEMBER.offset,
field->data.LF_MEMBER.lfEasy.kind);
uint32_t memberType = field->data.LF_MEMBER.index;
QString qname = memberName ? QString::fromUtf8(memberName) : QString();
// Check for bitfield type
uint32_t resolvedType = unwrapModifier(memberType);
const auto* typeRec = tt->get(resolvedType);
if (typeRec && typeRec->header.kind == TRK::LF_BITFIELD) {
uint32_t underlying = typeRec->data.LF_BITFIELD.type;
uint8_t bitLen = typeRec->data.LF_BITFIELD.length;
(void)bitLen;
// Determine slot size from underlying type
uint64_t slotSize = 4;
if (underlying < tt->firstIndex()) {
NodeKind k = mapPrimitiveType(underlying);
slotSize = sizeForKind(k);
}
auto key = qMakePair((int)offset, (int)slotSize);
if (!bitfieldSlots.contains(key)) {
bitfieldSlots.insert(key);
Node n;
n.kind = hexForSize(slotSize);
n.name = qname;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
}
} else {
importMemberType(memberType, offset, qname, parentId);
}
// Advance past this LF_MEMBER
i += static_cast<size_t>(memberName - reinterpret_cast<const char*>(field));
i += strnlen(memberName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3); // align to 4
}
else if (field->kind == TRK::LF_BCLASS) {
const char* leafEnd = leafName(field->data.LF_BCLASS.offset,
field->data.LF_BCLASS.lfEasy.kind);
i += static_cast<size_t>(leafEnd - reinterpret_cast<const char*>(field));
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_VBCLASS || field->kind == TRK::LF_IVBCLASS) {
TRK vbpKind = *reinterpret_cast<const TRK*>(field->data.LF_IVBCLASS.vbpOffset);
uint8_t vbpSize1 = leafSize(vbpKind);
TRK vbtKind = *reinterpret_cast<const TRK*>(field->data.LF_IVBCLASS.vbpOffset + vbpSize1);
uint8_t vbpSize2 = leafSize(vbtKind);
i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_VBCLASS) + vbpSize1 + vbpSize2;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_INDEX) {
// Continuation of field list in another record
importFieldList(field->data.LF_INDEX.type, parentId);
i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_INDEX);
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_VFUNCTAB) {
i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_VFUNCTAB);
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_NESTTYPE) {
const char* nestName = field->data.LF_NESTTYPE.name;
i += static_cast<size_t>(nestName - reinterpret_cast<const char*>(field));
i += strnlen(nestName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_STMEMBER) {
const char* smName = field->data.LF_STMEMBER.name;
i += static_cast<size_t>(smName - reinterpret_cast<const char*>(field));
i += strnlen(smName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_METHOD) {
const char* mName = field->data.LF_METHOD.name;
i += static_cast<size_t>(mName - reinterpret_cast<const char*>(field));
i += strnlen(mName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_ONEMETHOD) {
// Determine if it has a vbaseoff field
auto prop = static_cast<PDB::CodeView::TPI::MethodProperty>(
field->data.LF_ONEMETHOD.attributes.mprop);
const char* mName;
if (prop == PDB::CodeView::TPI::MethodProperty::Intro ||
prop == PDB::CodeView::TPI::MethodProperty::PureIntro)
mName = reinterpret_cast<const char*>(field->data.LF_ONEMETHOD.vbaseoff) + sizeof(uint32_t);
else
mName = reinterpret_cast<const char*>(field->data.LF_ONEMETHOD.vbaseoff);
i += static_cast<size_t>(mName - reinterpret_cast<const char*>(field));
i += strnlen(mName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_ENUMERATE) {
const char* eName = leafName(field->data.LF_ENUMERATE.value,
field->data.LF_ENUMERATE.lfEasy.kind);
i += static_cast<size_t>(eName - reinterpret_cast<const char*>(field));
i += strnlen(eName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else {
break; // unknown field kind, stop
}
}
}
void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& name, uint64_t parentId) {
// Handle primitive type indices (< 0x1000)
if (typeIndex < tt->firstIndex()) {
uint32_t ptrMode = (typeIndex >> 8) & 0xF;
if (ptrMode == 0x04 || ptrMode == 0x05) {
// 32-bit pointer to a base type
Node n;
n.kind = NodeKind::Pointer32;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.collapsed = true;
tree.addNode(n);
return;
}
if (ptrMode == 0x06) {
// 64-bit pointer to a base type
Node n;
n.kind = NodeKind::Pointer64;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.collapsed = true;
tree.addNode(n);
return;
}
if (ptrMode != 0x00) {
// Some other pointer mode (near, far, huge) — treat as 32-bit
Node n;
n.kind = NodeKind::Pointer32;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.collapsed = true;
tree.addNode(n);
return;
}
// Direct base type
Node n;
n.kind = mapPrimitiveType(typeIndex);
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
return;
}
const auto* rec = tt->get(typeIndex);
if (!rec) {
Node n;
n.kind = NodeKind::Hex32;
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
return;
}
switch (rec->header.kind) {
case TRK::LF_MODIFIER:
importMemberType(rec->data.LF_MODIFIER.type, offset, name, parentId);
break;
case TRK::LF_POINTER: {
uint32_t ptrSize = rec->data.LF_POINTER.attr.size;
uint32_t pointee = rec->data.LF_POINTER.utype;
// Unwrap modifier on pointee
uint32_t realPointee = unwrapModifier(pointee);
Node n;
n.kind = (ptrSize <= 4) ? NodeKind::Pointer32 : NodeKind::Pointer64;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.collapsed = true;
// Check if pointee is a UDT
if (realPointee >= tt->firstIndex()) {
const auto* pointeeRec = tt->get(realPointee);
if (pointeeRec) {
if (pointeeRec->header.kind == TRK::LF_STRUCTURE ||
pointeeRec->header.kind == TRK::LF_CLASS ||
pointeeRec->header.kind == TRK::LF_UNION) {
// If this is a forward ref, search for the definition
uint32_t defIndex = realPointee;
bool isFwd = false;
if (pointeeRec->header.kind == TRK::LF_UNION)
isFwd = pointeeRec->data.LF_UNION.property.fwdref;
else
isFwd = pointeeRec->data.LF_CLASS.property.fwdref;
if (isFwd) {
// Need to find the non-fwdref definition by name
const char* typeName = nullptr;
if (pointeeRec->header.kind == TRK::LF_UNION)
typeName = leafName(pointeeRec->data.LF_UNION.data, unionLeafKind(pointeeRec->data.LF_UNION.data));
else
typeName = leafName(pointeeRec->data.LF_CLASS.data,
pointeeRec->data.LF_CLASS.lfEasy.kind);
if (typeName) {
// Linear scan for the definition (cached after first import)
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
const auto* candidate = tt->get(ti);
if (!candidate) continue;
if (candidate->header.kind != pointeeRec->header.kind) continue;
bool candidateFwd;
const char* candidateName;
if (candidate->header.kind == TRK::LF_UNION) {
candidateFwd = candidate->data.LF_UNION.property.fwdref;
candidateName = leafName(candidate->data.LF_UNION.data, unionLeafKind(candidate->data.LF_UNION.data));
} else {
candidateFwd = candidate->data.LF_CLASS.property.fwdref;
candidateName = leafName(candidate->data.LF_CLASS.data,
candidate->data.LF_CLASS.lfEasy.kind);
}
if (!candidateFwd && candidateName && strcmp(candidateName, typeName) == 0) {
defIndex = ti;
break;
}
}
}
}
n.refId = importUDT(defIndex);
} else if (pointeeRec->header.kind == TRK::LF_PROCEDURE ||
pointeeRec->header.kind == TRK::LF_MFUNCTION) {
n.kind = (ptrSize <= 4) ? NodeKind::FuncPtr32 : NodeKind::FuncPtr64;
}
}
}
tree.addNode(n);
break;
}
case TRK::LF_STRUCTURE:
case TRK::LF_CLASS:
case TRK::LF_UNION: {
// Embedded struct/union
uint32_t defIndex = typeIndex;
// Handle forward reference
bool isFwd = false;
if (rec->header.kind == TRK::LF_UNION)
isFwd = rec->data.LF_UNION.property.fwdref;
else
isFwd = rec->data.LF_CLASS.property.fwdref;
if (isFwd) {
const char* typeName = nullptr;
if (rec->header.kind == TRK::LF_UNION)
typeName = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
else
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
if (typeName) {
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
const auto* candidate = tt->get(ti);
if (!candidate) continue;
if (candidate->header.kind != rec->header.kind) continue;
bool candidateFwd;
const char* candidateName;
if (candidate->header.kind == TRK::LF_UNION) {
candidateFwd = candidate->data.LF_UNION.property.fwdref;
candidateName = leafName(candidate->data.LF_UNION.data, unionLeafKind(candidate->data.LF_UNION.data));
} else {
candidateFwd = candidate->data.LF_CLASS.property.fwdref;
candidateName = leafName(candidate->data.LF_CLASS.data,
candidate->data.LF_CLASS.lfEasy.kind);
}
if (!candidateFwd && candidateName && strcmp(candidateName, typeName) == 0) {
defIndex = ti;
break;
}
}
}
}
uint64_t refId = importUDT(defIndex);
const char* typeName = nullptr;
bool isUnion = (rec->header.kind == TRK::LF_UNION);
if (isUnion)
typeName = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
else
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
Node n;
n.kind = NodeKind::Struct;
n.name = name;
n.structTypeName = typeName ? QString::fromUtf8(typeName) : QString();
n.classKeyword = isUnion ? QStringLiteral("union") : QStringLiteral("struct");
n.parentId = parentId;
n.offset = offset;
n.refId = refId;
n.collapsed = true;
tree.addNode(n);
break;
}
case TRK::LF_ARRAY: {
uint32_t elemType = rec->data.LF_ARRAY.elemtype;
uint64_t totalSize = leafValue(rec->data.LF_ARRAY.data,
*reinterpret_cast<const TRK*>(rec->data.LF_ARRAY.data));
// Get element size
uint64_t elemSize = 0;
uint32_t realElemType = unwrapModifier(elemType);
if (realElemType < tt->firstIndex()) {
NodeKind ek = mapPrimitiveType(realElemType);
elemSize = sizeForKind(ek);
} else {
const auto* elemRec = tt->get(realElemType);
if (elemRec) {
if (elemRec->header.kind == TRK::LF_STRUCTURE || elemRec->header.kind == TRK::LF_CLASS) {
const char* sizeData = elemRec->data.LF_CLASS.data;
elemSize = leafValue(sizeData, elemRec->data.LF_CLASS.lfEasy.kind);
} else if (elemRec->header.kind == TRK::LF_UNION) {
const char* sizeData = elemRec->data.LF_UNION.data;
elemSize = leafValue(sizeData, *reinterpret_cast<const TRK*>(sizeData));
} else if (elemRec->header.kind == TRK::LF_POINTER) {
elemSize = elemRec->data.LF_POINTER.attr.size;
} else if (elemRec->header.kind == TRK::LF_ENUM) {
// Size of enum's underlying type
uint32_t ut = elemRec->data.LF_ENUM.utype;
if (ut < tt->firstIndex()) {
NodeKind ek = mapPrimitiveType(ut);
elemSize = sizeForKind(ek);
} else {
elemSize = 4;
}
} else if (elemRec->header.kind == TRK::LF_ARRAY) {
// Nested array — get total size
elemSize = leafValue(elemRec->data.LF_ARRAY.data,
*reinterpret_cast<const TRK*>(elemRec->data.LF_ARRAY.data));
}
}
}
int count = (elemSize > 0) ? static_cast<int>(totalSize / elemSize) : 1;
Node n;
n.kind = NodeKind::Array;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.arrayLen = count;
// Determine element kind
if (realElemType < tt->firstIndex()) {
n.elementKind = mapPrimitiveType(realElemType);
} else {
const auto* elemRec = tt->get(realElemType);
if (elemRec) {
if (elemRec->header.kind == TRK::LF_STRUCTURE ||
elemRec->header.kind == TRK::LF_CLASS ||
elemRec->header.kind == TRK::LF_UNION) {
n.elementKind = NodeKind::Struct;
n.refId = importUDT(realElemType);
const char* tn = nullptr;
if (elemRec->header.kind == TRK::LF_UNION)
tn = leafName(elemRec->data.LF_UNION.data, unionLeafKind(elemRec->data.LF_UNION.data));
else
tn = leafName(elemRec->data.LF_CLASS.data, elemRec->data.LF_CLASS.lfEasy.kind);
if (tn) n.structTypeName = QString::fromUtf8(tn);
} else if (elemRec->header.kind == TRK::LF_POINTER) {
uint32_t sz = elemRec->data.LF_POINTER.attr.size;
n.elementKind = (sz <= 4) ? NodeKind::Pointer32 : NodeKind::Pointer64;
} else {
n.elementKind = hexForSize(elemSize);
}
}
}
tree.addNode(n);
break;
}
case TRK::LF_ENUM: {
// Map enum to its underlying integer type
uint32_t utype = rec->data.LF_ENUM.utype;
Node n;
if (utype < tt->firstIndex()) {
n.kind = mapPrimitiveType(utype);
} else {
n.kind = NodeKind::UInt32; // fallback
}
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
break;
}
case TRK::LF_PROCEDURE:
case TRK::LF_MFUNCTION: {
Node n;
n.kind = NodeKind::Hex64;
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
break;
}
case TRK::LF_BITFIELD: {
uint32_t underlying = rec->data.LF_BITFIELD.type;
uint64_t slotSize = 4;
if (underlying < tt->firstIndex()) {
NodeKind k = mapPrimitiveType(underlying);
slotSize = sizeForKind(k);
}
Node n;
n.kind = hexForSize(slotSize);
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
break;
}
default: {
// Unknown complex type — emit as Hex32
Node n;
n.kind = NodeKind::Hex32;
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
break;
}
}
}
// ── Helper: open PDB and build type table ──
struct PdbFile {
MappedFile mapped;
PDB::RawFile* rawFile = nullptr;
PDB::TPIStream* tpiStream = nullptr;
TypeTable* typeTable = nullptr;
~PdbFile() {
delete typeTable;
delete tpiStream;
delete rawFile;
}
bool open(const QString& pdbPath, QString* errorMsg) {
auto setErr = [&](const QString& msg) { if (errorMsg) *errorMsg = msg; };
if (!QFile::exists(pdbPath)) {
setErr(QStringLiteral("PDB file not found: ") + pdbPath);
return false;
}
if (!mapped.open(pdbPath)) {
setErr(QStringLiteral("Failed to memory-map PDB file: ") + pdbPath);
return false;
}
if (PDB::ValidateFile(mapped.base, mapped.size) != PDB::ErrorCode::Success) {
setErr(QStringLiteral("Invalid PDB file: ") + pdbPath);
return false;
}
rawFile = new PDB::RawFile(PDB::CreateRawFile(mapped.base));
if (PDB::HasValidTPIStream(*rawFile) != PDB::ErrorCode::Success) {
setErr(QStringLiteral("PDB has no valid TPI stream: ") + pdbPath);
return false;
}
tpiStream = new PDB::TPIStream(PDB::CreateTPIStream(*rawFile));
typeTable = new TypeTable(*tpiStream);
return true;
}
};
// ── Public API: enumeratePdbTypes ──
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg) {
PdbFile pdb;
if (!pdb.open(pdbPath, errorMsg)) return {};
const TypeTable& tt = *pdb.typeTable;
QVector<PdbTypeInfo> result;
for (uint32_t ti = tt.firstIndex(); ti < tt.lastIndex(); ti++) {
const auto* rec = tt.get(ti);
if (!rec) continue;
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
rec->header.kind == TRK::LF_CLASS ||
rec->header.kind == TRK::LF_UNION);
if (!isUDT) continue;
const char* name = nullptr;
uint16_t fieldCount = 0;
bool isUnion = false;
uint64_t size = 0;
if (rec->header.kind == TRK::LF_UNION) {
if (rec->data.LF_UNION.property.fwdref) continue;
isUnion = true;
fieldCount = rec->data.LF_UNION.count;
const char* sizeData = rec->data.LF_UNION.data;
TRK sizeKind = *reinterpret_cast<const TRK*>(sizeData);
size = leafValue(sizeData, sizeKind);
name = leafName(sizeData, sizeKind);
} else {
if (rec->data.LF_CLASS.property.fwdref) continue;
fieldCount = rec->data.LF_CLASS.count;
const char* sizeData = rec->data.LF_CLASS.data;
size = leafValue(sizeData, rec->data.LF_CLASS.lfEasy.kind);
name = leafName(sizeData, rec->data.LF_CLASS.lfEasy.kind);
}
if (!name || name[0] == '\0') continue;
// Skip anonymous types with compiler-generated names
if (name[0] == '<') continue;
PdbTypeInfo info;
info.typeIndex = ti;
info.name = QString::fromUtf8(name);
info.size = size;
info.childCount = fieldCount;
info.isUnion = isUnion;
result.append(info);
}
return result;
}
// ── Public API: importPdbSelected ──
NodeTree importPdbSelected(const QString& pdbPath,
const QVector<uint32_t>& typeIndices,
QString* errorMsg,
ProgressCb progressCb) {
PdbFile pdb;
if (!pdb.open(pdbPath, errorMsg)) return {};
PdbCtx ctx;
ctx.tt = pdb.typeTable;
int total = typeIndices.size();
for (int i = 0; i < total; i++) {
ctx.importUDT(typeIndices[i]);
if (progressCb && !progressCb(i + 1, total)) {
if (errorMsg) *errorMsg = QStringLiteral("Import cancelled");
return ctx.tree; // return partial result
}
}
if (ctx.tree.nodes.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("No types imported");
}
return ctx.tree;
}
// ── Public API: importPdb (legacy) ──
NodeTree importPdb(const QString& pdbPath, const QString& structFilter, QString* errorMsg) {
PdbFile pdb;
if (!pdb.open(pdbPath, errorMsg)) return {};
const TypeTable& tt = *pdb.typeTable;
PdbCtx ctx;
ctx.tt = &tt;
for (uint32_t ti = tt.firstIndex(); ti < tt.lastIndex(); ti++) {
const auto* rec = tt.get(ti);
if (!rec) continue;
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
rec->header.kind == TRK::LF_CLASS ||
rec->header.kind == TRK::LF_UNION);
if (!isUDT) continue;
bool fwdref = false;
const char* name = nullptr;
if (rec->header.kind == TRK::LF_UNION) {
fwdref = rec->data.LF_UNION.property.fwdref;
name = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
} else {
fwdref = rec->data.LF_CLASS.property.fwdref;
name = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
}
if (fwdref) continue;
if (!name) continue;
if (!structFilter.isEmpty()) {
if (QString::fromUtf8(name) != structFilter) continue;
}
ctx.importUDT(ti);
// If filtering to a single struct, stop after finding it
if (!structFilter.isEmpty()) break;
}
if (ctx.tree.nodes.isEmpty()) {
if (!structFilter.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("Type '") + structFilter +
QStringLiteral("' not found in PDB");
} else {
if (errorMsg) *errorMsg = QStringLiteral("No types found in PDB");
}
}
return ctx.tree;
}
} // namespace rcx
#else // !_WIN32
namespace rcx {
QVector<PdbTypeInfo> enumeratePdbTypes(const QString&, QString* errorMsg) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};
}
NodeTree importPdbSelected(const QString&, const QVector<uint32_t>&,
QString* errorMsg, ProgressCb) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};
}
NodeTree importPdb(const QString&, const QString&, QString* errorMsg) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};
}
} // namespace rcx
#endif

34
src/imports/import_pdb.h Normal file
View File

@@ -0,0 +1,34 @@
#pragma once
#include "core.h"
#include <QVector>
#include <functional>
namespace rcx {
struct PdbTypeInfo {
uint32_t typeIndex; // TPI type index
QString name; // struct/class/union name
uint64_t size; // sizeof in bytes
int childCount; // direct member count
bool isUnion; // union vs struct/class
};
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath,
QString* errorMsg = nullptr);
// Phase 2: Import selected types with full recursive child types.
// progressCb is called with (current, total) for each top-level type;
// return false from the callback to cancel the import.
using ProgressCb = std::function<bool(int current, int total)>;
NodeTree importPdbSelected(const QString& pdbPath,
const QVector<uint32_t>& typeIndices,
QString* errorMsg = nullptr,
ProgressCb progressCb = {});
// Legacy single-call API: import one struct by name (or all if filter empty).
NodeTree importPdb(const QString& pdbPath,
const QString& structFilter = {},
QString* errorMsg = nullptr);
} // namespace rcx

View File

@@ -0,0 +1,184 @@
#include "import_pdb_dialog.h"
#include "import_pdb.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QCheckBox>
#include <QListWidget>
#include <QLabel>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QFileDialog>
#include <QMessageBox>
#include <QApplication>
namespace rcx {
PdbImportDialog::PdbImportDialog(QWidget* parent)
: QDialog(parent)
{
setWindowTitle("Import from PDB");
resize(520, 480);
auto* layout = new QVBoxLayout(this);
// PDB path row
auto* pathRow = new QHBoxLayout;
pathRow->addWidget(new QLabel("PDB File:"));
m_pathEdit = new QLineEdit;
m_pathEdit->setPlaceholderText("Select a PDB file...");
pathRow->addWidget(m_pathEdit);
m_browseBtn = new QPushButton("...");
m_browseBtn->setFixedWidth(32);
pathRow->addWidget(m_browseBtn);
layout->addLayout(pathRow);
// Filter row
auto* filterRow = new QHBoxLayout;
filterRow->addWidget(new QLabel("Filter:"));
m_filterEdit = new QLineEdit;
m_filterEdit->setPlaceholderText("Type name filter...");
m_filterEdit->setEnabled(false);
filterRow->addWidget(m_filterEdit);
layout->addLayout(filterRow);
// Select all checkbox
m_selectAll = new QCheckBox("Select All");
m_selectAll->setEnabled(false);
layout->addWidget(m_selectAll);
// Type list
m_typeList = new QListWidget;
m_typeList->setEnabled(false);
layout->addWidget(m_typeList);
// Count label
m_countLabel = new QLabel("No PDB loaded");
layout->addWidget(m_countLabel);
// Buttons
m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
m_buttons->button(QDialogButtonBox::Ok)->setText("Import");
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
layout->addWidget(m_buttons);
connect(m_browseBtn, &QPushButton::clicked, this, &PdbImportDialog::browsePdb);
connect(m_pathEdit, &QLineEdit::returnPressed, this, &PdbImportDialog::loadPdb);
connect(m_filterEdit, &QLineEdit::textChanged, this, &PdbImportDialog::filterChanged);
connect(m_selectAll, &QCheckBox::toggled, this, &PdbImportDialog::selectAllToggled);
connect(m_typeList, &QListWidget::itemChanged, this, &PdbImportDialog::updateSelectionCount);
connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
QString PdbImportDialog::pdbPath() const {
return m_pathEdit->text();
}
QVector<uint32_t> PdbImportDialog::selectedTypeIndices() const {
QVector<uint32_t> result;
for (int i = 0; i < m_typeList->count(); i++) {
auto* item = m_typeList->item(i);
if (item->checkState() == Qt::Checked) {
uint32_t typeIndex = item->data(Qt::UserRole).toUInt();
result.append(typeIndex);
}
}
return result;
}
void PdbImportDialog::browsePdb() {
QString path = QFileDialog::getOpenFileName(this,
"Select PDB File", {},
"PDB Files (*.pdb);;All Files (*)");
if (path.isEmpty()) return;
m_pathEdit->setText(path);
loadPdb();
}
void PdbImportDialog::loadPdb() {
QString path = m_pathEdit->text();
if (path.isEmpty()) return;
m_typeList->clear();
m_allTypes.clear();
m_countLabel->setText("Loading...");
m_typeList->setEnabled(false);
m_filterEdit->setEnabled(false);
m_selectAll->setEnabled(false);
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
QApplication::processEvents();
QString error;
QVector<PdbTypeInfo> types = enumeratePdbTypes(path, &error);
if (types.isEmpty()) {
m_countLabel->setText(error.isEmpty() ? "No types found" : error);
return;
}
m_allTypes.reserve(types.size());
for (const auto& t : types) {
TypeItem item;
item.typeIndex = t.typeIndex;
item.name = t.name;
item.childCount = t.childCount;
item.isUnion = t.isUnion;
m_allTypes.append(item);
}
// Sort by name
std::sort(m_allTypes.begin(), m_allTypes.end(),
[](const TypeItem& a, const TypeItem& b) { return a.name < b.name; });
m_filterEdit->setEnabled(true);
m_selectAll->setEnabled(true);
m_typeList->setEnabled(true);
populateList();
}
void PdbImportDialog::populateList() {
m_typeList->blockSignals(true);
m_typeList->clear();
QString filter = m_filterEdit->text();
bool selectAll = m_selectAll->isChecked();
for (const auto& t : m_allTypes) {
if (!filter.isEmpty() && !t.name.contains(filter, Qt::CaseInsensitive))
continue;
QString label = QStringLiteral("%1 (%2 fields)")
.arg(t.name).arg(t.childCount);
auto* item = new QListWidgetItem(label, m_typeList);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(selectAll ? Qt::Checked : Qt::Unchecked);
item->setData(Qt::UserRole, t.typeIndex);
}
m_typeList->blockSignals(false);
updateSelectionCount();
}
void PdbImportDialog::filterChanged(const QString&) {
populateList();
}
void PdbImportDialog::selectAllToggled(bool) {
populateList();
}
void PdbImportDialog::updateSelectionCount() {
int checked = 0;
int total = m_typeList->count();
for (int i = 0; i < total; i++) {
if (m_typeList->item(i)->checkState() == Qt::Checked)
checked++;
}
m_countLabel->setText(QStringLiteral("%1 of %2 types selected")
.arg(checked).arg(m_allTypes.size()));
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(checked > 0);
}
} // namespace rcx

View File

@@ -0,0 +1,53 @@
#pragma once
#include <QDialog>
#include <QVector>
#include <cstdint>
class QLineEdit;
class QCheckBox;
class QListWidget;
class QLabel;
class QDialogButtonBox;
class QPushButton;
namespace rcx {
struct PdbTypeInfo;
class PdbImportDialog : public QDialog {
Q_OBJECT
public:
explicit PdbImportDialog(QWidget* parent = nullptr);
QString pdbPath() const;
QVector<uint32_t> selectedTypeIndices() const;
private slots:
void browsePdb();
void loadPdb();
void filterChanged(const QString& text);
void selectAllToggled(bool checked);
void updateSelectionCount();
private:
QLineEdit* m_pathEdit;
QPushButton* m_browseBtn;
QLineEdit* m_filterEdit;
QCheckBox* m_selectAll;
QListWidget* m_typeList;
QLabel* m_countLabel;
QDialogButtonBox* m_buttons;
struct TypeItem {
uint32_t typeIndex;
QString name;
int childCount;
bool isUnion;
};
QVector<TypeItem> m_allTypes;
void populateList();
};
} // namespace rcx

View File

@@ -0,0 +1,388 @@
#include "import_reclass_xml.h"
#include <QFile>
#include <QXmlStreamReader>
#include <QHash>
#include <QVector>
#include <QDebug>
namespace rcx {
// ── Version-specific type maps ──
// Maps XML Type attribute (integer) → NodeKind.
// Entries with no rcx equivalent use Hex8 as fallback.
enum class XmlVersion { V2013, V2016 };
// 2016 / ReClassEx / MemeClsEx type map (35 entries, index = XML Type value)
static const struct { int xmlType; NodeKind kind; } kTypeMap2016[] = {
// 0: null (unused)
{ 1, NodeKind::Struct }, // ClassInstance
// 2,3: null
{ 4, NodeKind::Hex32 },
{ 5, NodeKind::Hex64 },
{ 6, NodeKind::Hex16 },
{ 7, NodeKind::Hex8 },
{ 8, NodeKind::Pointer64 }, // ClassPointer
{ 9, NodeKind::Int64 },
{ 10, NodeKind::Int32 },
{ 11, NodeKind::Int16 },
{ 12, NodeKind::Int8 },
{ 13, NodeKind::Float },
{ 14, NodeKind::Double },
{ 15, NodeKind::UInt32 },
{ 16, NodeKind::UInt16 },
{ 17, NodeKind::UInt8 },
{ 18, NodeKind::UTF8 }, // UTF8Text
{ 19, NodeKind::UTF16 }, // UTF16Text
{ 20, NodeKind::Pointer64 }, // FunctionPtr
{ 21, NodeKind::Hex8 }, // Custom (expanded by Size)
{ 22, NodeKind::Vec2 },
{ 23, NodeKind::Vec3 },
{ 24, NodeKind::Vec4 },
{ 25, NodeKind::Mat4x4 },
{ 26, NodeKind::Pointer64 }, // VTable
{ 27, NodeKind::Array }, // ClassInstanceArray
// 28: null (used for Class elements, not nodes)
{ 29, NodeKind::Pointer64 }, // UTF8TextPtr
{ 30, NodeKind::Pointer64 }, // UTF16TextPtr
// 31: BitField → UInt8 fallback
{ 31, NodeKind::UInt8 },
{ 32, NodeKind::UInt64 },
{ 33, NodeKind::Pointer64 }, // Function
};
// 2013 / ReClass 2011 type map (31 entries)
static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
{ 1, NodeKind::Struct }, // ClassInstance
{ 4, NodeKind::Hex32 },
{ 5, NodeKind::Hex16 },
{ 6, NodeKind::Hex8 },
{ 7, NodeKind::Pointer64 }, // ClassPointer
{ 8, NodeKind::Int32 },
{ 9, NodeKind::Int16 },
{ 10, NodeKind::Int8 },
{ 11, NodeKind::Float },
{ 12, NodeKind::UInt32 },
{ 13, NodeKind::UInt16 },
{ 14, NodeKind::UInt8 },
{ 15, NodeKind::UTF8 }, // UTF8Text
{ 16, NodeKind::Pointer64 }, // FunctionPtr
{ 17, NodeKind::Hex8 }, // Custom
{ 18, NodeKind::Vec2 },
{ 19, NodeKind::Vec3 },
{ 20, NodeKind::Vec4 },
{ 21, NodeKind::Mat4x4 },
{ 22, NodeKind::Pointer64 }, // VTable
{ 23, NodeKind::Array }, // ClassInstanceArray
{ 27, NodeKind::Int64 },
{ 28, NodeKind::Double },
{ 29, NodeKind::UTF16 }, // UTF16Text
{ 30, NodeKind::Array }, // ClassPointerArray
};
static NodeKind lookupKind(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) {
for (const auto& e : kTypeMap2016)
if (e.xmlType == xmlType) return e.kind;
} else {
for (const auto& e : kTypeMap2013)
if (e.xmlType == xmlType) return e.kind;
}
return NodeKind::Hex8; // fallback
}
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
static bool isPointerType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016)
return xmlType == 8 || xmlType == 20 || xmlType == 26 || xmlType == 29 || xmlType == 30 || xmlType == 33;
else
return xmlType == 7 || xmlType == 16 || xmlType == 22;
}
// Is this XML type a ClassInstance (embedded struct)?
static bool isClassInstanceType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 1;
else return xmlType == 1;
}
// Is this XML type a ClassInstanceArray?
static bool isClassInstanceArrayType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 27;
else return xmlType == 23 || xmlType == 30;
}
// Is this XML type a text node?
static bool isTextType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 18 || xmlType == 19;
else return xmlType == 15 || xmlType == 29;
}
// Is this XML type a UTF16 text node?
static bool isUtf16TextType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 19;
else return xmlType == 29;
}
// Is this XML type a Custom node (expanded to hex)?
static bool isCustomType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 21;
else return xmlType == 17;
}
// Deferred pointer resolution entry
struct PendingRef {
uint64_t nodeId;
QString className;
};
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
qDebug() << "[ImportXML] Opening file:" << filePath;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "[ImportXML] ERROR: Cannot open file";
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file: ") + filePath;
return {};
}
qDebug() << "[ImportXML] File size:" << file.size() << "bytes";
QXmlStreamReader xml(&file);
XmlVersion version = XmlVersion::V2016; // default to 2016 (most common)
NodeTree tree;
tree.baseAddress = 0x00400000;
// Class name → struct node ID (for pointer resolution)
QHash<QString, uint64_t> classIds;
// Deferred pointer refs to resolve after all classes are parsed
QVector<PendingRef> pendingRefs;
// Detect version from first comment
bool versionDetected = false;
while (!xml.atEnd()) {
xml.readNext();
// Detect version from XML comments
if (!versionDetected && xml.isComment()) {
QString comment = xml.text().toString().trimmed();
if (comment.contains(QStringLiteral("ReClassEx"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("MemeClsEx"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2016"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2015"), Qt::CaseInsensitive)) {
version = XmlVersion::V2016;
} else if (comment.contains(QStringLiteral("2013"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2011"), Qt::CaseInsensitive)) {
version = XmlVersion::V2013;
}
// else keep default V2016
versionDetected = true;
qDebug() << "[ImportXML] Detected version:" << (version == XmlVersion::V2016 ? "V2016" : "V2013");
}
if (!xml.isStartElement()) continue;
if (xml.name() == QStringLiteral("Class")) {
// Parse a class element into a root Struct node
QString className = xml.attributes().value(QStringLiteral("Name")).toString();
QString strOffset = xml.attributes().value(QStringLiteral("strOffset")).toString();
// Create root struct node (collapsed by default for large files)
Node structNode;
structNode.kind = NodeKind::Struct;
structNode.name = className;
structNode.structTypeName = className;
structNode.parentId = 0; // root level
structNode.offset = 0;
structNode.collapsed = true;
int structIdx = tree.addNode(structNode);
uint64_t structId = tree.nodes[structIdx].id;
classIds[className] = structId;
qDebug() << "[ImportXML] Class:" << className << "id:" << structId;
// Parse child Node elements
int childOffset = 0;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement() && xml.name() == QStringLiteral("Class"))
break;
if (!xml.isStartElement() || xml.name() != QStringLiteral("Node"))
continue;
int xmlType = xml.attributes().value(QStringLiteral("Type")).toInt();
QString nodeName = xml.attributes().value(QStringLiteral("Name")).toString();
int nodeSize = xml.attributes().value(QStringLiteral("Size")).toInt();
QString ptrClass = xml.attributes().value(QStringLiteral("Pointer")).toString();
QString instClass = xml.attributes().value(QStringLiteral("Instance")).toString();
qDebug() << "[ImportXML] Node:" << nodeName << "type:" << xmlType
<< "size:" << nodeSize << "ptr:" << ptrClass << "inst:" << instClass;
// Handle Custom type: expand to appropriate hex nodes
if (isCustomType(xmlType, version) && nodeSize > 0) {
// Pick best-fit hex kind
NodeKind hexKind;
int hexSize;
if (nodeSize >= 8 && nodeSize % 8 == 0) {
hexKind = NodeKind::Hex64; hexSize = 8;
} else if (nodeSize >= 4 && nodeSize % 4 == 0) {
hexKind = NodeKind::Hex32; hexSize = 4;
} else if (nodeSize >= 2 && nodeSize % 2 == 0) {
hexKind = NodeKind::Hex16; hexSize = 2;
} else {
hexKind = NodeKind::Hex8; hexSize = 1;
}
int count = nodeSize / hexSize;
for (int i = 0; i < count; i++) {
Node n;
n.kind = hexKind;
n.name = (count == 1) ? nodeName : QString();
n.parentId = structId;
n.offset = childOffset;
tree.addNode(n);
childOffset += hexSize;
}
continue;
}
NodeKind kind = lookupKind(xmlType, version);
// Handle ClassInstanceArray: read child <Array> element
if (isClassInstanceArrayType(xmlType, version)) {
qDebug() << "[ImportXML] -> ClassInstanceArray";
int total = xml.attributes().value(QStringLiteral("Total")).toInt();
if (total <= 0)
total = xml.attributes().value(QStringLiteral("Count")).toInt();
if (total <= 0) total = 1;
// Read child <Array> element for class name
QString arrayClassName;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement() && xml.name() == QStringLiteral("Node"))
break;
if (xml.isStartElement() && xml.name() == QStringLiteral("Array")) {
arrayClassName = xml.attributes().value(QStringLiteral("Name")).toString();
int arrayTotal = xml.attributes().value(QStringLiteral("Total")).toInt();
if (arrayTotal <= 0)
arrayTotal = xml.attributes().value(QStringLiteral("Count")).toInt();
if (arrayTotal > 0) total = arrayTotal;
}
}
// Create an Array node wrapping Struct elements
Node arrNode;
arrNode.kind = NodeKind::Array;
arrNode.name = nodeName;
arrNode.parentId = structId;
arrNode.offset = childOffset;
arrNode.arrayLen = total;
arrNode.elementKind = NodeKind::Struct;
if (!arrayClassName.isEmpty())
arrNode.structTypeName = arrayClassName;
int arrIdx = tree.addNode(arrNode);
uint64_t arrId = tree.nodes[arrIdx].id;
// Defer ref resolution if array references a class
if (!arrayClassName.isEmpty()) {
pendingRefs.append({arrId, arrayClassName});
}
childOffset += nodeSize > 0 ? nodeSize : 0;
continue;
}
Node n;
n.kind = kind;
n.name = nodeName;
n.parentId = structId;
n.offset = childOffset;
// Handle text nodes
if (isTextType(xmlType, version)) {
if (isUtf16TextType(xmlType, version))
n.strLen = qMax(1, nodeSize / 2);
else
n.strLen = qMax(1, nodeSize);
}
// Handle pointer types
if (isPointerType(xmlType, version) && !ptrClass.isEmpty()) {
qDebug() << "[ImportXML] -> Pointer to class:" << ptrClass;
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, ptrClass});
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
continue;
}
// Handle embedded class instance
if (isClassInstanceType(xmlType, version)) {
QString resolvedClass = instClass.isEmpty() ? ptrClass : instClass;
qDebug() << "[ImportXML] -> ClassInstance:" << resolvedClass;
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
n.structTypeName = resolvedClass;
if (!n.structTypeName.isEmpty()) {
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, n.structTypeName});
} else {
tree.addNode(n);
}
childOffset += nodeSize > 0 ? nodeSize : 0;
continue;
}
tree.addNode(n);
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
}
}
}
if (xml.hasError() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
qDebug() << "[ImportXML] XML parse error at line" << xml.lineNumber() << ":" << xml.errorString();
if (errorMsg)
*errorMsg = QStringLiteral("XML parse error at line %1: %2")
.arg(xml.lineNumber())
.arg(xml.errorString());
return {};
}
qDebug() << "[ImportXML] Parsing complete. Total nodes:" << tree.nodes.size()
<< "classes:" << classIds.size() << "pending refs:" << pendingRefs.size();
if (tree.nodes.isEmpty()) {
qDebug() << "[ImportXML] ERROR: No classes found";
if (errorMsg) *errorMsg = QStringLiteral("No classes found in file");
return {};
}
// Resolve deferred pointer/struct references
int resolved = 0, unresolved = 0;
for (const auto& ref : pendingRefs) {
int nodeIdx = tree.indexOfId(ref.nodeId);
if (nodeIdx < 0) continue;
auto it = classIds.find(ref.className);
if (it != classIds.end()) {
tree.nodes[nodeIdx].refId = it.value();
tree.invalidateIdCache();
resolved++;
} else {
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
unresolved++;
}
}
qDebug() << "[ImportXML] Refs resolved:" << resolved << "unresolved:" << unresolved;
qDebug() << "[ImportXML] Import complete. Returning tree with" << tree.nodes.size() << "nodes";
return tree;
}
} // namespace rcx

View File

@@ -0,0 +1,11 @@
#pragma once
#include "core.h"
namespace rcx {
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr);
} // namespace rcx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
#pragma once
#include "core.h"
namespace rcx {
// Import C/C++ struct definitions from source code into a NodeTree.
// Supports two modes (auto-detected):
// 1. With comment offsets (// 0xNN) - trusts the offset values
// 2. Without comment offsets - computes offsets from type sizes
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr);
} // namespace rcx