mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
204
src/imports/export_reclass_xml.cpp
Normal file
204
src/imports/export_reclass_xml.cpp
Normal 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
|
||||
10
src/imports/export_reclass_xml.h
Normal file
10
src/imports/export_reclass_xml.h
Normal 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
971
src/imports/import_pdb.cpp
Normal 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
34
src/imports/import_pdb.h
Normal 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
|
||||
184
src/imports/import_pdb_dialog.cpp
Normal file
184
src/imports/import_pdb_dialog.cpp
Normal 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
|
||||
53
src/imports/import_pdb_dialog.h
Normal file
53
src/imports/import_pdb_dialog.h
Normal 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
|
||||
388
src/imports/import_reclass_xml.cpp
Normal file
388
src/imports/import_reclass_xml.cpp
Normal 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
|
||||
11
src/imports/import_reclass_xml.h
Normal file
11
src/imports/import_reclass_xml.h
Normal 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
|
||||
1066
src/imports/import_source.cpp
Normal file
1066
src/imports/import_source.cpp
Normal file
File diff suppressed because it is too large
Load Diff
13
src/imports/import_source.h
Normal file
13
src/imports/import_source.h
Normal 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
|
||||
Reference in New Issue
Block a user