diff --git a/src/compose.cpp b/src/compose.cpp index c7f1cba..899896d 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -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 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); } diff --git a/src/controller.cpp b/src/controller.cpp index c23f6fb..0198da3 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -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& nodeIds) { + if (nodeIds.size() < 2) return; + + // Collect nodes and verify they share the same parent + QVector 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 subtree; }; + QVector 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 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 subtree; }; + QVector 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 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); diff --git a/src/controller.h b/src/controller.h index 525fc96..f00cf6e 100644 --- a/src/controller.h +++ b/src/controller.h @@ -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& nodeIndices); void batchChangeKind(const QVector& nodeIndices, NodeKind newKind); void deleteRootStruct(uint64_t structId); + void groupIntoUnion(const QSet& 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 m_selIds; int m_anchorLine = -1; bool m_suppressRefresh = false; + bool m_compactColumns = false; uint64_t m_viewRootId = 0; // ── Saved sources for quick-switch ── diff --git a/src/core.h b/src/core.h index 14999b3..54be174 100644 --- a/src/core.h +++ b/src/core.h @@ -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> 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 diff --git a/src/examples/EPROCESS.rcx b/src/examples/EPROCESS.rcx new file mode 100644 index 0000000..8ed4f59 --- /dev/null +++ b/src/examples/EPROCESS.rcx @@ -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} + ] +} diff --git a/src/examples/MMPFN.rcx b/src/examples/MMPFN.rcx new file mode 100644 index 0000000..f1dd3a9 --- /dev/null +++ b/src/examples/MMPFN.rcx @@ -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 + } + ] +} diff --git a/src/format.cpp b/src/format.cpp index 2b72385..9f6cd45 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -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: { (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: { (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 diff --git a/src/generator.cpp b/src/generator.cpp index 67b674d..b0b87b0 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -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); diff --git a/src/imports/import_pdb.cpp b/src/imports/import_pdb.cpp index d43cfaa..50f7a8f 100644 --- a/src/imports/import_pdb.cpp +++ b/src/imports/import_pdb.cpp @@ -234,6 +234,7 @@ struct PdbCtx { QHash 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(""); + + 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( + reinterpret_cast(&flRec->data.LF_FIELD.list) + i); + if (field->kind != TRK::LF_ENUMERATE) break; + + int64_t val = static_cast(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(eName - reinterpret_cast(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 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 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 diff --git a/src/imports/import_pdb.h b/src/imports/import_pdb.h index 0e70982..fd39aa8 100644 --- a/src/imports/import_pdb.h +++ b/src/imports/import_pdb.h @@ -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). diff --git a/src/imports/import_source.cpp b/src/imports/import_source.cpp index 57bed07..1fbaeaf 100644 --- a/src/imports/import_source.cpp +++ b/src/imports/import_source.cpp @@ -1,5 +1,6 @@ #include "import_source.h" #include +#include #include #include #include @@ -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 unionMembers; // children of union }; struct ParsedStruct { QString name; - QString keyword; // "struct" or "class" + QString keyword; // "struct", "class", or "enum" QVector fields; int declaredSize = -1; // from static_assert + QVector> 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& typeTable; + QHash& classIds; + QVector& pendingRefs; + bool useCommentOffsets; + QSet enumNames; // enum type names (emit as UInt32 + refId) +}; + +static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset, + const QVector& 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 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& 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 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); diff --git a/src/main.cpp b/src/main.cpp index 8e9040d..549da29 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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(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("resizeGrip")) static_cast(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); diff --git a/src/titlebar.h b/src/titlebar.h index 8cbc376..bb72801 100644 --- a/src/titlebar.h +++ b/src/titlebar.h @@ -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(); diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index 255aa35..1baae25 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -1,4 +1,6 @@ #include +#include +#include #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 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) diff --git a/tests/test_import_source.cpp b/tests/test_import_source.cpp index 29b779e..43cc30b 100644 --- a/tests/test_import_source.cpp +++ b/tests/test_import_source.cpp @@ -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"