feat: enum support, workspace styling, EPROCESS/MMPFN test data

- Import enums from C/C++ source and PDB with name/value members
- Compose/format/generate enum definitions properly
- Workspace dock: rename to Project, theme-based titlebar and selection
- Add comprehensive EPROCESS.rcx (325 nodes) and MMPFN.rcx (65 nodes)
This commit is contained in:
IChooseYou
2026-02-23 16:01:35 -07:00
parent 078a6028f0
commit ff928df685
15 changed files with 2660 additions and 306 deletions

View File

@@ -22,6 +22,7 @@ struct ComposeState {
int nameW = kColName; // global name column width (fallback)
int offsetHexDigits = 8; // hex digit tier for offset margin
bool baseEmitted = false; // only first root struct shows base address
bool compactColumns = false; // compact column mode: cap type width, overflow long types
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
// Precomputed for O(1) lookups
@@ -132,6 +133,11 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
}
}
// Detect type overflow in compact mode (for effectiveTypeW)
QString rawType = ptrTypeOverride.isEmpty() ? fmt::typeNameRaw(node.kind) : ptrTypeOverride;
bool typeOverflow = state.compactColumns && rawType.size() > typeW;
int lineTypeW = typeOverflow ? rawType.size() : typeW;
for (int sub = 0; sub < numLines; sub++) {
bool isCont = (sub > 0);
@@ -148,7 +154,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
lm.foldLevel = computeFoldLevel(depth, false);
lm.effectiveTypeW = typeW;
lm.effectiveTypeW = lineTypeW;
lm.effectiveNameW = nameW;
lm.pointerTargetName = ptrTargetName;
@@ -158,7 +164,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
}
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
state.compactColumns);
state.emitLine(lineText, lm);
}
}
@@ -248,8 +255,6 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.foldCollapsed = node.collapsed;
lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = (1u << M_STRUCT_BG);
lm.effectiveTypeW = typeW;
lm.effectiveNameW = nameW;
QString headerText;
if (node.kind == NodeKind::Array) {
@@ -260,15 +265,68 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.arrayCount = node.arrayLen;
QString elemStructName = (node.elementKind == NodeKind::Struct)
? resolvePointerTarget(tree, node.refId) : QString();
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName);
QString rawType = fmt::arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
bool overflow = state.compactColumns && rawType.size() > typeW;
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
lm.effectiveNameW = nameW;
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName, state.compactColumns);
} else {
// All structs (root and nested) use the same header format
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW);
QString rawType = fmt::structTypeName(node);
bool overflow = state.compactColumns && rawType.size() > typeW;
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
lm.effectiveNameW = nameW;
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
}
state.emitLine(headerText, lm);
}
if (!node.collapsed || isArrayChild || isRootHeader) {
// Enum with members: render name = value lines instead of offset-based fields
if (node.resolvedClassKeyword() == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
int childDepth = depth + 1;
int maxNameLen = 4;
for (const auto& m : node.enumMembers)
maxNameLen = qMax(maxNameLen, (int)m.first.size());
for (int mi = 0; mi < node.enumMembers.size(); mi++) {
const auto& m = node.enumMembers[mi];
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.subLine = mi;
lm.depth = childDepth;
lm.lineKind = LineKind::Field;
lm.nodeKind = NodeKind::UInt32;
lm.foldLevel = computeFoldLevel(childDepth, false);
lm.markerMask = 0;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), lm);
}
// Footer
if (!isArrayChild) {
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Footer;
lm.nodeKind = node.kind;
lm.isRootHeader = isRootHeader;
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm);
}
state.visiting.remove(node.id);
return;
}
QVector<int> children = state.childMap.value(node.id);
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
@@ -309,11 +367,13 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
lm.foldLevel = computeFoldLevel(childDepth, false);
lm.effectiveTypeW = eTW;
bool elemOverflow = state.compactColumns && elemTypeStr.size() > eTW;
lm.effectiveTypeW = elemOverflow ? elemTypeStr.size() : eTW;
lm.effectiveNameW = eNW;
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
{}, eTW, eNW, elemTypeStr), lm);
{}, eTW, eNW, elemTypeStr,
state.compactColumns), lm);
}
}
@@ -351,6 +411,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
if (state.visiting.contains(child.id)) {
int typeW = state.effectiveTypeW(refScopeId);
int nameW = state.effectiveNameW(refScopeId);
QString rawType = fmt::structTypeName(child);
bool overflow = state.compactColumns && rawType.size() > typeW;
LineMeta lm;
lm.nodeIdx = nodeIdx; // parent struct — materialize target
lm.nodeId = child.id;
@@ -366,10 +428,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.foldCollapsed = true;
lm.foldLevel = computeFoldLevel(childDepth, true);
lm.markerMask = (1u << M_STRUCT_BG) | (1u << M_CYCLE);
lm.effectiveTypeW = typeW;
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
lm.effectiveNameW = nameW;
state.emitLine(fmt::fmtStructHeader(child, childDepth,
/*collapsed=*/true, typeW, nameW), lm);
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm);
continue;
}
composeNode(state, tree, prov, childIdx, childDepth,
@@ -458,12 +520,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
lm.effectiveTypeW = typeW;
bool ptrOverflow = state.compactColumns && ptrTypeOverride.size() > typeW;
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
lm.effectiveNameW = nameW;
lm.pointerTargetName = ptrTargetName;
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
prov, absAddr, ptrTypeOverride,
typeW, nameW), lm);
typeW, nameW, state.compactColumns), lm);
}
if (!effectiveCollapsed) {
@@ -558,8 +621,10 @@ void composeNode(ComposeState& state, const NodeTree& tree,
} // anonymous namespace
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId) {
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
bool compactColumns) {
ComposeState state;
state.compactColumns = compactColumns;
// Precompute parent→children map
for (int i = 0; i < tree.nodes.size(); i++)
@@ -599,11 +664,12 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
// Compute effective type column width from longest type name
// Include struct/array headers which use "struct TypeName" or "type[count]" format
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
int maxTypeLen = kMinTypeW;
for (const Node& node : tree.nodes) {
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
}
state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW);
state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap);
// Compute effective name column width from longest name
// Include struct/array names - they now use columnar layout too
@@ -647,7 +713,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
scopeMaxType = qMax(scopeMaxType, (int)longestElemType.size());
}
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW);
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, typeCap);
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
}
@@ -665,7 +731,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
rootMaxName = qMax(rootMaxName, (int)child.name.size());
}
}
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW);
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, typeCap);
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
}

View File

@@ -72,8 +72,8 @@ RcxDocument::RcxDocument(QObject* parent)
});
}
ComposeResult RcxDocument::compose(uint64_t viewRootId) const {
return rcx::compose(tree, *provider, viewRootId);
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns);
}
bool RcxDocument::save(const QString& path) {
@@ -493,9 +493,9 @@ void RcxController::refresh() {
// Compose against snapshot provider if active, otherwise real provider
if (m_snapshotProv)
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId);
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns);
else
m_lastResult = m_doc->compose(m_viewRootId);
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns);
s_composeDoc = nullptr;
@@ -805,6 +805,148 @@ void RcxController::deleteRootStruct(uint64_t structId) {
if (!m_suppressRefresh) refresh();
}
void RcxController::groupIntoUnion(const QSet<uint64_t>& nodeIds) {
if (nodeIds.size() < 2) return;
// Collect nodes and verify they share the same parent
QVector<int> indices;
uint64_t parentId = 0;
bool first = true;
for (uint64_t id : nodeIds) {
int idx = m_doc->tree.indexOfId(id);
if (idx < 0) return;
if (first) { parentId = m_doc->tree.nodes[idx].parentId; first = false; }
else if (m_doc->tree.nodes[idx].parentId != parentId) return;
indices.append(idx);
}
// Sort by offset to find the union's insertion point
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
return m_doc->tree.nodes[a].offset < m_doc->tree.nodes[b].offset;
});
int unionOffset = m_doc->tree.nodes[indices.first()].offset;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Group into union"));
// Save copies of nodes before removal (subtrees included)
struct SavedNode { Node node; QVector<Node> subtree; };
QVector<SavedNode> saved;
for (int idx : indices) {
SavedNode sn;
sn.node = m_doc->tree.nodes[idx];
auto sub = m_doc->tree.subtreeIndices(sn.node.id);
for (int si : sub)
if (si != idx) sn.subtree.append(m_doc->tree.nodes[si]);
saved.append(sn);
}
// Remove selected nodes (in reverse order to keep indices valid)
for (int i = indices.size() - 1; i >= 0; i--) {
int idx = m_doc->tree.indexOfId(saved[i].node.id);
if (idx >= 0) {
QVector<Node> subtree;
for (int si : m_doc->tree.subtreeIndices(saved[i].node.id))
subtree.append(m_doc->tree.nodes[si]);
m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{saved[i].node.id, subtree, {}}));
}
}
// Insert union node
Node unionNode;
unionNode.kind = NodeKind::Struct;
unionNode.classKeyword = QStringLiteral("union");
unionNode.parentId = parentId;
unionNode.offset = unionOffset;
unionNode.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{unionNode}));
uint64_t unionId = unionNode.id;
// Re-insert nodes as children of the union, all at offset 0
for (const auto& sn : saved) {
Node copy = sn.node;
copy.parentId = unionId;
copy.offset = 0;
copy.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{copy}));
// Re-insert subtree with updated parentId for direct children
uint64_t oldId = sn.node.id;
uint64_t newId = copy.id;
for (const auto& child : sn.subtree) {
Node cc = child;
if (cc.parentId == oldId) cc.parentId = newId;
cc.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{cc}));
}
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
}
void RcxController::dissolveUnion(uint64_t unionId) {
int ui = m_doc->tree.indexOfId(unionId);
if (ui < 0) return;
const Node& unionNode = m_doc->tree.nodes[ui];
if (unionNode.kind != NodeKind::Struct
|| unionNode.resolvedClassKeyword() != QStringLiteral("union")) return;
uint64_t parentId = unionNode.parentId;
int unionOffset = unionNode.offset;
// Collect union children
auto children = m_doc->tree.childrenOf(unionId);
struct SavedNode { Node node; QVector<Node> subtree; };
QVector<SavedNode> saved;
for (int ci : children) {
SavedNode sn;
sn.node = m_doc->tree.nodes[ci];
auto sub = m_doc->tree.subtreeIndices(sn.node.id);
for (int si : sub)
if (si != ci) sn.subtree.append(m_doc->tree.nodes[si]);
saved.append(sn);
}
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Dissolve union"));
// Remove the union (and all its children)
{
QVector<Node> subtree;
for (int si : m_doc->tree.subtreeIndices(unionId))
subtree.append(m_doc->tree.nodes[si]);
m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{unionId, subtree, {}}));
}
// Re-insert children under the union's parent, at the union's offset
for (const auto& sn : saved) {
Node copy = sn.node;
copy.parentId = parentId;
copy.offset = unionOffset + sn.node.offset;
copy.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{copy}));
uint64_t oldId = sn.node.id;
uint64_t newId = copy.id;
for (const auto& child : sn.subtree) {
Node cc = child;
if (cc.parentId == oldId) cc.parentId = newId;
cc.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{cc}));
}
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
}
void RcxController::toggleCollapse(int nodeIdx) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& node = m_doc->tree.nodes[nodeIdx];
@@ -1343,6 +1485,21 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
}
menu.addSeparator();
// Check if all selected nodes share the same parent (required for grouping)
{
bool sameParent = true;
uint64_t firstParent = 0;
bool fp = true;
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
if (idx < 0) { sameParent = false; break; }
if (fp) { firstParent = m_doc->tree.nodes[idx].parentId; fp = false; }
else if (m_doc->tree.nodes[idx].parentId != firstParent) { sameParent = false; break; }
}
if (sameParent)
menu.addAction("Group into Union", [this, ids]() { groupIntoUnion(ids); });
}
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
@@ -1551,6 +1708,26 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
}
// Dissolve Union: available on union itself or any of its children
{
uint64_t targetUnionId = 0;
if (node.kind == NodeKind::Struct
&& node.resolvedClassKeyword() == QStringLiteral("union")) {
targetUnionId = nodeId;
} else if (node.parentId != 0) {
int pi = m_doc->tree.indexOfId(node.parentId);
if (pi >= 0 && m_doc->tree.nodes[pi].kind == NodeKind::Struct
&& m_doc->tree.nodes[pi].resolvedClassKeyword() == QStringLiteral("union")) {
targetUnionId = node.parentId;
}
}
if (targetUnionId != 0) {
menu.addAction("Dissolve Union", [this, targetUnionId]() {
dissolveUnion(targetUnionId);
});
}
}
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) duplicateNode(ni);
@@ -1618,13 +1795,14 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
});
menu.addSeparator();
{
// Only add Track Value Changes here if not already added in node-specific section
if (!hasNode) {
auto* act = menu.addAction("Track Value Changes");
act->setCheckable(true);
act->setChecked(m_trackValues);
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
menu.addSeparator();
}
menu.addSeparator();
menu.addAction(icon("arrow-left.svg"), "Undo", [this]() {
m_doc->undoStack.undo();
@@ -2522,6 +2700,11 @@ void RcxController::setRefreshInterval(int ms) {
m_refreshTimer->setInterval(qMax(1, ms));
}
void RcxController::setCompactColumns(bool v) {
m_compactColumns = v;
refresh();
}
void RcxController::setupAutoRefresh() {
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
m_refreshTimer = new QTimer(this);

View File

@@ -40,7 +40,7 @@ public:
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
}
ComposeResult compose(uint64_t viewRootId = 0) const;
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false) const;
bool save(const QString& path);
bool load(const QString& path);
void loadData(const QString& binaryPath);
@@ -102,6 +102,8 @@ public:
void batchRemoveNodes(const QVector<int>& nodeIndices);
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
void deleteRootStruct(uint64_t structId);
void groupIntoUnion(const QSet<uint64_t>& nodeIds);
void dissolveUnion(uint64_t unionId);
void applyCommand(const Command& cmd, bool isUndo);
void refresh();
@@ -122,6 +124,7 @@ public:
RcxDocument* document() const { return m_doc; }
void setEditorFont(const QString& fontName);
void setRefreshInterval(int ms);
void setCompactColumns(bool v);
// MCP bridge accessors
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
@@ -154,6 +157,7 @@ private:
QSet<uint64_t> m_selIds;
int m_anchorLine = -1;
bool m_suppressRefresh = false;
bool m_compactColumns = false;
uint64_t m_viewRootId = 0;
// ── Saved sources for quick-switch ──

View File

@@ -196,6 +196,7 @@ struct Node {
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
int viewIndex = 0; // Array: current view offset (transient)
QVector<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
int byteSize() const {
@@ -229,6 +230,16 @@ struct Node {
o["elementKind"] = kindToString(elementKind);
if (ptrDepth > 0)
o["ptrDepth"] = ptrDepth;
if (!enumMembers.isEmpty()) {
QJsonArray arr;
for (const auto& m : enumMembers) {
QJsonObject em;
em["name"] = m.first;
em["value"] = QString::number(m.second);
arr.append(em);
}
o["enumMembers"] = arr;
}
return o;
}
static Node fromJson(const QJsonObject& o) {
@@ -246,6 +257,14 @@ struct Node {
n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
if (o.contains("enumMembers")) {
QJsonArray arr = o["enumMembers"].toArray();
for (const auto& v : arr) {
QJsonObject em = v.toObject();
n.enumMembers.append({em["name"].toString(),
em["value"].toString("0").toLongLong()});
}
}
return n;
}
@@ -599,6 +618,7 @@ inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uin
inline constexpr int kMaxTypeW = 128; // Maximum type column width
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
inline constexpr int kMaxNameW = 128; // Maximum name column width
inline constexpr int kCompactTypeW = 20; // Type column cap for compact column mode
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
@@ -851,17 +871,18 @@ namespace fmt {
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine = 0,
const QString& comment = {}, int colType = kColType, int colName = kColName,
const QString& typeOverride = {});
const QString& typeOverride = {}, bool compact = false);
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8);
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName, bool compact = false);
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {});
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {}, bool compact = false);
QString structTypeName(const Node& node); // Full type string for struct headers
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName = {});
QString pointerTypeName(NodeKind kind, const QString& targetName);
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
const Provider& prov, uint64_t addr,
const QString& ptrTypeName, int colType = kColType, int colName = kColName);
const QString& ptrTypeName, int colType = kColType, int colName = kColName,
bool compact = false);
QString validateBaseAddress(const QString& text);
QString indent(int depth);
QString readValue(const Node& node, const Provider& prov,
@@ -871,10 +892,12 @@ namespace fmt {
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
QString validateValue(NodeKind kind, const QString& text);
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW);
} // namespace fmt
// ── Compose function forward declaration ──
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0);
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
bool compactColumns = false);
} // namespace rcx

353
src/examples/EPROCESS.rcx Normal file
View File

@@ -0,0 +1,353 @@
{
"baseAddress": "FFFF800000000000",
"nextId": "9000",
"nodes": [
{"id":"100","kind":"Struct","name":"list_entry","structTypeName":"_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"101","kind":"Pointer64","name":"Flink","offset":0,"parentId":"100","refId":"100","collapsed":true},
{"id":"102","kind":"Pointer64","name":"Blink","offset":8,"parentId":"100","refId":"100","collapsed":true},
{"id":"110","kind":"Struct","name":"single_list_entry","structTypeName":"_SINGLE_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"111","kind":"Pointer64","name":"Next","offset":0,"parentId":"110","refId":"110","collapsed":true},
{"id":"120","kind":"Struct","name":"ex_push_lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"121","kind":"Hex64","name":"Value","offset":0,"parentId":"120"},
{"id":"130","kind":"Struct","name":"ex_rundown_ref","structTypeName":"_EX_RUNDOWN_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"131","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"130","refId":"0","collapsed":false},
{"id":"132","kind":"UInt64","name":"Count","offset":0,"parentId":"131"},
{"id":"133","kind":"Pointer64","name":"Ptr","offset":0,"parentId":"131"},
{"id":"140","kind":"Struct","name":"ex_fast_ref","structTypeName":"_EX_FAST_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"141","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"140","refId":"0","collapsed":false},
{"id":"142","kind":"Pointer64","name":"Object","offset":0,"parentId":"141"},
{"id":"143","kind":"UInt64","name":"Value","offset":0,"parentId":"141"},
{"id":"150","kind":"Struct","name":"unicode_string","structTypeName":"_UNICODE_STRING","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"151","kind":"UInt16","name":"Length","offset":0,"parentId":"150"},
{"id":"152","kind":"UInt16","name":"MaximumLength","offset":2,"parentId":"150"},
{"id":"153","kind":"Pointer64","name":"Buffer","offset":8,"parentId":"150"},
{"id":"160","kind":"Struct","name":"large_integer","structTypeName":"_LARGE_INTEGER","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"161","kind":"Struct","name":"","offset":0,"parentId":"160","refId":"0","collapsed":false},
{"id":"162","kind":"UInt32","name":"LowPart","offset":0,"parentId":"161"},
{"id":"163","kind":"Int32","name":"HighPart","offset":4,"parentId":"161"},
{"id":"164","kind":"Int64","name":"QuadPart","offset":0,"parentId":"160"},
{"id":"170","kind":"Struct","name":"rtl_avl_tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"171","kind":"Pointer64","name":"Root","offset":0,"parentId":"170"},
{"id":"180","kind":"Struct","name":"kstack_count","structTypeName":"_KSTACK_COUNT","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"181","kind":"Int32","name":"Value","offset":0,"parentId":"180"},
{"id":"182","kind":"Hex32","name":"State:3 StackCount:29","offset":0,"parentId":"180"},
{"id":"190","kind":"Struct","name":"kexecute_options","structTypeName":"_KEXECUTE_OPTIONS","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"191","kind":"Hex8","name":"ExecuteOptions","offset":0,"parentId":"190"},
{"id":"200","kind":"Struct","name":"se_audit_info","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"201","kind":"Pointer64","name":"ImageFileName","offset":0,"parentId":"200"},
{"id":"210","kind":"Struct","name":"ps_protection","structTypeName":"_PS_PROTECTION","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"211","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"210","refId":"0","collapsed":false},
{"id":"212","kind":"UInt8","name":"Level","offset":0,"parentId":"211"},
{"id":"213","kind":"Hex8","name":"Type:3 Audit:1 Signer:4","offset":0,"parentId":"211"},
{"id":"220","kind":"Struct","name":"timer_delay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"221","kind":"Hex64","name":"DelayMs:30 CoalescingWindowMs:30 Reserved:1 NewTimerWheel:1 Retry:1 Locked:1","offset":0,"parentId":"220"},
{"id":"222","kind":"UInt64","name":"All","offset":0,"parentId":"220"},
{"id":"230","kind":"Struct","name":"wnf_state_name","structTypeName":"_WNF_STATE_NAME","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"231","kind":"UInt32","name":"Data_0","offset":0,"parentId":"230"},
{"id":"232","kind":"UInt32","name":"Data_1","offset":4,"parentId":"230"},
{"id":"240","kind":"Struct","name":"dynamic_ranges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"241","kind":"Struct","name":"Tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"240","refId":"170","collapsed":true},
{"id":"242","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":8,"parentId":"240","refId":"120","collapsed":true},
{"id":"250","kind":"Struct","name":"alpc_context","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"251","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"250","refId":"120","collapsed":true},
{"id":"252","kind":"Struct","name":"ViewListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"250","refId":"100","collapsed":true},
{"id":"253","kind":"UInt64","name":"PagedPoolQuotaCache","offset":24,"parentId":"250"},
{"id":"260","kind":"Struct","name":"mmsupport_flags","structTypeName":"_MMSUPPORT_FLAGS","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"261","kind":"Hex32","name":"EntireFlags","offset":0,"parentId":"260"},
{"id":"270","kind":"Struct","name":"mmsupport_shared","structTypeName":"_MMSUPPORT_SHARED","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"271","kind":"Pointer64","name":"WorkingSetLockArray","offset":0,"parentId":"270"},
{"id":"272","kind":"UInt64","name":"ReleasedCommitDebt","offset":8,"parentId":"270"},
{"id":"273","kind":"UInt64","name":"ResetPagesRepurposedCount","offset":16,"parentId":"270"},
{"id":"274","kind":"Pointer64","name":"WsSwapSupport","offset":24,"parentId":"270"},
{"id":"275","kind":"Pointer64","name":"CommitReleaseContext","offset":32,"parentId":"270"},
{"id":"276","kind":"Pointer64","name":"AccessLog","offset":40,"parentId":"270"},
{"id":"277","kind":"UInt64","name":"ChargedWslePages","offset":48,"parentId":"270"},
{"id":"278","kind":"UInt64","name":"ActualWslePages","offset":56,"parentId":"270"},
{"id":"279","kind":"Int32","name":"WorkingSetCoreLock","offset":64,"parentId":"270"},
{"id":"280","kind":"Pointer64","name":"ShadowMapping","offset":72,"parentId":"270"},
{"id":"300","kind":"Struct","name":"mmsupport_instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"301","kind":"UInt32","name":"NextPageColor","offset":0,"parentId":"300"},
{"id":"302","kind":"UInt32","name":"PageFaultCount","offset":4,"parentId":"300"},
{"id":"303","kind":"UInt64","name":"TrimmedPageCount","offset":8,"parentId":"300"},
{"id":"304","kind":"Pointer64","name":"VmWorkingSetList","offset":16,"parentId":"300"},
{"id":"305","kind":"Struct","name":"WorkingSetExpansionLinks","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"300","refId":"100","collapsed":true},
{"id":"306","kind":"UInt64","name":"AgeDistribution_0","offset":40,"parentId":"300"},
{"id":"307","kind":"UInt64","name":"AgeDistribution_1","offset":48,"parentId":"300"},
{"id":"308","kind":"UInt64","name":"AgeDistribution_2","offset":56,"parentId":"300"},
{"id":"309","kind":"UInt64","name":"AgeDistribution_3","offset":64,"parentId":"300"},
{"id":"310","kind":"UInt64","name":"AgeDistribution_4","offset":72,"parentId":"300"},
{"id":"311","kind":"UInt64","name":"AgeDistribution_5","offset":80,"parentId":"300"},
{"id":"312","kind":"UInt64","name":"AgeDistribution_6","offset":88,"parentId":"300"},
{"id":"313","kind":"UInt64","name":"AgeDistribution_7","offset":96,"parentId":"300"},
{"id":"314","kind":"Pointer64","name":"ExitOutswapGate","offset":104,"parentId":"300"},
{"id":"315","kind":"UInt64","name":"MinimumWorkingSetSize","offset":112,"parentId":"300"},
{"id":"316","kind":"UInt64","name":"MaximumWorkingSetSize","offset":120,"parentId":"300"},
{"id":"317","kind":"UInt64","name":"WorkingSetLeafSize","offset":128,"parentId":"300"},
{"id":"318","kind":"UInt64","name":"WorkingSetLeafPrivateSize","offset":136,"parentId":"300"},
{"id":"319","kind":"UInt64","name":"WorkingSetSize","offset":144,"parentId":"300"},
{"id":"320","kind":"UInt64","name":"WorkingSetPrivateSize","offset":152,"parentId":"300"},
{"id":"321","kind":"UInt64","name":"PeakWorkingSetSize","offset":160,"parentId":"300"},
{"id":"322","kind":"UInt32","name":"HardFaultCount","offset":168,"parentId":"300"},
{"id":"323","kind":"UInt16","name":"LastTrimStamp","offset":172,"parentId":"300"},
{"id":"324","kind":"UInt16","name":"PartitionId","offset":174,"parentId":"300"},
{"id":"325","kind":"UInt64","name":"SelfmapLock","offset":176,"parentId":"300"},
{"id":"326","kind":"Struct","name":"Flags","structTypeName":"_MMSUPPORT_FLAGS","offset":184,"parentId":"300","refId":"260","collapsed":true},
{"id":"350","kind":"Struct","name":"mmsupport_full","structTypeName":"_MMSUPPORT_FULL","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"351","kind":"Struct","name":"Instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"350","refId":"300","collapsed":true},
{"id":"352","kind":"Struct","name":"Shared","structTypeName":"_MMSUPPORT_SHARED","offset":192,"parentId":"350","refId":"270","collapsed":true},
{"id":"400","kind":"Struct","name":"dispatcher_header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"401","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"400","refId":"0","collapsed":false},
{"id":"402","kind":"UInt8","name":"Type","offset":0,"parentId":"401"},
{"id":"403","kind":"UInt8","name":"Signalling","offset":1,"parentId":"401"},
{"id":"404","kind":"UInt8","name":"Size","offset":2,"parentId":"401"},
{"id":"405","kind":"UInt8","name":"Reserved1","offset":3,"parentId":"401"},
{"id":"406","kind":"Int32","name":"Lock","offset":0,"parentId":"401"},
{"id":"407","kind":"Int32","name":"SignalState","offset":4,"parentId":"400"},
{"id":"408","kind":"Struct","name":"WaitListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"400","refId":"100","collapsed":true},
{"id":"500","kind":"Struct","name":"kprocess","structTypeName":"_KPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":true},
{"id":"501","kind":"Struct","name":"Header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"500","refId":"400","collapsed":true},
{"id":"502","kind":"Struct","name":"ProfileListHead","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"500","refId":"100","collapsed":true},
{"id":"503","kind":"UInt64","name":"DirectoryTableBase","offset":40,"parentId":"500"},
{"id":"504","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":48,"parentId":"500","refId":"100","collapsed":true},
{"id":"505","kind":"UInt32","name":"ProcessLock","offset":64,"parentId":"500"},
{"id":"506","kind":"UInt32","name":"ProcessTimerDelay","offset":68,"parentId":"500"},
{"id":"507","kind":"UInt64","name":"DeepFreezeStartTime","offset":72,"parentId":"500"},
{"id":"508","kind":"Pointer64","name":"Affinity","offset":80,"parentId":"500"},
{"id":"509","kind":"Hex64","name":"AutoBoostState","offset":88,"parentId":"500"},
{"id":"510","kind":"Struct","name":"ReadyListHead","structTypeName":"_LIST_ENTRY","offset":104,"parentId":"500","refId":"100","collapsed":true},
{"id":"511","kind":"Struct","name":"SwapListEntry","structTypeName":"_SINGLE_LIST_ENTRY","offset":120,"parentId":"500","refId":"110","collapsed":true},
{"id":"512","kind":"Pointer64","name":"ActiveProcessors","offset":128,"parentId":"500"},
{"id":"513","kind":"Struct","name":"","classKeyword":"union","offset":136,"parentId":"500","refId":"0","collapsed":false},
{"id":"514","kind":"Hex32","name":"AutoAlignment:1 DisableBoost:1 DisableQuantum:1 DeepFreeze:1 TimerVirtualization:1 CheckStackExtents:1 CacheIsolationEnabled:1 PpmPolicy:4 VaSpaceDeleted:1 MultiGroup:1 ForegroundProcess:1 ReservedFlags:18","offset":0,"parentId":"513"},
{"id":"515","kind":"Int32","name":"ProcessFlags","offset":0,"parentId":"513"},
{"id":"516","kind":"Int8","name":"BasePriority","offset":144,"parentId":"500"},
{"id":"517","kind":"Int8","name":"QuantumReset","offset":145,"parentId":"500"},
{"id":"518","kind":"Int8","name":"Visited","offset":146,"parentId":"500"},
{"id":"519","kind":"Struct","name":"Flags","structTypeName":"_KEXECUTE_OPTIONS","offset":147,"parentId":"500","refId":"190","collapsed":true},
{"id":"520","kind":"Struct","name":"StackCount","structTypeName":"_KSTACK_COUNT","offset":264,"parentId":"500","refId":"180","collapsed":true},
{"id":"521","kind":"Struct","name":"ProcessListEntry","structTypeName":"_LIST_ENTRY","offset":272,"parentId":"500","refId":"100","collapsed":true},
{"id":"522","kind":"UInt64","name":"CycleTime","offset":288,"parentId":"500"},
{"id":"523","kind":"UInt64","name":"ContextSwitches","offset":296,"parentId":"500"},
{"id":"524","kind":"Pointer64","name":"SchedulingGroup","offset":304,"parentId":"500"},
{"id":"525","kind":"UInt64","name":"KernelTime","offset":312,"parentId":"500"},
{"id":"526","kind":"UInt64","name":"UserTime","offset":320,"parentId":"500"},
{"id":"527","kind":"UInt64","name":"ReadyTime","offset":328,"parentId":"500"},
{"id":"528","kind":"UInt32","name":"FreezeCount","offset":336,"parentId":"500"},
{"id":"529","kind":"UInt64","name":"UserDirectoryTableBase","offset":344,"parentId":"500"},
{"id":"530","kind":"UInt8","name":"AddressPolicy","offset":352,"parentId":"500"},
{"id":"531","kind":"Pointer64","name":"InstrumentationCallback","offset":360,"parentId":"500"},
{"id":"532","kind":"UInt64","name":"SecureHandle","offset":368,"parentId":"500"},
{"id":"533","kind":"UInt64","name":"KernelWaitTime","offset":376,"parentId":"500"},
{"id":"534","kind":"UInt64","name":"UserWaitTime","offset":384,"parentId":"500"},
{"id":"535","kind":"UInt64","name":"LastRebalanceQpc","offset":392,"parentId":"500"},
{"id":"536","kind":"Pointer64","name":"PerProcessorCycleTimes","offset":400,"parentId":"500"},
{"id":"537","kind":"UInt64","name":"ExtendedFeatureDisableMask","offset":408,"parentId":"500"},
{"id":"538","kind":"UInt16","name":"PrimaryGroup","offset":416,"parentId":"500"},
{"id":"539","kind":"Pointer64","name":"UserCetLogging","offset":424,"parentId":"500"},
{"id":"540","kind":"Struct","name":"CpuPartitionList","structTypeName":"_LIST_ENTRY","offset":432,"parentId":"500","refId":"100","collapsed":true},
{"id":"541","kind":"Pointer64","name":"AvailableCpuState","offset":448,"parentId":"500"},
{"id":"2000","kind":"Struct","name":"eprocess","structTypeName":"_EPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":false},
{"id":"2001","kind":"Struct","name":"Pcb","structTypeName":"_KPROCESS","offset":0,"parentId":"2000","refId":"500","collapsed":true},
{"id":"2002","kind":"Struct","name":"ProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":456,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2003","kind":"Pointer64","name":"UniqueProcessId","offset":464,"parentId":"2000"},
{"id":"2004","kind":"Struct","name":"ActiveProcessLinks","structTypeName":"_LIST_ENTRY","offset":472,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2005","kind":"Struct","name":"RundownProtect","structTypeName":"_EX_RUNDOWN_REF","offset":488,"parentId":"2000","refId":"130","collapsed":true},
{"id":"2006","kind":"Struct","name":"","classKeyword":"union","offset":496,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2007","kind":"UInt32","name":"Flags2","offset":0,"parentId":"2006"},
{"id":"2008","kind":"Hex32","name":"JobNotReallyActive:1 AccountingFolded:1 NewProcessReported:1 ExitProcessReported:1 ReportCommitChanges:1 LastReportMemory:1 ForceWakeCharge:1 CrossSessionCreate:1 NeedsHandleRundown:1 RefTraceEnabled:1 PicoCreated:1 EmptyJobEvaluated:1 DefaultPagePriority:3 PrimaryTokenFrozen:1","offset":0,"parentId":"2006"},
{"id":"2009","kind":"Struct","name":"","classKeyword":"union","offset":500,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2010","kind":"UInt32","name":"Flags","offset":0,"parentId":"2009"},
{"id":"2011","kind":"Hex32","name":"CreateReported:1 NoDebugInherit:1 ProcessExiting:1 ProcessDelete:1 ManageExecutableMemoryWrites:1 VmDeleted:1 OutswapEnabled:1 Outswapped:1 FailFastOnCommitFail:1 Wow64VaSpace4Gb:1 AddressSpaceInitialized:2 SetTimerResolution:1 BreakOnTermination:1","offset":0,"parentId":"2009"},
{"id":"2012","kind":"Int64","name":"CreateTime","offset":504,"parentId":"2000"},
{"id":"2013","kind":"UInt64","name":"ProcessQuotaUsage_0","offset":512,"parentId":"2000"},
{"id":"2014","kind":"UInt64","name":"ProcessQuotaUsage_1","offset":520,"parentId":"2000"},
{"id":"2015","kind":"UInt64","name":"ProcessQuotaPeak_0","offset":528,"parentId":"2000"},
{"id":"2016","kind":"UInt64","name":"ProcessQuotaPeak_1","offset":536,"parentId":"2000"},
{"id":"2017","kind":"UInt64","name":"PeakVirtualSize","offset":544,"parentId":"2000"},
{"id":"2018","kind":"UInt64","name":"VirtualSize","offset":552,"parentId":"2000"},
{"id":"2019","kind":"Struct","name":"SessionProcessLinks","structTypeName":"_LIST_ENTRY","offset":560,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2020","kind":"Struct","name":"","classKeyword":"union","offset":576,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2021","kind":"Pointer64","name":"ExceptionPortData","offset":0,"parentId":"2020"},
{"id":"2022","kind":"UInt64","name":"ExceptionPortValue","offset":0,"parentId":"2020"},
{"id":"2023","kind":"Struct","name":"Token","structTypeName":"_EX_FAST_REF","offset":584,"parentId":"2000","refId":"140","collapsed":true},
{"id":"2024","kind":"UInt64","name":"MmReserved","offset":592,"parentId":"2000"},
{"id":"2025","kind":"Struct","name":"AddressCreationLock","structTypeName":"_EX_PUSH_LOCK","offset":600,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2026","kind":"Struct","name":"PageTableCommitmentLock","structTypeName":"_EX_PUSH_LOCK","offset":608,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2027","kind":"Pointer64","name":"RotateInProgress","offset":616,"parentId":"2000"},
{"id":"2028","kind":"Pointer64","name":"ForkInProgress","offset":624,"parentId":"2000"},
{"id":"2029","kind":"Pointer64","name":"CommitChargeJob","offset":632,"parentId":"2000"},
{"id":"2030","kind":"Struct","name":"CloneRoot","structTypeName":"_RTL_AVL_TREE","offset":640,"parentId":"2000","refId":"170","collapsed":true},
{"id":"2031","kind":"UInt64","name":"NumberOfPrivatePages","offset":648,"parentId":"2000"},
{"id":"2032","kind":"UInt64","name":"NumberOfLockedPages","offset":656,"parentId":"2000"},
{"id":"2033","kind":"Pointer64","name":"Win32Process","offset":664,"parentId":"2000"},
{"id":"2034","kind":"Pointer64","name":"Job","offset":672,"parentId":"2000"},
{"id":"2035","kind":"Pointer64","name":"SectionObject","offset":680,"parentId":"2000"},
{"id":"2036","kind":"Pointer64","name":"SectionBaseAddress","offset":688,"parentId":"2000"},
{"id":"2037","kind":"UInt32","name":"Cookie","offset":696,"parentId":"2000"},
{"id":"2038","kind":"Pointer64","name":"WorkingSetWatch","offset":704,"parentId":"2000"},
{"id":"2039","kind":"Pointer64","name":"Win32WindowStation","offset":712,"parentId":"2000"},
{"id":"2040","kind":"Pointer64","name":"InheritedFromUniqueProcessId","offset":720,"parentId":"2000"},
{"id":"2041","kind":"UInt64","name":"OwnerProcessId","offset":728,"parentId":"2000"},
{"id":"2042","kind":"Pointer64","name":"Peb","offset":736,"parentId":"2000"},
{"id":"2043","kind":"Pointer64","name":"Session","offset":744,"parentId":"2000"},
{"id":"2044","kind":"Pointer64","name":"Spare1","offset":752,"parentId":"2000"},
{"id":"2045","kind":"Pointer64","name":"QuotaBlock","offset":760,"parentId":"2000"},
{"id":"2046","kind":"Pointer64","name":"ObjectTable","offset":768,"parentId":"2000"},
{"id":"2047","kind":"Pointer64","name":"DebugPort","offset":776,"parentId":"2000"},
{"id":"2048","kind":"Pointer64","name":"WoW64Process","offset":784,"parentId":"2000"},
{"id":"2049","kind":"Struct","name":"DeviceMap","structTypeName":"_EX_FAST_REF","offset":792,"parentId":"2000","refId":"140","collapsed":true},
{"id":"2050","kind":"Pointer64","name":"EtwDataSource","offset":800,"parentId":"2000"},
{"id":"2051","kind":"UInt64","name":"PageDirectoryPte","offset":808,"parentId":"2000"},
{"id":"2052","kind":"Pointer64","name":"ImageFilePointer","offset":816,"parentId":"2000"},
{"id":"2053","kind":"Hex64","name":"ImageFileName_lo","offset":824,"parentId":"2000"},
{"id":"2054","kind":"Hex32","name":"ImageFileName_mi","offset":832,"parentId":"2000"},
{"id":"2055","kind":"Hex16","name":"ImageFileName_hi","offset":836,"parentId":"2000"},
{"id":"2056","kind":"UInt8","name":"ImageFileName_14","offset":838,"parentId":"2000"},
{"id":"2057","kind":"UInt8","name":"PriorityClass","offset":839,"parentId":"2000"},
{"id":"2058","kind":"Pointer64","name":"SecurityPort","offset":840,"parentId":"2000"},
{"id":"2059","kind":"Struct","name":"SeAuditProcessCreationInfo","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":848,"parentId":"2000","refId":"200","collapsed":true},
{"id":"2060","kind":"Struct","name":"JobLinks","structTypeName":"_LIST_ENTRY","offset":856,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2061","kind":"Pointer64","name":"HighestUserAddress","offset":872,"parentId":"2000"},
{"id":"2062","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":880,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2063","kind":"UInt32","name":"ActiveThreads","offset":896,"parentId":"2000"},
{"id":"2064","kind":"UInt32","name":"ImagePathHash","offset":900,"parentId":"2000"},
{"id":"2065","kind":"UInt32","name":"DefaultHardErrorProcessing","offset":904,"parentId":"2000"},
{"id":"2066","kind":"Int32","name":"LastThreadExitStatus","offset":908,"parentId":"2000"},
{"id":"2067","kind":"Struct","name":"PrefetchTrace","structTypeName":"_EX_FAST_REF","offset":912,"parentId":"2000","refId":"140","collapsed":true},
{"id":"2068","kind":"Pointer64","name":"LockedPagesList","offset":920,"parentId":"2000"},
{"id":"2069","kind":"Int64","name":"ReadOperationCount","offset":928,"parentId":"2000"},
{"id":"2070","kind":"Int64","name":"WriteOperationCount","offset":936,"parentId":"2000"},
{"id":"2071","kind":"Int64","name":"OtherOperationCount","offset":944,"parentId":"2000"},
{"id":"2072","kind":"Int64","name":"ReadTransferCount","offset":952,"parentId":"2000"},
{"id":"2073","kind":"Int64","name":"WriteTransferCount","offset":960,"parentId":"2000"},
{"id":"2074","kind":"Int64","name":"OtherTransferCount","offset":968,"parentId":"2000"},
{"id":"2075","kind":"UInt64","name":"CommitChargeLimit","offset":976,"parentId":"2000"},
{"id":"2076","kind":"UInt64","name":"CommitCharge","offset":984,"parentId":"2000"},
{"id":"2077","kind":"UInt64","name":"CommitChargePeak","offset":992,"parentId":"2000"},
{"id":"2078","kind":"Struct","name":"Vm","structTypeName":"_MMSUPPORT_FULL","offset":1024,"parentId":"2000","refId":"350","collapsed":true},
{"id":"2079","kind":"Struct","name":"MmProcessLinks","structTypeName":"_LIST_ENTRY","offset":1344,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2080","kind":"UInt32","name":"ModifiedPageCount","offset":1360,"parentId":"2000"},
{"id":"2081","kind":"Int32","name":"ExitStatus","offset":1364,"parentId":"2000"},
{"id":"2082","kind":"Struct","name":"VadRoot","structTypeName":"_RTL_AVL_TREE","offset":1368,"parentId":"2000","refId":"170","collapsed":true},
{"id":"2083","kind":"Pointer64","name":"VadHint","offset":1376,"parentId":"2000"},
{"id":"2084","kind":"UInt64","name":"VadCount","offset":1384,"parentId":"2000"},
{"id":"2085","kind":"UInt64","name":"VadPhysicalPages","offset":1392,"parentId":"2000"},
{"id":"2086","kind":"UInt64","name":"VadPhysicalPagesLimit","offset":1400,"parentId":"2000"},
{"id":"2087","kind":"Struct","name":"AlpcContext","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":1408,"parentId":"2000","refId":"250","collapsed":true},
{"id":"2088","kind":"Struct","name":"TimerResolutionLink","structTypeName":"_LIST_ENTRY","offset":1440,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2089","kind":"Pointer64","name":"TimerResolutionStackRecord","offset":1456,"parentId":"2000"},
{"id":"2090","kind":"UInt32","name":"RequestedTimerResolution","offset":1464,"parentId":"2000"},
{"id":"2091","kind":"UInt32","name":"SmallestTimerResolution","offset":1468,"parentId":"2000"},
{"id":"2092","kind":"Int64","name":"ExitTime","offset":1472,"parentId":"2000"},
{"id":"2093","kind":"Pointer64","name":"InvertedFunctionTable","offset":1480,"parentId":"2000"},
{"id":"2094","kind":"Struct","name":"InvertedFunctionTableLock","structTypeName":"_EX_PUSH_LOCK","offset":1488,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2095","kind":"UInt32","name":"ActiveThreadsHighWatermark","offset":1496,"parentId":"2000"},
{"id":"2096","kind":"UInt32","name":"LargePrivateVadCount","offset":1500,"parentId":"2000"},
{"id":"2097","kind":"Struct","name":"ThreadListLock","structTypeName":"_EX_PUSH_LOCK","offset":1504,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2098","kind":"Pointer64","name":"WnfContext","offset":1512,"parentId":"2000"},
{"id":"2099","kind":"Pointer64","name":"ServerSilo","offset":1520,"parentId":"2000"},
{"id":"2100","kind":"UInt8","name":"SignatureLevel","offset":1528,"parentId":"2000"},
{"id":"2101","kind":"UInt8","name":"SectionSignatureLevel","offset":1529,"parentId":"2000"},
{"id":"2102","kind":"Struct","name":"Protection","structTypeName":"_PS_PROTECTION","offset":1530,"parentId":"2000","refId":"210","collapsed":true},
{"id":"2103","kind":"Hex8","name":"HangCount:3 GhostCount:3 PrefilterException:1","offset":1531,"parentId":"2000"},
{"id":"2104","kind":"Struct","name":"","classKeyword":"union","offset":1532,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2105","kind":"UInt32","name":"Flags3","offset":0,"parentId":"2104"},
{"id":"2106","kind":"Hex32","name":"Minimal:1 ReplacingPageRoot:1 Crashed:1 JobVadsAreTracked:1 VadTrackingDisabled:1 AuxiliaryProcess:1 SubsystemProcess:1 IndirectCpuSets:1 RelinquishedCommit:1 HighGraphicsPriority:1 CommitFailLogged:1 ReserveFailLogged:1 SystemProcess:1","offset":0,"parentId":"2104"},
{"id":"2107","kind":"Int32","name":"DeviceAsid","offset":1536,"parentId":"2000"},
{"id":"2108","kind":"Pointer64","name":"SvmData","offset":1544,"parentId":"2000"},
{"id":"2109","kind":"Struct","name":"SvmProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":1552,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2110","kind":"UInt64","name":"SvmLock","offset":1560,"parentId":"2000"},
{"id":"2111","kind":"Struct","name":"SvmProcessDeviceListHead","structTypeName":"_LIST_ENTRY","offset":1568,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2112","kind":"UInt64","name":"LastFreezeInterruptTime","offset":1584,"parentId":"2000"},
{"id":"2113","kind":"Pointer64","name":"DiskCounters","offset":1592,"parentId":"2000"},
{"id":"2114","kind":"Pointer64","name":"PicoContext","offset":1600,"parentId":"2000"},
{"id":"2115","kind":"Pointer64","name":"EnclaveTable","offset":1608,"parentId":"2000"},
{"id":"2116","kind":"UInt64","name":"EnclaveNumber","offset":1616,"parentId":"2000"},
{"id":"2117","kind":"Struct","name":"EnclaveLock","structTypeName":"_EX_PUSH_LOCK","offset":1624,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2118","kind":"UInt32","name":"HighPriorityFaultsAllowed","offset":1632,"parentId":"2000"},
{"id":"2119","kind":"Pointer64","name":"EnergyContext","offset":1640,"parentId":"2000"},
{"id":"2120","kind":"Pointer64","name":"VmContext","offset":1648,"parentId":"2000"},
{"id":"2121","kind":"UInt64","name":"SequenceNumber","offset":1656,"parentId":"2000"},
{"id":"2122","kind":"UInt64","name":"CreateInterruptTime","offset":1664,"parentId":"2000"},
{"id":"2123","kind":"UInt64","name":"CreateUnbiasedInterruptTime","offset":1672,"parentId":"2000"},
{"id":"2124","kind":"UInt64","name":"TotalUnbiasedFrozenTime","offset":1680,"parentId":"2000"},
{"id":"2125","kind":"UInt64","name":"LastAppStateUpdateTime","offset":1688,"parentId":"2000"},
{"id":"2126","kind":"Hex64","name":"LastAppStateUptime:61 LastAppState:3","offset":1696,"parentId":"2000"},
{"id":"2127","kind":"UInt64","name":"SharedCommitCharge","offset":1704,"parentId":"2000"},
{"id":"2128","kind":"Struct","name":"SharedCommitLock","structTypeName":"_EX_PUSH_LOCK","offset":1712,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2129","kind":"Struct","name":"SharedCommitLinks","structTypeName":"_LIST_ENTRY","offset":1720,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2130","kind":"UInt64","name":"AllowedCpuSets","offset":1736,"parentId":"2000"},
{"id":"2131","kind":"UInt64","name":"DefaultCpuSets","offset":1744,"parentId":"2000"},
{"id":"2132","kind":"Pointer64","name":"DiskIoAttribution","offset":1752,"parentId":"2000"},
{"id":"2133","kind":"Pointer64","name":"DxgProcess","offset":1760,"parentId":"2000"},
{"id":"2134","kind":"UInt32","name":"Win32KFilterSet","offset":1768,"parentId":"2000"},
{"id":"2135","kind":"UInt16","name":"Machine","offset":1772,"parentId":"2000"},
{"id":"2136","kind":"UInt8","name":"MmSlabIdentity","offset":1774,"parentId":"2000"},
{"id":"2137","kind":"UInt8","name":"Spare0","offset":1775,"parentId":"2000"},
{"id":"2138","kind":"Struct","name":"ProcessTimerDelay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","offset":1776,"parentId":"2000","refId":"220","collapsed":true},
{"id":"2139","kind":"UInt32","name":"KTimerSets","offset":1784,"parentId":"2000"},
{"id":"2140","kind":"UInt32","name":"KTimer2Sets","offset":1788,"parentId":"2000"},
{"id":"2141","kind":"UInt32","name":"ThreadTimerSets","offset":1792,"parentId":"2000"},
{"id":"2142","kind":"UInt64","name":"VirtualTimerListLock","offset":1800,"parentId":"2000"},
{"id":"2143","kind":"Struct","name":"VirtualTimerListHead","structTypeName":"_LIST_ENTRY","offset":1808,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2144","kind":"Struct","name":"WakeChannel","structTypeName":"_WNF_STATE_NAME","offset":1824,"parentId":"2000","refId":"230","collapsed":true},
{"id":"2145","kind":"Struct","name":"","classKeyword":"union","offset":1872,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2146","kind":"UInt32","name":"MitigationFlags","offset":0,"parentId":"2145"},
{"id":"2147","kind":"Hex32","name":"ControlFlowGuardEnabled:1 ControlFlowGuardExportSuppressionEnabled:1 ControlFlowGuardStrict:1 DisallowStrippedImages:1 ForceRelocateImages:1 HighEntropyASLREnabled:1 StackRandomizationDisabled:1 ExtensionPointDisable:1 DisableDynamicCode:1","offset":0,"parentId":"2145"},
{"id":"2148","kind":"Struct","name":"","classKeyword":"union","offset":1876,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2149","kind":"UInt32","name":"MitigationFlags2","offset":0,"parentId":"2148"},
{"id":"2150","kind":"Hex32","name":"EnableExportAddressFilter:1 AuditExportAddressFilter:1 EnableRopStackPivot:1 AuditRopStackPivot:1 CetUserShadowStacks:1 SpeculativeStoreBypassDisable:1","offset":0,"parentId":"2148"},
{"id":"2151","kind":"Pointer64","name":"PartitionObject","offset":1880,"parentId":"2000"},
{"id":"2152","kind":"UInt64","name":"SecurityDomain","offset":1888,"parentId":"2000"},
{"id":"2153","kind":"UInt64","name":"ParentSecurityDomain","offset":1896,"parentId":"2000"},
{"id":"2154","kind":"Pointer64","name":"CoverageSamplerContext","offset":1904,"parentId":"2000"},
{"id":"2155","kind":"Pointer64","name":"MmHotPatchContext","offset":1912,"parentId":"2000"},
{"id":"2156","kind":"Struct","name":"DynamicEHContinuationTargetsTree","structTypeName":"_RTL_AVL_TREE","offset":1920,"parentId":"2000","refId":"170","collapsed":true},
{"id":"2157","kind":"Struct","name":"DynamicEHContinuationTargetsLock","structTypeName":"_EX_PUSH_LOCK","offset":1928,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2158","kind":"Struct","name":"DynamicEnforcedCetCompatibleRanges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":1936,"parentId":"2000","refId":"240","collapsed":true},
{"id":"2159","kind":"UInt32","name":"DisabledComponentFlags","offset":1952,"parentId":"2000"},
{"id":"2160","kind":"Int32","name":"PageCombineSequence","offset":1956,"parentId":"2000"},
{"id":"2161","kind":"Struct","name":"EnableOptionalXStateFeaturesLock","structTypeName":"_EX_PUSH_LOCK","offset":1960,"parentId":"2000","refId":"120","collapsed":true},
{"id":"2162","kind":"Pointer64","name":"PathRedirectionHashes","offset":1968,"parentId":"2000"},
{"id":"2163","kind":"Pointer64","name":"SyscallProvider","offset":1976,"parentId":"2000"},
{"id":"2164","kind":"Struct","name":"SyscallProviderProcessLinks","structTypeName":"_LIST_ENTRY","offset":1984,"parentId":"2000","refId":"100","collapsed":true},
{"id":"2165","kind":"Hex64","name":"SyscallProviderDispatchContext","offset":2000,"parentId":"2000"},
{"id":"2166","kind":"Struct","name":"","classKeyword":"union","offset":2008,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2167","kind":"UInt32","name":"MitigationFlags3","offset":0,"parentId":"2166"},
{"id":"2168","kind":"Hex32","name":"RestrictCoreSharing:1 DisallowFsctlSystemCalls:1 AuditDisallowFsctlSystemCalls:1 MitigationFlags3Spare:29","offset":0,"parentId":"2166"},
{"id":"2169","kind":"Struct","name":"","classKeyword":"union","offset":2012,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2170","kind":"UInt32","name":"Flags4","offset":0,"parentId":"2169"},
{"id":"2171","kind":"Hex32","name":"ThreadWasActive:1 MinimalTerminate:1 ImageExpansionDisable:1 SessionFirstProcess:1","offset":0,"parentId":"2169"},
{"id":"2172","kind":"Struct","name":"","classKeyword":"union","offset":2016,"parentId":"2000","refId":"0","collapsed":true},
{"id":"2173","kind":"UInt32","name":"SyscallUsage","offset":0,"parentId":"2172"},
{"id":"2174","kind":"Hex32","name":"SystemModuleInformation:1 SystemModuleInformationEx:1 SystemLocksInformation:1 SystemHandleInformation:1 SystemExtendedHandleInformation:1","offset":0,"parentId":"2172"},
{"id":"2175","kind":"Int32","name":"SupervisorDeviceAsid","offset":2020,"parentId":"2000"},
{"id":"2176","kind":"Pointer64","name":"SupervisorSvmData","offset":2024,"parentId":"2000"},
{"id":"2177","kind":"Pointer64","name":"NetworkCounters","offset":2032,"parentId":"2000"},
{"id":"2178","kind":"Hex64","name":"Execution","offset":2040,"parentId":"2000"},
{"id":"2179","kind":"Pointer64","name":"ThreadIndexTable","offset":2048,"parentId":"2000"},
{"id":"2180","kind":"Struct","name":"FreezeWorkLinks","structTypeName":"_LIST_ENTRY","offset":2056,"parentId":"2000","refId":"100","collapsed":true}
]
}

616
src/examples/MMPFN.rcx Normal file
View File

@@ -0,0 +1,616 @@
{
"baseAddress": "FFFFCA8010000000",
"nextId": "3000",
"nodes": [
{
"id": "100",
"kind": "Struct",
"name": "list_entry",
"structTypeName": "_LIST_ENTRY",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "101",
"kind": "Pointer64",
"name": "Flink",
"offset": 0,
"parentId": "100",
"refId": "100",
"collapsed": true
},
{
"id": "102",
"kind": "Pointer64",
"name": "Blink",
"offset": 8,
"parentId": "100",
"refId": "100",
"collapsed": true
},
{
"id": "200",
"kind": "Struct",
"name": "balanced_node",
"structTypeName": "_RTL_BALANCED_NODE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "210",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 0,
"parentId": "200",
"refId": "0",
"collapsed": false
},
{
"id": "211",
"kind": "Pointer64",
"name": "Left",
"offset": 0,
"parentId": "210",
"refId": "200",
"collapsed": true
},
{
"id": "212",
"kind": "Pointer64",
"name": "Right",
"offset": 8,
"parentId": "210",
"refId": "200",
"collapsed": true
},
{
"id": "220",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 16,
"parentId": "200",
"refId": "0",
"collapsed": false
},
{
"id": "221",
"kind": "UInt64",
"name": "ParentValue",
"offset": 0,
"parentId": "220"
},
{
"id": "300",
"kind": "Struct",
"name": "mmpte",
"structTypeName": "_MMPTE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "301",
"kind": "Struct",
"name": "u",
"classKeyword": "union",
"offset": 0,
"parentId": "300",
"refId": "0",
"collapsed": false
},
{
"id": "302",
"kind": "UInt64",
"name": "Long",
"offset": 0,
"parentId": "301"
},
{
"id": "303",
"kind": "Struct",
"name": "Hard",
"structTypeName": "_MMPTE_HARDWARE",
"offset": 0,
"parentId": "301",
"refId": "400",
"collapsed": true
},
{
"id": "304",
"kind": "Struct",
"name": "Proto",
"structTypeName": "_MMPTE_PROTOTYPE",
"offset": 0,
"parentId": "301",
"refId": "600",
"collapsed": true
},
{
"id": "305",
"kind": "Struct",
"name": "Soft",
"structTypeName": "_MMPTE_SOFTWARE",
"offset": 0,
"parentId": "301",
"refId": "500",
"collapsed": true
},
{
"id": "306",
"kind": "Struct",
"name": "Trans",
"structTypeName": "_MMPTE_TRANSITION",
"offset": 0,
"parentId": "301",
"refId": "700",
"collapsed": true
},
{
"id": "307",
"kind": "Struct",
"name": "Subsect",
"structTypeName": "_MMPTE_SUBSECTION",
"offset": 0,
"parentId": "301",
"refId": "800",
"collapsed": true
},
{
"id": "308",
"kind": "Struct",
"name": "TimeStamp",
"structTypeName": "_MMPTE_TIMESTAMP",
"offset": 0,
"parentId": "301",
"refId": "900",
"collapsed": true
},
{
"id": "309",
"kind": "Struct",
"name": "List",
"structTypeName": "_MMPTE_LIST",
"offset": 0,
"parentId": "301",
"refId": "1000",
"collapsed": true
},
{
"id": "400",
"kind": "Struct",
"name": "mmpte_hardware",
"structTypeName": "_MMPTE_HARDWARE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "401",
"kind": "Hex64",
"name": "Valid:1 Dirty1:1 Owner:1 WriteThrough:1 CacheDisable:1 Accessed:1 Dirty:1 LargePage:1 Global:1 CopyOnWrite:1 Unused:1 Write:1 PageFrameNumber:40 ReservedForSoftware:4 WsleAge:4 WsleProtection:3 NoExecute:1",
"offset": 0,
"parentId": "400"
},
{
"id": "500",
"kind": "Struct",
"name": "mmpte_software",
"structTypeName": "_MMPTE_SOFTWARE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "501",
"kind": "Hex64",
"name": "Valid:1 PageFileReserved:1 PageFileAllocated:1 ColdPage:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 UsedPageTableEntries:10 ShadowStack:1 OnStandbyLookaside:1 Unused:4 PageFileHigh:32",
"offset": 0,
"parentId": "500"
},
{
"id": "600",
"kind": "Struct",
"name": "mmpte_prototype",
"structTypeName": "_MMPTE_PROTOTYPE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "601",
"kind": "Hex64",
"name": "Valid:1 DemandFillProto:1 HiberVerifyConverted:1 ReadOnly:1 SwizzleBit:1 Protection:5 Prototype:1 Combined:1 Unused1:4 ProtoAddress:48",
"offset": 0,
"parentId": "600"
},
{
"id": "700",
"kind": "Struct",
"name": "mmpte_transition",
"structTypeName": "_MMPTE_TRANSITION",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "701",
"kind": "Hex64",
"name": "Valid:1 Write:1 OnStandbyLookaside:1 IoTracker:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFrameNumber:40 Unused:12",
"offset": 0,
"parentId": "700"
},
{
"id": "800",
"kind": "Struct",
"name": "mmpte_subsection",
"structTypeName": "_MMPTE_SUBSECTION",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "801",
"kind": "Hex64",
"name": "Valid:1 Unused0:2 OnStandbyLookaside:1 SwizzleBit:1 Protection:5 Prototype:1 ColdPage:1 Unused2:3 ExecutePrivilege:1 SubsectionAddress:48",
"offset": 0,
"parentId": "800"
},
{
"id": "900",
"kind": "Struct",
"name": "mmpte_timestamp",
"structTypeName": "_MMPTE_TIMESTAMP",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "901",
"kind": "Hex64",
"name": "MustBeZero:1 Unused:3 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 Reserved:16 GlobalTimeStamp:32",
"offset": 0,
"parentId": "900"
},
{
"id": "1000",
"kind": "Struct",
"name": "mmpte_list",
"structTypeName": "_MMPTE_LIST",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1001",
"kind": "Hex64",
"name": "Valid:1 OneEntry:1 filler0:2 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 filler1:13 NextEntry:39",
"offset": 0,
"parentId": "1000"
},
{
"id": "1100",
"kind": "Struct",
"name": "mipfnflink",
"structTypeName": "_MIPFNFLINK",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1101",
"kind": "Hex64",
"name": "Flink",
"offset": 0,
"parentId": "1100"
},
{
"id": "1200",
"kind": "Struct",
"name": "mipfnblink",
"structTypeName": "_MIPFNBLINK",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1201",
"kind": "Hex64",
"name": "Blink",
"offset": 0,
"parentId": "1200"
},
{
"id": "1300",
"kind": "Struct",
"name": "mmpfnentry1",
"structTypeName": "_MMPFNENTRY1",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1301",
"kind": "Hex8",
"name": "Flags",
"offset": 0,
"parentId": "1300"
},
{
"id": "1400",
"kind": "Struct",
"name": "mmpfnentry3",
"structTypeName": "_MMPFNENTRY3",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1401",
"kind": "Hex8",
"name": "Flags",
"offset": 0,
"parentId": "1400"
},
{
"id": "1500",
"kind": "Struct",
"name": "mi_pfn_flags",
"structTypeName": "_MI_PFN_FLAGS",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1501",
"kind": "Hex32",
"name": "Flags",
"offset": 0,
"parentId": "1500"
},
{
"id": "1600",
"kind": "Struct",
"name": "mi_pfn_flags4",
"structTypeName": "_MI_PFN_FLAGS4",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1601",
"kind": "Hex64",
"name": "Flags",
"offset": 0,
"parentId": "1600"
},
{
"id": "1700",
"kind": "Struct",
"name": "mi_pfn_flags5",
"structTypeName": "_MI_PFN_FLAGS5",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1701",
"kind": "Hex32",
"name": "Flags",
"offset": 0,
"parentId": "1700"
},
{
"id": "2000",
"kind": "Struct",
"name": "mmpfn",
"structTypeName": "_MMPFN",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": false
},
{
"id": "2001",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 0,
"parentId": "2000",
"refId": "0",
"collapsed": false
},
{
"id": "2010",
"kind": "Struct",
"name": "ListEntry",
"structTypeName": "_LIST_ENTRY",
"offset": 0,
"parentId": "2001",
"refId": "100",
"collapsed": true
},
{
"id": "2011",
"kind": "Struct",
"name": "TreeNode",
"structTypeName": "_RTL_BALANCED_NODE",
"offset": 0,
"parentId": "2001",
"refId": "200",
"collapsed": true
},
{
"id": "2012",
"kind": "Struct",
"name": "",
"offset": 0,
"parentId": "2001",
"refId": "0",
"collapsed": false
},
{
"id": "2013",
"kind": "Struct",
"name": "u1",
"structTypeName": "_MIPFNFLINK",
"offset": 0,
"parentId": "2012",
"refId": "1100",
"collapsed": true
},
{
"id": "2014",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 8,
"parentId": "2012",
"refId": "0",
"collapsed": false
},
{
"id": "2015",
"kind": "Pointer64",
"name": "PteAddress",
"offset": 0,
"parentId": "2014",
"refId": "300",
"collapsed": true
},
{
"id": "2016",
"kind": "UInt64",
"name": "PteLong",
"offset": 0,
"parentId": "2014"
},
{
"id": "2017",
"kind": "Struct",
"name": "OriginalPte",
"structTypeName": "_MMPTE",
"offset": 16,
"parentId": "2012",
"refId": "300",
"collapsed": true
},
{
"id": "2020",
"kind": "Struct",
"name": "u2",
"structTypeName": "_MIPFNBLINK",
"offset": 24,
"parentId": "2000",
"refId": "1200",
"collapsed": true
},
{
"id": "2030",
"kind": "Struct",
"name": "u3",
"classKeyword": "union",
"offset": 32,
"parentId": "2000",
"refId": "0",
"collapsed": false
},
{
"id": "2031",
"kind": "Struct",
"name": "",
"offset": 0,
"parentId": "2030",
"refId": "0",
"collapsed": false
},
{
"id": "2032",
"kind": "UInt16",
"name": "ReferenceCount",
"offset": 0,
"parentId": "2031"
},
{
"id": "2033",
"kind": "Struct",
"name": "e1",
"structTypeName": "_MMPFNENTRY1",
"offset": 2,
"parentId": "2031",
"refId": "1300",
"collapsed": true
},
{
"id": "2034",
"kind": "Struct",
"name": "e4",
"structTypeName": "_MI_PFN_FLAGS",
"offset": 0,
"parentId": "2030",
"refId": "1500",
"collapsed": true
},
{
"id": "2040",
"kind": "Struct",
"name": "u5",
"structTypeName": "_MI_PFN_FLAGS5",
"offset": 36,
"parentId": "2000",
"refId": "1700",
"collapsed": true
},
{
"id": "2050",
"kind": "Struct",
"name": "u4",
"structTypeName": "_MI_PFN_FLAGS4",
"offset": 40,
"parentId": "2000",
"refId": "1600",
"collapsed": true
}
]
}

View File

@@ -23,6 +23,14 @@ static QString fit(QString s, int w) {
return s.leftJustified(w, ' ');
}
// Like fit() but overflows instead of truncating: if s exceeds w, return full string
static QString fitOverflow(const QString& s, int w) {
if (w <= 0) return {};
if (s.size() <= w)
return s.leftJustified(w, ' ');
return s;
}
// ── Type name ──
// Override seam: injectable type-name provider
@@ -140,8 +148,8 @@ QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDig
// ── Struct type name (for width calculation) ──
QString structTypeName(const Node& node) {
// Full type string: "struct TypeName" or just "struct" if no typename
QString base = typeName(node.kind).trimmed(); // "struct"
// Full type string: "struct TypeName", "union TypeName", "class TypeName", etc.
QString base = node.resolvedClassKeyword();
if (!node.structTypeName.isEmpty())
return base + QStringLiteral(" ") + node.structTypeName;
return base;
@@ -149,11 +157,16 @@ QString structTypeName(const Node& node) {
// ── Struct header / footer ──
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) {
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName, bool compact) {
// Columnar format: <type> <name> { (or no brace when collapsed)
QString ind = indent(depth);
QString type = fit(structTypeName(node), colType);
QString rawType = structTypeName(node);
QString suffix = collapsed ? QString() : QStringLiteral("{");
if (node.name.isEmpty()) {
// Anonymous struct/union: "union {" with no column padding
return ind + rawType + SEP + suffix;
}
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
return ind + type + SEP + node.name + SEP + suffix;
}
@@ -163,9 +176,10 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
// ── Array header ──
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName) {
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName, bool compact) {
QString ind = indent(depth);
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen, elemStructName), colType);
QString rawType = arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
QString suffix = collapsed ? QString() : QStringLiteral("{");
return ind + type + SEP + node.name + SEP + suffix;
}
@@ -174,10 +188,16 @@ QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collap
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
const Provider& prov, uint64_t addr,
const QString& ptrTypeName, int colType, int colName) {
const QString& ptrTypeName, int colType, int colName,
bool compact) {
QString ind = indent(depth);
QString type = fit(ptrTypeName, colType);
bool overflow = compact && ptrTypeName.size() > colType;
QString type = compact ? fitOverflow(ptrTypeName, colType) : fit(ptrTypeName, colType);
if (collapsed) {
if (overflow) {
// Overflow: no column padding
return ind + type + SEP + node.name + SEP + readValue(node, prov, addr, 0);
}
// Collapsed: show pointer value instead of brace (name padded for value alignment)
QString name = fit(node.name, colName);
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
@@ -366,12 +386,22 @@ QString readValue(const Node& node, const Provider& prov,
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine,
const QString& comment, int colType, int colName,
const QString& typeOverride) {
const QString& typeOverride, bool compact) {
QString ind = indent(depth);
QString type = typeOverride.isEmpty() ? typeName(node.kind, colType) : fit(typeOverride, colType);
QString name = fit(node.name, colName);
// Compute raw type string for overflow detection
QString rawType = typeOverride.isEmpty() ? typeNameRaw(node.kind) : typeOverride;
bool overflow = compact && rawType.size() > colType;
QString type = overflow ? fitOverflow(rawType, colType)
: (typeOverride.isEmpty() ? typeName(node.kind, colType)
: fit(typeOverride, colType));
QString name = overflow ? node.name : fit(node.name, colName);
// Effective column width for this line (accounts for overflow, clamped to hard max)
int effectiveColType = overflow ? rawType.size() : colType;
// Blank prefix for continuation lines (same width as type+sep+name+sep)
const int prefixW = colType + colName + 2 * kSepWidth;
const int prefixW = effectiveColType + (overflow ? name.size() : colName) + 2 * kSepWidth;
// Comment suffix (only present when a comment is provided; no trailing padding)
QString cmtSuffix = comment.isEmpty() ? QString()
@@ -394,7 +424,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
}
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
QString val = overflow ? readValue(node, prov, addr, subLine)
: fit(readValue(node, prov, addr, subLine), COL_VALUE);
return ind + type + SEP + name + SEP + val + cmtSuffix;
}
@@ -674,4 +705,9 @@ QString validateBaseAddress(const QString& text) {
return AddressParser::validate(s);
}
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) {
QString ind = indent(depth);
return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value);
}
} // namespace rcx::fmt

View File

@@ -315,7 +315,8 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
&& !ctx.forwardDeclared.contains(child.refId)) {
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
if (fwdKw == QStringLiteral("enum")) fwdKw = QStringLiteral("struct");
if (fwdKw == QStringLiteral("enum") && ctx.tree.nodes[refIdx].enumMembers.isEmpty())
fwdKw = QStringLiteral("struct");
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
ctx.forwardDeclared.insert(child.refId);
}
@@ -327,7 +328,21 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
QString kw = node.resolvedClassKeyword();
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
// Enum with members: emit as proper C enum
if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
ctx.output += QStringLiteral("enum %1 {\n").arg(typeName);
for (const auto& m : node.enumMembers) {
ctx.output += QStringLiteral(" %1 = %2,\n")
.arg(sanitizeIdent(m.first))
.arg(m.second);
}
ctx.output += QStringLiteral("};\n\n");
ctx.visiting.remove(structId);
return;
}
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum without members: fallback
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
emitStructBody(ctx, structId);

View File

@@ -234,6 +234,7 @@ struct PdbCtx {
QHash<uint32_t, uint64_t> typeCache; // typeIndex → nodeId
uint64_t importUDT(uint32_t typeIndex);
uint64_t importEnum(uint32_t typeIndex);
void importFieldList(uint32_t fieldListIndex, uint64_t parentId);
void importMemberType(uint32_t typeIndex, int offset, const QString& name, uint64_t parentId);
@@ -300,6 +301,59 @@ uint64_t PdbCtx::importUDT(uint32_t typeIndex) {
return nodeId;
}
uint64_t PdbCtx::importEnum(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 || rec->header.kind != TRK::LF_ENUM) return 0;
if (rec->data.LF_ENUM.property.fwdref) return 0;
QString qname = rec->data.LF_ENUM.name
? QString::fromUtf8(rec->data.LF_ENUM.name)
: QStringLiteral("<anon>");
Node s;
s.kind = NodeKind::Struct;
s.name = qname;
s.structTypeName = qname;
s.classKeyword = QStringLiteral("enum");
s.parentId = 0;
s.collapsed = true;
// Extract enum members from field list
uint32_t fieldListIndex = rec->data.LF_ENUM.field;
const auto* flRec = tt->get(fieldListIndex);
if (flRec && flRec->header.kind == TRK::LF_FIELDLIST) {
auto maxSize = flRec->header.size - sizeof(uint16_t);
for (size_t i = 0; i < maxSize; ) {
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
reinterpret_cast<const uint8_t*>(&flRec->data.LF_FIELD.list) + i);
if (field->kind != TRK::LF_ENUMERATE) break;
int64_t val = static_cast<int64_t>(leafValue(
field->data.LF_ENUMERATE.value,
field->data.LF_ENUMERATE.lfEasy.kind));
const char* eName = leafName(
field->data.LF_ENUMERATE.value,
field->data.LF_ENUMERATE.lfEasy.kind);
if (eName)
s.enumMembers.append({QString::fromUtf8(eName), val});
i += static_cast<size_t>(eName - reinterpret_cast<const char*>(field));
i += strnlen(eName, maxSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
}
int idx = tree.addNode(s);
uint64_t nodeId = tree.nodes[idx].id;
typeCache[typeIndex] = 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;
@@ -707,8 +761,9 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
}
case TRK::LF_ENUM: {
// Map enum to its underlying integer type
// Map enum to its underlying integer type, link to enum definition
uint32_t utype = rec->data.LF_ENUM.utype;
uint64_t enumNodeId = importEnum(typeIndex);
Node n;
if (utype < tt->firstIndex()) {
n.kind = mapPrimitiveType(utype);
@@ -718,6 +773,7 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.refId = enumNodeId;
tree.addNode(n);
break;
}
@@ -823,14 +879,27 @@ QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
rec->header.kind == TRK::LF_CLASS ||
rec->header.kind == TRK::LF_UNION);
if (!isUDT) continue;
bool isEnum = (rec->header.kind == TRK::LF_ENUM);
if (!isUDT && !isEnum) continue;
const char* name = nullptr;
uint16_t fieldCount = 0;
bool isUnion = false;
uint64_t size = 0;
if (rec->header.kind == TRK::LF_UNION) {
if (isEnum) {
if (rec->data.LF_ENUM.property.fwdref) continue;
fieldCount = rec->data.LF_ENUM.count;
name = rec->data.LF_ENUM.name;
// Size from underlying type
uint32_t ut = rec->data.LF_ENUM.utype;
if (ut < tt.firstIndex()) {
NodeKind ek = mapPrimitiveType(ut);
size = sizeForKind(ek);
} else {
size = 4;
}
} else if (rec->header.kind == TRK::LF_UNION) {
if (rec->data.LF_UNION.property.fwdref) continue;
isUnion = true;
fieldCount = rec->data.LF_UNION.count;
@@ -856,6 +925,7 @@ QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg
info.size = size;
info.childCount = fieldCount;
info.isUnion = isUnion;
info.isEnum = isEnum;
result.append(info);
}
@@ -876,7 +946,12 @@ NodeTree importPdbSelected(const QString& pdbPath,
int total = typeIndices.size();
for (int i = 0; i < total; i++) {
ctx.importUDT(typeIndices[i]);
uint32_t ti = typeIndices[i];
const auto* rec = pdb.typeTable->get(ti);
if (rec && rec->header.kind == TRK::LF_ENUM)
ctx.importEnum(ti);
else
ctx.importUDT(ti);
if (progressCb && !progressCb(i + 1, total)) {
if (errorMsg) *errorMsg = QStringLiteral("Import cancelled");
return ctx.tree; // return partial result

View File

@@ -7,10 +7,11 @@ namespace rcx {
struct PdbTypeInfo {
uint32_t typeIndex; // TPI type index
QString name; // struct/class/union name
QString name; // struct/class/union/enum name
uint64_t size; // sizeof in bytes
int childCount; // direct member count
bool isUnion; // union vs struct/class
bool isEnum = false; // enum type
};
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).

View File

@@ -1,5 +1,6 @@
#include "import_source.h"
#include <QHash>
#include <QSet>
#include <QVector>
#include <QRegularExpression>
#include <QDebug>
@@ -285,13 +286,16 @@ struct ParsedField {
int commentOffset = -1; // from // 0xNN (-1 = none)
int bitfieldWidth = -1; // -1 = not a bitfield
QString pointerTarget; // for Type* -> the type name
bool isUnion = false; // union container
QVector<ParsedField> unionMembers; // children of union
};
struct ParsedStruct {
QString name;
QString keyword; // "struct" or "class"
QString keyword; // "struct", "class", or "enum"
QVector<ParsedField> fields;
int declaredSize = -1; // from static_assert
QVector<QPair<QString, int64_t>> enumValues; // for keyword="enum"
};
struct PendingRef {
@@ -378,8 +382,7 @@ struct Parser {
} else if (checkIdent("typedef")) {
parseTypedef();
} else if (checkIdent("enum")) {
skipToSemiOrBrace();
if (check(TokKind::RBrace)) { advance(); match(TokKind::Semi); }
parseEnumDef();
} else if (peek().kind == TokKind::Hash) {
// preprocessor (shouldn't reach here if tokenizer skipped them)
advance();
@@ -464,12 +467,18 @@ struct Parser {
// Might be "struct TypeName fieldName;" - fall through to field parsing
}
// Union: pick first member only
// Union: create container with all members
if (checkIdent("union")) {
parseUnion(ps);
continue;
}
// Enum definition inside struct
if (checkIdent("enum")) {
parseEnumDef();
continue;
}
// Static assert inside struct
if (checkIdent("static_assert")) {
parseStaticAssert();
@@ -489,33 +498,76 @@ struct Parser {
void parseUnion(ParsedStruct& ps) {
advance(); // skip "union"
// Optional union name
// Optional union tag name (before {)
if (check(TokKind::Ident) && peek(1).kind == TokKind::LBrace) {
advance(); // skip union name
advance(); // skip union tag name
}
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
// Parse first member of union
bool gotFirst = false;
// Parse ALL members of the union
ParsedField unionField;
unionField.isUnion = true;
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
if (!gotFirst) {
ParsedField field;
if (parseField(field)) {
ps.fields.append(field);
gotFirst = true;
} else {
advance();
// Handle nested unions inside this union
if (checkIdent("union")) {
// Recurse: create a sub-union ParsedStruct temporarily,
// then steal its fields as a nested union member
ParsedStruct tmp;
parseUnion(tmp);
for (auto& f : tmp.fields)
unionField.unionMembers.append(f);
continue;
}
// Handle anonymous struct inside union: struct { ... };
if ((checkIdent("struct") || checkIdent("class")) && peek(1).kind == TokKind::LBrace) {
advance(); // skip "struct"
advance(); // skip "{"
int depth = 1;
while (peek().kind != TokKind::Eof && depth > 0) {
if (peek().kind == TokKind::LBrace) depth++;
else if (peek().kind == TokKind::RBrace) depth--;
if (depth > 0) advance();
}
if (check(TokKind::RBrace)) advance();
if (check(TokKind::Ident)) advance(); // optional field name
match(TokKind::Semi);
continue;
}
// Handle nested named struct definition inside union
if ((checkIdent("struct") || checkIdent("class")) &&
peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) {
parseStructOrForward();
continue;
}
ParsedField field;
if (parseField(field)) {
unionField.unionMembers.append(field);
} else {
// Skip remaining union members
skipToSemiOrBrace();
advance();
}
}
match(TokKind::RBrace);
// Optional field name after union close
if (check(TokKind::Ident)) advance();
// Optional field name after union close: union { ... } u3;
if (check(TokKind::Ident)) {
unionField.name = advance().text;
}
match(TokKind::Semi);
// Determine offset from first member with a known offset
for (const auto& m : unionField.unionMembers) {
if (m.commentOffset >= 0) {
unionField.commentOffset = m.commentOffset;
break;
}
}
ps.fields.append(unionField);
}
bool parseField(ParsedField& field) {
@@ -719,6 +771,90 @@ struct Parser {
}
match(TokKind::Semi);
}
void parseEnumDef() {
advance(); // skip "enum"
// Optional "class" or "struct" (enum class)
if (checkIdent("class") || checkIdent("struct"))
advance();
// Optional name
QString name;
if (check(TokKind::Ident) && peek(1).kind != TokKind::Semi) {
// Could be: enum Name { ... }; or enum Name : Type { ... };
// But NOT: enum Name; (forward decl) or enum Name field; (field usage)
if (peek(1).kind == TokKind::LBrace || peek(1).kind == TokKind::Colon) {
name = advance().text;
} else {
// Not an enum definition — revert. This might be a field like "enum Foo bar;"
return;
}
}
// Optional underlying type: enum Name : uint8_t { ... }
if (check(TokKind::Colon)) {
advance();
parseTypeName(); // skip underlying type
}
// Forward declaration: enum Name;
if (check(TokKind::Semi)) {
advance();
return;
}
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
ParsedStruct ps;
ps.name = name;
ps.keyword = QStringLiteral("enum");
// Parse enum members: Name [= Value], ...
int64_t nextValue = 0;
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
if (!check(TokKind::Ident)) { advance(); continue; }
QString memberName = advance().text;
int64_t memberValue = nextValue;
if (check(TokKind::Equals)) {
advance();
// Parse value: could be number, negative number, or expression
bool negative = false;
if (peek().kind == TokKind::Other && peek().text == QStringLiteral("-")) {
negative = true;
advance();
}
if (check(TokKind::Number)) {
bool ok;
QString numText = peek().text;
if (numText.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
memberValue = numText.mid(2).toLongLong(&ok, 16);
else
memberValue = numText.toLongLong(&ok);
if (negative) memberValue = -memberValue;
advance();
} else {
// Complex expression — skip to comma or brace
while (peek().kind != TokKind::Comma &&
peek().kind != TokKind::RBrace &&
peek().kind != TokKind::Eof)
advance();
}
}
ps.enumValues.append({memberName, memberValue});
nextValue = memberValue + 1;
// Skip comma between members
match(TokKind::Comma);
}
match(TokKind::RBrace);
match(TokKind::Semi);
if (!ps.name.isEmpty())
structs.append(ps);
}
};
// ── Padding field detection ──
@@ -758,6 +894,305 @@ static void emitHexPadding(NodeTree& tree, uint64_t parentId, int offset, int si
}
}
// ── Bitfield grouping: emit a single hex node covering consecutive bitfields ──
static void emitBitfieldGroup(NodeTree& tree, uint64_t parentId, int offset, int totalBits) {
int bytes = (totalBits + 7) / 8;
// Round up to nearest power-of-2 hex node
NodeKind hexKind;
if (bytes <= 1) hexKind = NodeKind::Hex8;
else if (bytes <= 2) hexKind = NodeKind::Hex16;
else if (bytes <= 4) hexKind = NodeKind::Hex32;
else hexKind = NodeKind::Hex64;
Node n;
n.kind = hexKind;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
}
// ── NodeTree builder: recursive field emitter ──
struct BuildContext {
NodeTree& tree;
const QHash<QString, TypeInfo>& typeTable;
QHash<QString, uint64_t>& classIds;
QVector<PendingRef>& pendingRefs;
bool useCommentOffsets;
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
};
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
const QVector<ParsedField>& fields) {
int computedOffset = 0;
for (int fi = 0; fi < fields.size(); fi++) {
const auto& field = fields[fi];
// Bitfield group: consume consecutive bitfields, emit single hex node
if (field.bitfieldWidth >= 0) {
int groupOffset;
if (ctx.useCommentOffsets && field.commentOffset >= 0)
groupOffset = field.commentOffset - baseOffset;
else
groupOffset = computedOffset;
int totalBits = 0;
while (fi < fields.size() && fields[fi].bitfieldWidth >= 0) {
totalBits += fields[fi].bitfieldWidth;
fi++;
}
fi--; // compensate for outer loop increment
if (totalBits > 0)
emitBitfieldGroup(ctx.tree, parentId, groupOffset, totalBits);
int bytes = (totalBits + 7) / 8;
int nodeSize = (bytes <= 1) ? 1 : (bytes <= 2) ? 2 : (bytes <= 4) ? 4 : 8;
computedOffset = groupOffset + nodeSize;
continue;
}
// Union container field
if (field.isUnion) {
int unionOffset;
if (ctx.useCommentOffsets && field.commentOffset >= 0)
unionOffset = field.commentOffset - baseOffset;
else
unionOffset = computedOffset;
Node unionNode;
unionNode.kind = NodeKind::Struct;
unionNode.classKeyword = QStringLiteral("union");
unionNode.name = field.name;
unionNode.parentId = parentId;
unionNode.offset = unionOffset;
unionNode.collapsed = true;
int unionIdx = ctx.tree.addNode(unionNode);
uint64_t unionId = ctx.tree.nodes[unionIdx].id;
// Build each union member independently so each starts at offset 0
int absUnionOffset = baseOffset + unionOffset;
for (const auto& member : field.unionMembers) {
QVector<ParsedField> single;
single.append(member);
buildFields(ctx, unionId, absUnionOffset, single);
}
// Advance computed offset past the union (max member size)
int unionSpan = ctx.tree.structSpan(unionId);
computedOffset = unionOffset + (unionSpan > 0 ? unionSpan : 0);
continue;
}
int fieldOffset;
if (ctx.useCommentOffsets && field.commentOffset >= 0)
fieldOffset = field.commentOffset - baseOffset;
else
fieldOffset = computedOffset;
// Resolve type
auto typeIt = ctx.typeTable.find(field.typeName);
bool knownType = typeIt != ctx.typeTable.end();
// Pointer field
if (field.isPointer) {
Node n;
n.kind = NodeKind::Pointer64;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
n.collapsed = true;
int nodeIdx = ctx.tree.addNode(n);
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
if (!field.pointerTarget.isEmpty() &&
field.pointerTarget != QStringLiteral("void")) {
ctx.pendingRefs.append({nodeId, field.pointerTarget});
}
computedOffset = fieldOffset + 8;
continue;
}
// Enum-typed field: emit as UInt32 with refId to enum definition
if (!knownType && ctx.enumNames.contains(field.typeName)) {
int elemSize = 4;
NodeKind elemKind = NodeKind::UInt32;
if (!field.arraySizes.isEmpty()) {
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n;
n.kind = NodeKind::Array;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
n.arrayLen = totalElements;
n.elementKind = elemKind;
ctx.tree.addNode(n);
computedOffset = fieldOffset + totalElements * elemSize;
} else {
Node n;
n.kind = elemKind;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
int nodeIdx = ctx.tree.addNode(n);
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
ctx.pendingRefs.append({nodeId, field.typeName});
computedOffset = fieldOffset + elemSize;
}
continue;
}
// Determine base type info
NodeKind baseKind = NodeKind::Hex8;
int baseSize = 1;
bool isStructType = false;
if (knownType) {
baseKind = typeIt->kind;
baseSize = typeIt->size;
} else {
isStructType = true;
}
// Padding fields
if (isPaddingName(field.name) && !field.arraySizes.isEmpty()) {
int totalSize = baseSize;
for (int dim : field.arraySizes) totalSize *= (dim > 0 ? dim : 1);
emitHexPadding(ctx.tree, parentId, fieldOffset, totalSize);
computedOffset = fieldOffset + totalSize;
continue;
}
// Array fields
if (!field.arraySizes.isEmpty() && !isStructType) {
int firstDim = field.arraySizes.value(0, 1);
if (firstDim <= 0) firstDim = 1;
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
field.typeName == QStringLiteral("char")) {
Node n;
n.kind = NodeKind::UTF8;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
n.strLen = firstDim;
ctx.tree.addNode(n);
computedOffset = fieldOffset + firstDim;
continue;
}
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
Node n;
n.kind = NodeKind::UTF16;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
n.strLen = firstDim;
ctx.tree.addNode(n);
computedOffset = fieldOffset + firstDim * 2;
continue;
}
if (baseKind == NodeKind::Float && field.arraySizes.size() == 1) {
if (firstDim == 2) {
Node n; n.kind = NodeKind::Vec2; n.name = field.name;
n.parentId = parentId; n.offset = fieldOffset;
ctx.tree.addNode(n); computedOffset = fieldOffset + 8; continue;
}
if (firstDim == 3) {
Node n; n.kind = NodeKind::Vec3; n.name = field.name;
n.parentId = parentId; n.offset = fieldOffset;
ctx.tree.addNode(n); computedOffset = fieldOffset + 12; continue;
}
if (firstDim == 4) {
Node n; n.kind = NodeKind::Vec4; n.name = field.name;
n.parentId = parentId; n.offset = fieldOffset;
ctx.tree.addNode(n); computedOffset = fieldOffset + 16; continue;
}
}
if (baseKind == NodeKind::Float && field.arraySizes.size() == 2 &&
field.arraySizes[0] == 4 && field.arraySizes[1] == 4) {
Node n; n.kind = NodeKind::Mat4x4; n.name = field.name;
n.parentId = parentId; n.offset = fieldOffset;
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
}
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n;
n.kind = NodeKind::Array;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
n.arrayLen = totalElements;
n.elementKind = baseKind;
ctx.tree.addNode(n);
computedOffset = fieldOffset + totalElements * baseSize;
continue;
}
// Struct-type field
if (isStructType) {
if (!field.arraySizes.isEmpty()) {
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n;
n.kind = NodeKind::Array;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
n.arrayLen = totalElements;
n.elementKind = NodeKind::Struct;
n.structTypeName = field.typeName;
n.collapsed = true;
int nodeIdx = ctx.tree.addNode(n);
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
ctx.pendingRefs.append({nodeId, field.typeName});
continue;
}
Node n;
n.kind = NodeKind::Struct;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
n.structTypeName = field.typeName;
n.collapsed = true;
int nodeIdx = ctx.tree.addNode(n);
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
ctx.pendingRefs.append({nodeId, field.typeName});
continue;
}
// Simple primitive field
Node n;
n.kind = baseKind;
n.name = field.name;
n.parentId = parentId;
n.offset = fieldOffset;
ctx.tree.addNode(n);
computedOffset = fieldOffset + baseSize;
}
}
// ── Check if any field (or union member) has a comment offset ──
static bool hasAnyCommentOffset(const QVector<ParsedField>& fields) {
for (const auto& f : fields) {
if (f.commentOffset >= 0) return true;
if (f.isUnion && hasAnyCommentOffset(f.unionMembers)) return true;
}
return false;
}
// ── NodeTree builder ──
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
@@ -775,7 +1210,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
parser.parse();
if (parser.structs.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("No struct definitions found");
if (errorMsg) *errorMsg = QStringLiteral("No struct or enum definitions found");
return {};
}
@@ -798,13 +1233,19 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
// Determine offset mode: if ANY field in ANY struct has a comment offset, use comment mode
bool useCommentOffsets = false;
for (const auto& ps : parser.structs) {
for (const auto& f : ps.fields) {
if (f.commentOffset >= 0) { useCommentOffsets = true; break; }
}
if (useCommentOffsets) break;
if (hasAnyCommentOffset(ps.fields)) { useCommentOffsets = true; break; }
}
// Build nodes for each struct
// Collect enum type names for field-type detection
QSet<QString> enumNames;
for (const auto& ps : parser.structs) {
if (ps.keyword == QStringLiteral("enum") && !ps.name.isEmpty())
enumNames.insert(ps.name);
}
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames};
// Build nodes for each struct/enum
for (const auto& ps : parser.structs) {
Node structNode;
structNode.kind = NodeKind::Struct;
@@ -815,222 +1256,21 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
structNode.offset = 0;
structNode.collapsed = true;
// Enum: store members directly on the node, no child fields
if (ps.keyword == QStringLiteral("enum")) {
structNode.enumMembers = ps.enumValues;
int idx = tree.addNode(structNode);
uint64_t nodeId = tree.nodes[idx].id;
if (!ps.name.isEmpty())
classIds[ps.name] = nodeId;
continue;
}
int structIdx = tree.addNode(structNode);
uint64_t structId = tree.nodes[structIdx].id;
classIds[ps.name] = structId;
int computedOffset = 0;
for (const auto& field : ps.fields) {
// Skip bitfields
if (field.bitfieldWidth >= 0) continue;
int fieldOffset;
if (useCommentOffsets && field.commentOffset >= 0)
fieldOffset = field.commentOffset;
else
fieldOffset = computedOffset;
// Resolve type
auto typeIt = typeTable.find(field.typeName);
bool knownType = typeIt != typeTable.end();
// Pointer field
if (field.isPointer) {
Node n;
n.kind = NodeKind::Pointer64;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
n.collapsed = true;
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
// If target is not void and not a primitive, defer resolution
if (!field.pointerTarget.isEmpty() &&
field.pointerTarget != QStringLiteral("void")) {
pendingRefs.append({nodeId, field.pointerTarget});
}
computedOffset = fieldOffset + 8; // pointer size
continue;
}
// Determine base type info
NodeKind baseKind = NodeKind::Hex8;
int baseSize = 1;
bool isStructType = false;
if (knownType) {
baseKind = typeIt->kind;
baseSize = typeIt->size;
} else {
// Unknown type = assume struct reference
isStructType = true;
}
// Padding fields: name-based detection
if (isPaddingName(field.name) && !field.arraySizes.isEmpty()) {
int totalSize = baseSize;
for (int dim : field.arraySizes) totalSize *= (dim > 0 ? dim : 1);
emitHexPadding(tree, structId, fieldOffset, totalSize);
computedOffset = fieldOffset + totalSize;
continue;
}
// Array fields
if (!field.arraySizes.isEmpty() && !isStructType) {
int firstDim = field.arraySizes.value(0, 1);
if (firstDim <= 0) firstDim = 1;
// Special: char[N] -> UTF8
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
field.typeName == QStringLiteral("char")) {
Node n;
n.kind = NodeKind::UTF8;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
n.strLen = firstDim;
tree.addNode(n);
computedOffset = fieldOffset + firstDim;
continue;
}
// Special: wchar_t[N] -> UTF16
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
Node n;
n.kind = NodeKind::UTF16;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
n.strLen = firstDim;
tree.addNode(n);
computedOffset = fieldOffset + firstDim * 2;
continue;
}
// Special: float[2] -> Vec2, float[3] -> Vec3, float[4] -> Vec4
if (baseKind == NodeKind::Float && field.arraySizes.size() == 1) {
if (firstDim == 2) {
Node n;
n.kind = NodeKind::Vec2;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
tree.addNode(n);
computedOffset = fieldOffset + 8;
continue;
}
if (firstDim == 3) {
Node n;
n.kind = NodeKind::Vec3;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
tree.addNode(n);
computedOffset = fieldOffset + 12;
continue;
}
if (firstDim == 4) {
Node n;
n.kind = NodeKind::Vec4;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
tree.addNode(n);
computedOffset = fieldOffset + 16;
continue;
}
}
// Special: float[4][4] -> Mat4x4
if (baseKind == NodeKind::Float && field.arraySizes.size() == 2 &&
field.arraySizes[0] == 4 && field.arraySizes[1] == 4) {
Node n;
n.kind = NodeKind::Mat4x4;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
tree.addNode(n);
computedOffset = fieldOffset + 64;
continue;
}
// Generic array
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n;
n.kind = NodeKind::Array;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
n.arrayLen = totalElements;
n.elementKind = baseKind;
tree.addNode(n);
computedOffset = fieldOffset + totalElements * baseSize;
continue;
}
// Struct-type field (embedded struct or array of structs)
if (isStructType) {
if (!field.arraySizes.isEmpty()) {
// Array of structs
int totalElements = 1;
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n;
n.kind = NodeKind::Array;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
n.arrayLen = totalElements;
n.elementKind = NodeKind::Struct;
n.structTypeName = field.typeName;
n.collapsed = true;
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, field.typeName});
// For computed offsets: we don't know struct size yet, use 0
// The offset will be approximate for unknown struct sizes
if (!useCommentOffsets) {
// Try to estimate from same-file structs
// Can't know size yet since we may not have parsed it
// Just advance by 0 (will be corrected by comment offsets if present)
}
continue;
}
// Embedded struct
Node n;
n.kind = NodeKind::Struct;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
n.structTypeName = field.typeName;
n.collapsed = true;
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, field.typeName});
// Don't advance computed offset for unknown struct size
continue;
}
// Simple primitive field
Node n;
n.kind = baseKind;
n.name = field.name;
n.parentId = structId;
n.offset = fieldOffset;
tree.addNode(n);
computedOffset = fieldOffset + baseSize;
}
buildFields(ctx, structId, 0, ps.fields);
// Apply static_assert size: add tail padding if needed
auto sizeIt = parser.sizeAsserts.find(ps.name);

View File

@@ -312,6 +312,18 @@ public:
}
}
}
// Tree view items — use theme.hover for selection instead of blue
if (element == CE_ItemViewItem) {
if (auto* vi = qstyleoption_cast<const QStyleOptionViewItem*>(opt)) {
QStyleOptionViewItem patched = *vi;
patched.palette.setColor(QPalette::Highlight,
vi->palette.color(QPalette::Mid)); // theme.hover
patched.palette.setColor(QPalette::HighlightedText,
vi->palette.color(QPalette::Text));
QProxyStyle::drawControl(element, &patched, p, w);
return;
}
}
QProxyStyle::drawControl(element, opt, p, w);
}
};
@@ -427,7 +439,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
// Restore menu bar title case setting (after menus are created)
{
QSettings s("Reclass", "Reclass");
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", true).toBool());
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", false).toBool());
if (s.value("showIcon", false).toBool())
m_titleBar->setShowIcon(true);
}
@@ -568,6 +580,16 @@ void MainWindow::createMenus() {
themeMenu->addSeparator();
Qt5Qt6AddAction(themeMenu, "Edit Theme...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::editTheme);
view->addSeparator();
auto* actCompact = view->addAction("Compact &Columns");
actCompact->setCheckable(true);
actCompact->setChecked(settings.value("compactColumns", true).toBool());
connect(actCompact, &QAction::triggered, this, [this](bool checked) {
QSettings("Reclass", "Reclass").setValue("compactColumns", checked);
for (auto& tab : m_tabs)
tab.ctrl->setCompactColumns(checked);
});
view->addSeparator();
view->addAction(m_workspaceDock->toggleViewAction());
@@ -988,6 +1010,9 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
// Create the initial split pane
tab.panes.append(createSplitPane(tab));
// Apply global compact columns setting to new tab
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
// Give every controller the shared document list for cross-tab type visibility
ctrl->setProjectDocuments(&m_allDocs);
rebuildAllDocs();
@@ -1101,6 +1126,76 @@ static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QStri
n.offset = i * 8;
tree.addNode(n);
}
// Default project: add an example enum and a class with a union
if (classKeyword.isEmpty()) {
// ── Example enum: _POOL_TYPE ──
{
Node e;
e.kind = NodeKind::Struct;
e.name = QStringLiteral("_POOL_TYPE");
e.structTypeName = QStringLiteral("_POOL_TYPE");
e.classKeyword = QStringLiteral("enum");
e.parentId = 0;
e.collapsed = false;
e.enumMembers = {
{QStringLiteral("NonPagedPool"), 0},
{QStringLiteral("PagedPool"), 1},
{QStringLiteral("NonPagedPoolMustSucceed"), 2},
{QStringLiteral("DontUseThisType"), 3},
{QStringLiteral("NonPagedPoolCacheAligned"), 4},
{QStringLiteral("PagedPoolCacheAligned"), 5},
};
tree.addNode(e);
}
// ── Example class with a union: _SAMPLE_OBJECT ──
{
Node cls;
cls.kind = NodeKind::Struct;
cls.name = QStringLiteral("sample");
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
cls.classKeyword = QStringLiteral("class");
cls.parentId = 0;
cls.offset = 0;
int ci = tree.addNode(cls);
uint64_t clsId = tree.nodes[ci].id;
// Field: uint32_t Type at offset 0
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
// Field: uint32_t Size at offset 4
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
// Union at offset 8
{
Node u;
u.kind = NodeKind::Struct;
u.name = QStringLiteral("Data");
u.structTypeName = QStringLiteral("Data");
u.classKeyword = QStringLiteral("union");
u.parentId = clsId;
u.offset = 8;
int ui = tree.addNode(u);
uint64_t uId = tree.nodes[ui].id;
// Union member: uint64_t AsLong
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
n.parentId = uId; n.offset = 0; tree.addNode(n); }
// Union member: void* AsPointer
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
// Union member: float[2] AsFloat2
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
n.parentId = uId; n.offset = 0; tree.addNode(n); }
}
// Field: void* Next at offset 16
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
}
}
}
void MainWindow::newClass() {
@@ -1185,6 +1280,73 @@ static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
n.offset = off;
tree.addNode(n);
}
// ── Example enum: _POOL_TYPE ──
{
Node e;
e.kind = NodeKind::Struct;
e.name = QStringLiteral("_POOL_TYPE");
e.structTypeName = QStringLiteral("_POOL_TYPE");
e.classKeyword = QStringLiteral("enum");
e.parentId = 0;
e.collapsed = false;
e.enumMembers = {
{QStringLiteral("NonPagedPool"), 0},
{QStringLiteral("PagedPool"), 1},
{QStringLiteral("NonPagedPoolMustSucceed"), 2},
{QStringLiteral("DontUseThisType"), 3},
{QStringLiteral("NonPagedPoolCacheAligned"), 4},
{QStringLiteral("PagedPoolCacheAligned"), 5},
};
tree.addNode(e);
}
// ── Example class with a union: _SAMPLE_OBJECT ──
{
Node cls;
cls.kind = NodeKind::Struct;
cls.name = QStringLiteral("sample");
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
cls.classKeyword = QStringLiteral("class");
cls.parentId = 0;
cls.offset = 0;
int ci = tree.addNode(cls);
uint64_t clsId = tree.nodes[ci].id;
// Field: uint32_t Type at offset 0
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
// Field: uint32_t Size at offset 4
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
// Union at offset 8
{
Node u;
u.kind = NodeKind::Struct;
u.name = QStringLiteral("Data");
u.structTypeName = QStringLiteral("Data");
u.classKeyword = QStringLiteral("union");
u.parentId = clsId;
u.offset = 8;
int ui = tree.addNode(u);
uint64_t uId = tree.nodes[ui].id;
// Union member: uint64_t AsLong
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
n.parentId = uId; n.offset = 0; tree.addNode(n); }
// Union member: void* AsPointer
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
// Union member: float[2] AsFloat2
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
n.parentId = uId; n.offset = 0; tree.addNode(n); }
}
// Field: void* Next at offset 16
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
}
}
void MainWindow::selfTest() {
@@ -1424,16 +1586,26 @@ void MainWindow::applyTheme(const Theme& theme) {
if (auto* w = findChild<QWidget*>("resizeGrip"))
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
// Workspace tree: text color matches menu bar
// Workspace tree: colors from theme (selection + text)
if (m_workspaceTree) {
QPalette tp = m_workspaceTree->palette();
tp.setColor(QPalette::Text, theme.textDim);
tp.setColor(QPalette::Highlight, theme.hover);
tp.setColor(QPalette::HighlightedText, theme.text);
m_workspaceTree->setPalette(tp);
}
// Dock titlebar: restyle label + close button
if (m_dockTitleLabel)
m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
// Dock titlebar: restyle via palette + close button
if (m_dockTitleLabel) {
QPalette lp = m_dockTitleLabel->palette();
lp.setColor(QPalette::WindowText, theme.textDim);
m_dockTitleLabel->setPalette(lp);
}
if (auto* titleBar = m_workspaceDock ? m_workspaceDock->titleBarWidget() : nullptr) {
QPalette tbPal = titleBar->palette();
tbPal.setColor(QPalette::Window, theme.backgroundAlt);
titleBar->setPalette(tbPal);
}
if (m_dockCloseBtn)
m_dockCloseBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
@@ -1873,6 +2045,7 @@ void MainWindow::importFromSource() {
m_mdiArea->closeAllSubWindows();
createTab(doc);
rebuildWorkspaceModel();
m_workspaceDock->show();
m_statusLabel->setText(QStringLiteral("Imported %1 classes from source").arg(classCount));
}
@@ -1922,6 +2095,7 @@ void MainWindow::importPdb() {
m_mdiArea->closeAllSubWindows();
createTab(doc);
rebuildWorkspaceModel();
m_workspaceDock->show();
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
.arg(classCount).arg(QFileInfo(pdbPath).fileName()));
}
@@ -2051,6 +2225,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
m_mdiArea->closeAllSubWindows();
auto* sub = createTab(doc);
rebuildWorkspaceModel();
m_workspaceDock->show();
int classCount = 0;
for (const auto& n : doc->tree.nodes)
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
@@ -2071,6 +2246,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
auto* sub = createTab(doc);
rebuildWorkspaceModel();
m_workspaceDock->show();
return sub;
}
@@ -2101,7 +2277,7 @@ void MainWindow::project_close(QMdiSubWindow* sub) {
// ── Workspace Dock ──
void MainWindow::createWorkspaceDock() {
m_workspaceDock = new QDockWidget("Project Tree", this);
m_workspaceDock = new QDockWidget("Project", this);
m_workspaceDock->setObjectName("WorkspaceDock");
m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
m_workspaceDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
@@ -2111,17 +2287,21 @@ void MainWindow::createWorkspaceDock() {
const auto& t = ThemeManager::instance().current();
auto* titleBar = new QWidget(m_workspaceDock);
titleBar->setAutoFillBackground(true);
{
QPalette tbPal = titleBar->palette();
tbPal.setColor(QPalette::Window, t.backgroundAlt);
titleBar->setPalette(tbPal);
}
auto* layout = new QHBoxLayout(titleBar);
layout->setContentsMargins(6, 2, 2, 2);
layout->setSpacing(0);
m_dockTitleLabel = new QLabel("Project Tree", titleBar);
m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(t.textDim.name()));
m_dockTitleLabel = new QLabel("Project", titleBar);
{
QString fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
m_dockTitleLabel->setFont(f);
QPalette lp = m_dockTitleLabel->palette();
lp.setColor(QPalette::WindowText, t.textDim);
m_dockTitleLabel->setPalette(lp);
}
layout->addWidget(m_dockTitleLabel);
@@ -2150,6 +2330,16 @@ void MainWindow::createWorkspaceDock() {
m_workspaceTree->setExpandsOnDoubleClick(false);
m_workspaceTree->setMouseTracking(true);
// Override palette: selection + hover use theme colors (not default blue)
{
const auto& t = ThemeManager::instance().current();
QPalette tp = m_workspaceTree->palette();
tp.setColor(QPalette::Text, t.textDim);
tp.setColor(QPalette::Highlight, t.hover);
tp.setColor(QPalette::HighlightedText, t.text);
m_workspaceTree->setPalette(tp);
}
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
QModelIndex index = m_workspaceTree->indexAt(pos);

View File

@@ -34,7 +34,7 @@ private:
QToolButton* m_btnClose = nullptr;
Theme m_theme;
bool m_titleCase = true;
bool m_titleCase = false;
QToolButton* makeChromeButton(const QString& iconPath);
void toggleMaximize();

View File

@@ -1,4 +1,6 @@
#include <QtTest/QTest>
#include <QJsonDocument>
#include <QFile>
#include "core.h"
using namespace rcx;
@@ -1984,6 +1986,371 @@ private slots:
}
}
// ═════════════════════════════════════════════════════════════
// Union tests
// ═════════════════════════════════════════════════════════════
void testUnionHeaderShowsKeyword() {
// Union (Struct with classKeyword="union") should display "union" in header
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// Union container
Node u;
u.kind = NodeKind::Struct;
u.classKeyword = "union";
u.name = "u1";
u.parentId = rootId;
u.offset = 0;
int ui = tree.addNode(u);
uint64_t uId = tree.nodes[ui].id;
// Two members at offset 0
Node m1;
m1.kind = NodeKind::UInt32;
m1.name = "asInt";
m1.parentId = uId;
m1.offset = 0;
tree.addNode(m1);
Node m2;
m2.kind = NodeKind::Float;
m2.name = "asFloat";
m2.parentId = uId;
m2.offset = 0;
tree.addNode(m2);
NullProvider prov;
ComposeResult result = compose(tree, prov);
QStringList lines = result.text.split('\n');
// Find the union header line
int headerLine = -1;
for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].lineKind == LineKind::Header &&
result.meta[i].nodeKind == NodeKind::Struct &&
result.meta[i].depth == 1) {
headerLine = i;
break;
}
}
QVERIFY(headerLine >= 0);
QVERIFY2(lines[headerLine].contains("union"),
qPrintable("Union header should contain 'union': " + lines[headerLine]));
// Both members should be rendered at depth 2
int memberCount = 0;
for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth == 2)
memberCount++;
}
QCOMPARE(memberCount, 2);
// Both members share the same offset text (both at 0000)
QVector<int> memberLines;
for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth == 2)
memberLines.append(i);
}
QCOMPARE(memberLines.size(), 2);
QCOMPARE(result.meta[memberLines[0]].offsetText,
result.meta[memberLines[1]].offsetText);
}
void testUnionCollapsed() {
// Collapsed union should hide children
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node u;
u.kind = NodeKind::Struct;
u.classKeyword = "union";
u.name = "u1";
u.parentId = rootId;
u.offset = 0;
u.collapsed = true;
int ui = tree.addNode(u);
uint64_t uId = tree.nodes[ui].id;
Node m;
m.kind = NodeKind::UInt64;
m.name = "val";
m.parentId = uId;
m.offset = 0;
tree.addNode(m);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// No field lines at depth 2
int deepFields = 0;
for (const auto& lm : result.meta) {
if (lm.lineKind == LineKind::Field && lm.depth >= 2)
deepFields++;
}
QCOMPARE(deepFields, 0);
}
void testUnionStructSpan() {
// structSpan of a union = max(child offset + size), not sum
NodeTree tree;
Node u;
u.kind = NodeKind::Struct;
u.classKeyword = "union";
u.name = "U";
u.parentId = 0;
u.offset = 0;
int ui = tree.addNode(u);
uint64_t uId = tree.nodes[ui].id;
// 2-byte member
Node m1;
m1.kind = NodeKind::UInt16;
m1.name = "small";
m1.parentId = uId;
m1.offset = 0;
tree.addNode(m1);
// 8-byte member
Node m2;
m2.kind = NodeKind::UInt64;
m2.name = "big";
m2.parentId = uId;
m2.offset = 0;
tree.addNode(m2);
// structSpan = max(0+2, 0+8) = 8
QCOMPARE(tree.structSpan(uId), 8);
}
// ═════════════════════════════════════════════════════════════
// Enum compose tests
// ═════════════════════════════════════════════════════════════
void testEnumDisplaysMembers() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node e;
e.kind = NodeKind::Struct;
e.classKeyword = "enum";
e.name = "Color";
e.structTypeName = "Color";
e.parentId = rootId;
e.offset = 0;
e.collapsed = false;
e.enumMembers = {{"Red", 0}, {"Green", 1}, {"Blue", 2}};
tree.addNode(e);
NullProvider prov;
auto result = compose(tree, prov);
// Should have enum members in the text
QVERIFY(result.text.contains("Red"));
QVERIFY(result.text.contains("Green"));
QVERIFY(result.text.contains("Blue"));
QVERIFY(result.text.contains("= 0"));
QVERIFY(result.text.contains("= 2"));
// Header should contain "enum"
QVERIFY(result.text.contains("enum"));
}
void testEnumCollapsed() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node e;
e.kind = NodeKind::Struct;
e.classKeyword = "enum";
e.name = "Flags";
e.structTypeName = "Flags";
e.parentId = rootId;
e.offset = 0;
e.collapsed = true;
e.enumMembers = {{"A", 0}, {"B", 1}};
tree.addNode(e);
NullProvider prov;
auto result = compose(tree, prov);
// Collapsed: members should NOT appear
QVERIFY(!result.text.contains("= 0"));
QVERIFY(!result.text.contains("= 1"));
// But header should still show
QVERIFY(result.text.contains("enum"));
QVERIFY(result.text.contains("Flags"));
}
// ═════════════════════════════════════════════════════════════
// Compact columns: load EPROCESS.rcx and compare output
// ═════════════════════════════════════════════════════════════
void testCompactColumnsEprocess() {
// Load the EPROCESS example .rcx
// Try multiple paths: build dir examples, or source dir
QString rcxPath;
QStringList candidates = {
QCoreApplication::applicationDirPath() + "/examples/EPROCESS.rcx",
QCoreApplication::applicationDirPath() + "/../src/examples/EPROCESS.rcx",
};
for (const auto& c : candidates) {
if (QFile::exists(c)) { rcxPath = c; break; }
}
if (rcxPath.isEmpty())
QSKIP("EPROCESS.rcx not found");
QFile file(rcxPath);
QVERIFY2(file.open(QIODevice::ReadOnly),
qPrintable("Cannot open " + rcxPath));
QJsonDocument jdoc = QJsonDocument::fromJson(file.readAll());
NodeTree tree = NodeTree::fromJson(jdoc.object());
NullProvider prov;
// Compose WITHOUT compact (default)
ComposeResult normal = compose(tree, prov, 0, false);
// Compose WITH compact
ComposeResult compact = compose(tree, prov, 0, true);
// Compact typeW should be capped at kCompactTypeW (22)
QVERIFY2(compact.layout.typeW <= kCompactTypeW,
qPrintable(QString("compact typeW=%1, expected <= %2")
.arg(compact.layout.typeW).arg(kCompactTypeW)));
// Normal typeW should be wider (the _EPROCESS has long type names)
QVERIFY2(normal.layout.typeW > compact.layout.typeW,
qPrintable(QString("normal typeW=%1 should exceed compact typeW=%2")
.arg(normal.layout.typeW).arg(compact.layout.typeW)));
// Print side-by-side sample for visual inspection
QStringList normalLines = normal.text.split('\n');
QStringList compactLines = compact.text.split('\n');
qDebug() << "\n=== EPROCESS compact columns comparison ===";
qDebug() << "Normal typeW:" << normal.layout.typeW
<< " Compact typeW:" << compact.layout.typeW;
qDebug() << "Normal lines:" << normalLines.size()
<< " Compact lines:" << compactLines.size();
// Dump full output to files for visual diffing
{
QFile nf(QCoreApplication::applicationDirPath() + "/../eprocess_normal.txt");
nf.open(QIODevice::WriteOnly);
nf.write(normal.text.toUtf8());
}
{
QFile cf(QCoreApplication::applicationDirPath() + "/../eprocess_compact.txt");
cf.open(QIODevice::WriteOnly);
cf.write(compact.text.toUtf8());
}
qDebug() << "Wrote eprocess_normal.txt and eprocess_compact.txt";
// Show first 50 lines of each for quick inspection
qDebug() << "\n--- NORMAL (first 50 lines) ---";
for (int i = 0; i < qMin(50, normalLines.size()); ++i)
qDebug().noquote() << normalLines[i];
qDebug() << "\n--- COMPACT (first 50 lines) ---";
for (int i = 0; i < qMin(50, compactLines.size()); ++i)
qDebug().noquote() << compactLines[i];
// Overflow types should print in full (no truncation)
bool foundFull = false;
for (const QString& l : compactLines) {
if (l.contains("_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES")) {
foundFull = true;
break;
}
}
QVERIFY2(foundFull,
"Long type _PS_DYNAMIC_ENFORCED_ADDRESS_RANGES should print in full (no truncation)");
}
void testMmpfnRcxLoadsAndComposes() {
// Load the MMPFN.rcx example file and verify it composes without errors
// Try several paths to find the .rcx file
QString rcxPath;
for (const auto& p : {
QStringLiteral("../src/examples/MMPFN.rcx"),
QStringLiteral("../../src/examples/MMPFN.rcx"),
QStringLiteral("src/examples/MMPFN.rcx")}) {
if (QFile::exists(p)) { rcxPath = p; break; }
}
if (rcxPath.isEmpty()) {
QSKIP("MMPFN.rcx not found (run from build dir)");
}
QFile f(rcxPath);
QVERIFY2(f.open(QIODevice::ReadOnly), "Cannot open MMPFN.rcx");
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
QVERIFY(jdoc.isObject());
NodeTree tree = NodeTree::fromJson(jdoc.object());
QVERIFY2(tree.nodes.size() >= 60, "Expected at least 60 nodes");
// Check key top-level types exist
bool hasMmpfn = false, hasListEntry = false, hasMmpte = false;
for (const auto& n : tree.nodes) {
if (n.parentId == 0 && n.structTypeName == "_MMPFN") hasMmpfn = true;
if (n.parentId == 0 && n.structTypeName == "_LIST_ENTRY") hasListEntry = true;
if (n.parentId == 0 && n.structTypeName == "_MMPTE") hasMmpte = true;
}
QVERIFY2(hasMmpfn, "Missing _MMPFN top-level type");
QVERIFY2(hasListEntry, "Missing _LIST_ENTRY top-level type");
QVERIFY2(hasMmpte, "Missing _MMPTE top-level type");
// Compose and verify output
NullProvider prov;
ComposeResult result = compose(tree, prov, 0, false);
QStringList lines = result.text.split('\n');
QVERIFY2(lines.size() > 10, "Expected non-trivial compose output");
// Print first 30 lines for manual inspection
qDebug() << "=== MMPFN compose output ===";
for (int i = 0; i < qMin(30, lines.size()); ++i)
qDebug().noquote() << lines[i];
qDebug() << "... total lines:" << lines.size();
// Verify _MMPFN header appears in output
bool foundMmpfn = false;
for (const auto& l : lines) {
if (l.contains("_MMPFN")) { foundMmpfn = true; break; }
}
QVERIFY2(foundMmpfn, "Compose output should contain _MMPFN");
// Verify no M_CYCLE markers on any lines (all self-ref pointers are collapsed)
for (int i = 0; i < result.meta.size(); i++) {
bool hasCycle = (result.meta[i].markerMask & (1u << M_CYCLE)) != 0;
QVERIFY2(!hasCycle,
qPrintable(QString("Unexpected cycle marker on line %1").arg(i)));
}
}
};
QTEST_MAIN(TestCompose)

View File

@@ -49,7 +49,9 @@ private slots:
void forwardDeclaration();
// Union handling
void unionPickFirst();
void unionContainer();
void unionWithCommentOffsets();
void namedUnion();
// Padding fields
void paddingFieldExpansion();
@@ -69,11 +71,19 @@ private slots:
// Edge cases
void bitfieldSkipped();
void bitfieldWithOffsetsEmitsHex();
void hexArraySizes();
void windowsStylePEB();
void classKeyword();
void inheritanceSkipped();
// Enum tests
void enumBasic();
void enumAutoValues();
void enumHexValues();
void enumInStruct();
void enumClass();
// Round-trip test (requires generator.h)
void basicRoundTrip();
};
@@ -575,7 +585,7 @@ void TestImportSource::forwardDeclaration() {
QVERIFY(tree.nodes[kids[0]].refId != 0);
}
void TestImportSource::unionPickFirst() {
void TestImportSource::unionContainer() {
NodeTree tree = importFromSource(QStringLiteral(
"struct WithUnion {\n"
" union {\n"
@@ -586,12 +596,85 @@ void TestImportSource::unionPickFirst() {
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
// Should have 2 fields: asFloat (first union member) + after
// Should have 2 direct children: union container + after
QCOMPARE(kids.size(), 2);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("asFloat"));
// First child is the union container
const auto& unionNode = tree.nodes[kids[0]];
QCOMPARE(unionNode.kind, NodeKind::Struct);
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
QCOMPARE(unionNode.offset, 0);
// Union has 2 children, both at offset 0
auto unionKids = childrenOf(tree, unionNode.id);
QCOMPARE(unionKids.size(), 2);
QCOMPARE(tree.nodes[unionKids[0]].kind, NodeKind::Float);
QCOMPARE(tree.nodes[unionKids[0]].name, QStringLiteral("asFloat"));
QCOMPARE(tree.nodes[unionKids[0]].offset, 0);
QCOMPARE(tree.nodes[unionKids[1]].kind, NodeKind::UInt32);
QCOMPARE(tree.nodes[unionKids[1]].name, QStringLiteral("asInt"));
QCOMPARE(tree.nodes[unionKids[1]].offset, 0);
// structSpan of union = max member size = 4
QCOMPARE(tree.structSpan(unionNode.id), 4);
// after field follows the union at offset 4
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
QCOMPARE(tree.nodes[kids[1]].offset, 4);
}
void TestImportSource::unionWithCommentOffsets() {
NodeTree tree = importFromSource(QStringLiteral(
"struct S {\n"
" uint64_t a; // 0x0\n"
" union {\n"
" uint32_t x; // 0x8\n"
" float y; // 0x8\n"
" };\n"
" uint32_t b; // 0xC\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 3); // a + union + b
// Union at offset 0x8
const auto& unionNode = tree.nodes[kids[1]];
QCOMPARE(unionNode.kind, NodeKind::Struct);
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
QCOMPARE(unionNode.offset, 0x8);
// Union members at offset 0 (relative to union)
auto unionKids = childrenOf(tree, unionNode.id);
QCOMPARE(unionKids.size(), 2);
QCOMPARE(tree.nodes[unionKids[0]].offset, 0);
QCOMPARE(tree.nodes[unionKids[1]].offset, 0);
// b at 0xC
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
}
void TestImportSource::namedUnion() {
NodeTree tree = importFromSource(QStringLiteral(
"struct S {\n"
" union {\n"
" uint16_t shortVal;\n"
" uint64_t longVal;\n"
" } u3;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
const auto& unionNode = tree.nodes[kids[0]];
QCOMPARE(unionNode.kind, NodeKind::Struct);
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
QCOMPARE(unionNode.name, QStringLiteral("u3"));
auto unionKids = childrenOf(tree, unionNode.id);
QCOMPARE(unionKids.size(), 2);
// structSpan = max(2, 8) = 8
QCOMPARE(tree.structSpan(unionNode.id), 8);
}
void TestImportSource::paddingFieldExpansion() {
@@ -697,6 +780,7 @@ void TestImportSource::structPrefixOnType() {
}
void TestImportSource::bitfieldSkipped() {
// Bitfields emit a hex placeholder covering the group
NodeTree tree = importFromSource(QStringLiteral(
"struct BF {\n"
" uint32_t normal;\n"
@@ -706,10 +790,38 @@ void TestImportSource::bitfieldSkipped() {
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
// Bitfields should be skipped, only normal + after
QCOMPARE(kids.size(), 2);
// normal + Hex16 (16 bits → 2 bytes) + after
QCOMPARE(kids.size(), 3);
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
QCOMPARE(tree.nodes[kids[0]].offset, 0);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex16);
QCOMPARE(tree.nodes[kids[1]].offset, 4);
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
QCOMPARE(tree.nodes[kids[2]].offset, 6);
}
void TestImportSource::bitfieldWithOffsetsEmitsHex() {
NodeTree tree = importFromSource(QStringLiteral(
"struct BF2 {\n"
" uint32_t normal; // 0x0\n"
" ULONGLONG Valid : 1; // 0x4\n"
" ULONGLONG Dirty : 1; // 0x4\n"
" ULONGLONG PageFrameNumber : 36; // 0x4\n"
" ULONGLONG Reserved : 26; // 0x4\n"
" uint32_t after; // 0xC\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
// normal + hex64 (bitfield group: 64 bits) + after = 3
QCOMPARE(kids.size(), 3);
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
QCOMPARE(tree.nodes[kids[0]].offset, 0);
// Bitfield group emitted as Hex64 at offset 4
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
QCOMPARE(tree.nodes[kids[1]].offset, 4);
// after at 0xC
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
}
void TestImportSource::hexArraySizes() {
@@ -842,5 +954,78 @@ void TestImportSource::basicRoundTrip() {
}
}
// ── Enum tests ──
void TestImportSource::enumBasic() {
auto tree = importFromSource(QStringLiteral(
"enum Color { Red = 0, Green = 1, Blue = 2 };"));
QCOMPARE(countRoots(tree), 1);
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("enum"));
QCOMPARE(tree.nodes[0].structTypeName, QStringLiteral("Color"));
QCOMPARE(tree.nodes[0].enumMembers.size(), 3);
QCOMPARE(tree.nodes[0].enumMembers[0].first, QStringLiteral("Red"));
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0LL);
QCOMPARE(tree.nodes[0].enumMembers[1].first, QStringLiteral("Green"));
QCOMPARE(tree.nodes[0].enumMembers[1].second, 1LL);
QCOMPARE(tree.nodes[0].enumMembers[2].first, QStringLiteral("Blue"));
QCOMPARE(tree.nodes[0].enumMembers[2].second, 2LL);
}
void TestImportSource::enumAutoValues() {
auto tree = importFromSource(QStringLiteral(
"enum Flags { A, B, C };"));
QCOMPARE(tree.nodes[0].enumMembers.size(), 3);
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0LL);
QCOMPARE(tree.nodes[0].enumMembers[1].second, 1LL);
QCOMPARE(tree.nodes[0].enumMembers[2].second, 2LL);
}
void TestImportSource::enumHexValues() {
auto tree = importFromSource(QStringLiteral(
"enum { X = 0x10, Y = 0x20 };"));
// Anonymous enum has no name — parser skips it (unnamed enums are not added)
// Actually, let's use a named enum with hex values
tree = importFromSource(QStringLiteral(
"enum Hex { X = 0x10, Y = 0x20 };"));
QCOMPARE(tree.nodes[0].enumMembers.size(), 2);
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0x10LL);
QCOMPARE(tree.nodes[0].enumMembers[1].second, 0x20LL);
}
void TestImportSource::enumInStruct() {
auto tree = importFromSource(QStringLiteral(
"enum PoolType { NonPaged = 0, Paged = 1 };\n"
"struct Foo {\n"
" PoolType pool; //0x0\n"
" uint32_t size; //0x4\n"
"};"));
// Should have 2 roots: PoolType enum + Foo struct
QCOMPARE(countRoots(tree), 2);
// Find Foo struct
int fooIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == QStringLiteral("Foo")) { fooIdx = i; break; }
}
QVERIFY(fooIdx >= 0);
auto kids = childrenOf(tree, tree.nodes[fooIdx].id);
QCOMPARE(kids.size(), 2);
// First child should be UInt32 (enum mapped to int) with refId to PoolType
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("pool"));
QVERIFY(tree.nodes[kids[0]].refId != 0); // linked to enum definition
}
void TestImportSource::enumClass() {
auto tree = importFromSource(QStringLiteral(
"enum class Scope : uint8_t { A = 1, B = 2 };"));
QCOMPARE(countRoots(tree), 1);
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("enum"));
QCOMPARE(tree.nodes[0].structTypeName, QStringLiteral("Scope"));
QCOMPARE(tree.nodes[0].enumMembers.size(), 2);
QCOMPARE(tree.nodes[0].enumMembers[0].first, QStringLiteral("A"));
QCOMPARE(tree.nodes[0].enumMembers[0].second, 1LL);
}
QTEST_MAIN(TestImportSource)
#include "test_import_source.moc"