From 4d35db224e1784fa95c25fdf2604293c99acf353 Mon Sep 17 00:00:00 2001 From: IChooChoose Date: Thu, 5 Feb 2026 06:26:00 -0700 Subject: [PATCH] Condensed array display + per-scope column widths + MIT license - Array element structs render without { } braces (condensed display) - [N] separators show element indices within arrays - Per-scope column width calculation (nested elements use tighter spacing) - Array headers show struct[N] for struct arrays - [N] separators are not interactive (no hover/click highlight) - Dynamic type column width (min 8, max 14) - PE32+ sample data with full headers, DataDirectory[16], SectionHeaders[4] - Added MIT license Co-Authored-By: Claude Opus 4.5 --- LICENSE | 21 ++ src/compose.cpp | 151 +++++++++++--- src/controller.cpp | 66 +++++-- src/core.h | 102 +++++++--- src/editor.cpp | 150 ++++++++++---- src/editor.h | 7 +- src/format.cpp | 46 +++-- src/main.cpp | 434 +++++++++++++++++++++++++---------------- tests/test_compose.cpp | 123 +----------- tests/test_core.cpp | 30 +-- tests/test_format.cpp | 27 +-- 11 files changed, 727 insertions(+), 430 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c565508 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 IChooChoose + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/compose.cpp b/src/compose.cpp index d01ff87..9ba195e 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -16,12 +16,24 @@ struct ComposeState { QSet visiting; // cycle detection for struct recursion QSet ptrVisiting; // cycle guard for pointer expansions int currentLine = 0; - int nameW = kColName; // effective name column width + int typeW = kColType; // global type column width (fallback) + int nameW = kColName; // global name column width (fallback) // Precomputed for O(1) lookups QHash> childMap; QVector absOffsets; // indexed by node index + // Per-scope column widths (containerId -> width for direct children) + QHash scopeTypeW; + QHash scopeNameW; + + int effectiveTypeW(uint64_t scopeId) const { + return scopeTypeW.value(scopeId, typeW); + } + int effectiveNameW(uint64_t scopeId) const { + return scopeNameW.value(scopeId, nameW); + } + void emitLine(const QString& lineText, LineMeta lm) { if (currentLine > 0) text += '\n'; // 3-char fold indicator column: " - " expanded, " + " collapsed, " " other @@ -93,9 +105,13 @@ static inline uint64_t resolveAddr(const ComposeState& state, void composeLeaf(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, - int depth, uint64_t absAddr) { + int depth, uint64_t absAddr, uint64_t scopeId) { const Node& node = tree.nodes[nodeIdx]; + // Get per-scope widths (falls back to global if no scope entry) + int typeW = state.effectiveTypeW(scopeId); + int nameW = state.effectiveNameW(scopeId); + // Line count: padding wraps at 8 bytes per line int numLines; if (node.kind == NodeKind::Padding) { @@ -119,9 +135,11 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont); lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth); lm.foldLevel = computeFoldLevel(depth, false); + lm.effectiveTypeW = typeW; + lm.effectiveNameW = nameW; QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub, - /*comment=*/{}, state.nameW); + /*comment=*/{}, typeW, nameW); state.emitLine(lineText, lm); } } @@ -129,14 +147,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, // Forward declarations (base/rootId default to 0 = use precomputed offsets) void composeNode(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, int depth, - uint64_t base = 0, uint64_t rootId = 0); + uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false, + uint64_t scopeId = 0, int arrayElementIdx = -1); void composeParent(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, int depth, - uint64_t base = 0, uint64_t rootId = 0); + uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false, + uint64_t scopeId = 0, int arrayElementIdx = -1); void composeParent(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, int depth, - uint64_t base, uint64_t rootId) { + uint64_t base, uint64_t rootId, bool isArrayChild, + uint64_t scopeId, int arrayElementIdx) { const Node& node = tree.nodes[nodeIdx]; uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId); @@ -157,8 +178,23 @@ void composeParent(ComposeState& state, const NodeTree& tree, } state.visiting.insert(node.id); - // Header line - { + // Array element separator: show [N] to indicate which element this is + if (isArrayChild && arrayElementIdx >= 0) { + LineMeta lm; + lm.nodeIdx = nodeIdx; + lm.nodeId = node.id; + lm.depth = depth; + lm.lineKind = LineKind::ArrayElementSeparator; + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false); + lm.nodeKind = node.kind; + lm.foldLevel = computeFoldLevel(depth, false); + lm.markerMask = 0; + lm.arrayElementIdx = arrayElementIdx; + state.emitLine(fmt::indent(depth) + QStringLiteral("[%1]").arg(arrayElementIdx), lm); + } + + // Header line (skip for array element structs - condensed display) + if (!isArrayChild) { LineMeta lm; lm.nodeIdx = nodeIdx; lm.nodeId = node.id; @@ -170,28 +206,46 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.foldCollapsed = node.collapsed; lm.foldLevel = computeFoldLevel(depth, true); lm.markerMask = (1u << M_STRUCT_BG); - lm.isRootHeader = (node.parentId == 0); // Root-level struct + lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct); - // Root structs show base address, nested structs show normal header - QString headerText = lm.isRootHeader - ? fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress) - : fmt::fmtStructHeader(node, depth); + QString headerText; + if (node.kind == NodeKind::Array) { + // Array header with navigation: "uint32_t[16] name { <0/16>" + lm.isArrayHeader = true; + lm.elementKind = node.elementKind; + lm.arrayViewIdx = node.viewIndex; + lm.arrayCount = node.arrayLen; + headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex); + } else if (lm.isRootHeader) { + // Root structs show base address + headerText = fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress); + } else { + // Nested structs show normal header + headerText = fmt::fmtStructHeader(node, depth); + } state.emitLine(headerText, lm); } - if (!node.collapsed) { + if (!node.collapsed || isArrayChild) { 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; }); + // For arrays, render children as condensed (no header/footer for struct elements) + bool childrenAreArrayElements = (node.kind == NodeKind::Array); + int elementIdx = 0; for (int childIdx : children) { - composeNode(state, tree, prov, childIdx, depth + 1, base, rootId); + // Pass this container's id as the scope for children (for per-scope widths) + // For array elements, also pass the element index for [N] separator + composeNode(state, tree, prov, childIdx, depth + 1, base, rootId, + childrenAreArrayElements, node.id, + childrenAreArrayElements ? elementIdx++ : -1); } } - // Footer line - { + // Footer line (skip for array element structs - condensed display) + if (!isArrayChild) { LineMeta lm; lm.nodeIdx = nodeIdx; lm.nodeId = node.id; @@ -210,10 +264,15 @@ void composeParent(ComposeState& state, const NodeTree& tree, void composeNode(ComposeState& state, const NodeTree& tree, const Provider& prov, int nodeIdx, int depth, - uint64_t base, uint64_t rootId) { + uint64_t base, uint64_t rootId, bool isArrayChild, + uint64_t scopeId, int arrayElementIdx) { const Node& node = tree.nodes[nodeIdx]; uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId); + // Get per-scope widths for this node + int typeW = state.effectiveTypeW(scopeId); + int nameW = state.effectiveNameW(scopeId); + // Pointer deref expansion if ((node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) && node.refId != 0) { @@ -229,7 +288,9 @@ void composeNode(ComposeState& state, const NodeTree& tree, lm.foldCollapsed = node.collapsed; lm.foldLevel = computeFoldLevel(depth, true); lm.markerMask = computeMarkers(node, prov, absAddr, false, depth); - state.emitLine(fmt::fmtNodeLine(node, prov, absAddr, depth, 0), lm); + lm.effectiveTypeW = typeW; + lm.effectiveNameW = nameW; + state.emitLine(fmt::fmtNodeLine(node, prov, absAddr, depth, 0, {}, typeW, nameW), lm); } if (!node.collapsed) { int sz = node.byteSize(); @@ -257,9 +318,9 @@ void composeNode(ComposeState& state, const NodeTree& tree, } if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) { - composeParent(state, tree, prov, nodeIdx, depth, base, rootId); + composeParent(state, tree, prov, nodeIdx, depth, base, rootId, isArrayChild, scopeId, arrayElementIdx); } else { - composeLeaf(state, tree, prov, nodeIdx, depth, absAddr); + composeLeaf(state, tree, prov, nodeIdx, depth, absAddr, scopeId); } } @@ -277,6 +338,20 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { for (int i = 0; i < tree.nodes.size(); i++) state.absOffsets[i] = tree.computeOffset(i); + // Compute effective type column width from longest type name + int maxTypeLen = kMinTypeW; + for (const Node& node : tree.nodes) { + QString typeName; + if (node.kind == NodeKind::Array) { + // Array type: "int32_t[10]", "char[64]", etc. + typeName = fmt::arrayTypeName(node.elementKind, node.arrayLen); + } else { + typeName = fmt::typeNameRaw(node.kind); + } + maxTypeLen = qMax(maxTypeLen, typeName.size()); + } + state.typeW = qBound(kMinTypeW, maxTypeLen + 1, kMaxTypeW); + // Compute effective name column width from longest name int maxNameLen = kMinNameW; for (const Node& node : tree.nodes) { @@ -288,6 +363,38 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { } state.nameW = qBound(kMinNameW, maxNameLen + 1, kMaxNameW); + // Pre-compute per-scope widths (each container gets widths based on direct children only) + for (int i = 0; i < tree.nodes.size(); i++) { + const Node& container = tree.nodes[i]; + if (container.kind != NodeKind::Struct && container.kind != NodeKind::Array) + continue; + + int scopeMaxType = kMinTypeW; + int scopeMaxName = kMinNameW; + + for (int childIdx : state.childMap.value(container.id)) { + const Node& child = tree.nodes[childIdx]; + + // Type width + QString childTypeName; + if (child.kind == NodeKind::Array) { + childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen); + } else { + childTypeName = fmt::typeNameRaw(child.kind); + } + scopeMaxType = qMax(scopeMaxType, childTypeName.size()); + + // Name width (skip hex/padding and containers) + if (!isHexPreview(child.kind) && + child.kind != NodeKind::Struct && child.kind != NodeKind::Array) { + scopeMaxName = qMax(scopeMaxName, child.name.size()); + } + } + + state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType + 1, kMaxTypeW); + state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName + 1, kMaxNameW); + } + QVector roots = state.childMap.value(0); std::sort(roots.begin(), roots.end(), [&](int a, int b) { return tree.nodes[a].offset < tree.nodes[b].offset; @@ -297,7 +404,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { composeNode(state, tree, prov, idx, 0); } - return { state.text, state.meta, LayoutInfo{state.nameW} }; + return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW} }; } QSet NodeTree::normalizePreferAncestors(const QSet& ids) const { diff --git a/src/controller.cpp b/src/controller.cpp index 9e6e251..d4bbc87 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -11,6 +11,9 @@ namespace rcx { +// Footer selection ID: set high bit to distinguish footer-only selections from node selections +static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL; + // ── RcxDocument ── RcxDocument::RcxDocument(QObject* parent) @@ -137,9 +140,33 @@ void RcxController::connectEditor(RcxEditor* editor) { break; } case EditTarget::Type: { - bool ok; - NodeKind k = kindFromTypeName(text, &ok); - if (ok) changeNodeKind(nodeIdx, k); + // Check for array type syntax: "type[count]" e.g. "int32_t[10]" + int bracketPos = text.indexOf('['); + if (bracketPos > 0 && text.endsWith(']')) { + QString elemTypeName = text.left(bracketPos).trimmed(); + QString countStr = text.mid(bracketPos + 1, text.size() - bracketPos - 2); + bool countOk; + int newCount = countStr.toInt(&countOk); + if (countOk && newCount > 0) { + bool typeOk; + NodeKind elemKind = kindFromTypeName(elemTypeName, &typeOk); + if (typeOk && nodeIdx < m_doc->tree.nodes.size()) { + Node& node = m_doc->tree.nodes[nodeIdx]; + if (node.kind == NodeKind::Array) { + // Update element kind and count (no undo for now) + node.elementKind = elemKind; + node.arrayLen = newCount; + if (node.viewIndex >= newCount) + node.viewIndex = qMax(0, newCount - 1); + } + } + } + } else { + // Regular type change + bool ok; + NodeKind k = kindFromTypeName(text, &ok); + if (ok) changeNodeKind(nodeIdx, k); + } break; } case EditTarget::Value: @@ -197,6 +224,10 @@ void RcxController::connectEditor(RcxEditor* editor) { } break; } + case EditTarget::ArrayIndex: + case EditTarget::ArrayCount: + // Array navigation removed - these cases are unreachable + break; } // Always refresh to restore canonical text (handles parse failures, no-ops, etc.) refresh(); @@ -640,20 +671,30 @@ void RcxController::handleNodeClick(RcxEditor* source, int line, bool ctrl = mods & Qt::ControlModifier; bool shift = mods & Qt::ShiftModifier; + // Compute effective selection ID: footers use nodeId | kFooterIdBit + auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t { + if (ln >= 0 && ln < m_lastResult.meta.size() && + m_lastResult.meta[ln].lineKind == LineKind::Footer) + return nid | kFooterIdBit; + return nid; + }; + + uint64_t selId = effectiveId(line, nodeId); + if (!ctrl && !shift) { m_selIds.clear(); - m_selIds.insert(nodeId); + m_selIds.insert(selId); m_anchorLine = line; } else if (ctrl && !shift) { - if (m_selIds.contains(nodeId)) - m_selIds.remove(nodeId); + if (m_selIds.contains(selId)) + m_selIds.remove(selId); else - m_selIds.insert(nodeId); + m_selIds.insert(selId); m_anchorLine = line; } else if (shift && !ctrl) { if (m_anchorLine < 0) { m_selIds.clear(); - m_selIds.insert(nodeId); + m_selIds.insert(selId); m_anchorLine = line; } else { m_selIds.clear(); @@ -661,19 +702,19 @@ void RcxController::handleNodeClick(RcxEditor* source, int line, int to = qMax(m_anchorLine, line); for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) { uint64_t nid = m_lastResult.meta[i].nodeId; - if (nid != 0) m_selIds.insert(nid); + if (nid != 0) m_selIds.insert(effectiveId(i, nid)); } } } else { // Ctrl+Shift if (m_anchorLine < 0) { - m_selIds.insert(nodeId); + m_selIds.insert(selId); m_anchorLine = line; } else { int from = qMin(m_anchorLine, line); int to = qMax(m_anchorLine, line); for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) { uint64_t nid = m_lastResult.meta[i].nodeId; - if (nid != 0) m_selIds.insert(nid); + if (nid != 0) m_selIds.insert(effectiveId(i, nid)); } } } @@ -682,7 +723,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line, if (m_selIds.size() == 1) { uint64_t sid = *m_selIds.begin(); - int idx = m_doc->tree.indexOfId(sid); + // Strip footer bit for node lookup + int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit); if (idx >= 0) emit nodeSelected(idx); } } diff --git a/src/core.h b/src/core.h index ecdacb7..477cfba 100644 --- a/src/core.h +++ b/src/core.h @@ -235,16 +235,19 @@ struct Node { QString name; uint64_t parentId = 0; // 0 = root (no parent) int offset = 0; - int arrayLen = 0; + int arrayLen = 1; // Array: element count int strLen = 64; bool collapsed = false; uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr + NodeKind elementKind = NodeKind::UInt8; // Array: element type + int viewIndex = 0; // Array: current view offset (transient) int byteSize() const { switch (kind) { case NodeKind::UTF8: return strLen; case NodeKind::UTF16: return strLen * 2; case NodeKind::Padding: return qMax(1, arrayLen); + case NodeKind::Array: return arrayLen * sizeForKind(elementKind); default: return sizeForKind(kind); } } @@ -260,6 +263,7 @@ struct Node { o["strLen"] = strLen; o["collapsed"] = collapsed; o["refId"] = QString::number(refId); + o["elementKind"] = kindToString(elementKind); return o; } static Node fromJson(const QJsonObject& o) { @@ -269,12 +273,19 @@ struct Node { n.name = o["name"].toString(); n.parentId = o["parentId"].toString("0").toULongLong(); n.offset = o["offset"].toInt(0); - n.arrayLen = o["arrayLen"].toInt(0); + n.arrayLen = o["arrayLen"].toInt(1); n.strLen = o["strLen"].toInt(64); n.collapsed = o["collapsed"].toBool(false); n.refId = o["refId"].toString("0").toULongLong(); + n.elementKind = kindFromString(o["elementKind"].toString("UInt8")); return n; } + + // Helper: is this a string-like array (char[] or wchar_t[])? + bool isStringArray() const { + return kind == NodeKind::Array && + (elementKind == NodeKind::UInt8 || elementKind == NodeKind::UInt16); + } }; // ── NodeTree ── @@ -415,7 +426,7 @@ struct NodeTree { // ── LineMeta ── enum class LineKind : uint8_t { - Header, Field, Continuation, Footer + Header, Field, Continuation, Footer, ArrayElementSeparator }; struct LineMeta { @@ -428,15 +439,23 @@ struct LineMeta { bool foldCollapsed = false; bool isContinuation = false; bool isRootHeader = false; // true for top-level struct headers (base address editable) + bool isArrayHeader = false; // true for array headers (has nav) LineKind lineKind = LineKind::Field; NodeKind nodeKind = NodeKind::Int32; + NodeKind elementKind = NodeKind::UInt8; // Array element type + int arrayViewIdx = 0; // Array: current view index + int arrayCount = 0; // Array: total element count + int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element) QString offsetText; uint32_t markerMask = 0; + int effectiveTypeW = 14; // Per-line type column width used for rendering + int effectiveNameW = 22; // Per-line name column width used for rendering }; // ── Layout Info ── struct LayoutInfo { + int typeW = 14; // Effective type column width (default = kColType) int nameW = 22; // Effective name column width (default = kColName) }; @@ -476,30 +495,32 @@ struct ColumnSpan { bool valid = false; }; -enum class EditTarget { Name, Type, Value, BaseAddress }; +enum class EditTarget { Name, Type, Value, BaseAddress, ArrayIndex, ArrayCount }; // Column layout constants (shared with format.cpp span computation) inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line -inline constexpr int kColType = 10; +inline constexpr int kColType = 14; // Max type column width (fits "uint64_t[999]") inline constexpr int kColName = 22; inline constexpr int kColValue = 32; inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address) inline constexpr int kSepWidth = 2; +inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t") +inline constexpr int kMaxTypeW = 14; // Maximum type column width (fits "uint64_t[999]") inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview) inline constexpr int kMaxNameW = 22; // Maximum name column width (= kColName) -inline ColumnSpan typeSpanFor(const LineMeta& lm) { +inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) { if (lm.lineKind != LineKind::Field || lm.isContinuation) return {}; int ind = kFoldCol + lm.depth * 3; - return {ind, ind + kColType, true}; + return {ind, ind + typeW, true}; } -inline ColumnSpan nameSpanFor(const LineMeta& lm, int nameW = kColName) { +inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) { if (lm.isContinuation || lm.lineKind != LineKind::Field) return {}; int ind = kFoldCol + lm.depth * 3; - int start = ind + kColType + kSepWidth; + int start = ind + typeW + kSepWidth; // Hex/Padding: ASCII preview takes the name column position (8 chars) if (isHexPreview(lm.nodeKind)) @@ -508,8 +529,9 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int nameW = kColName) { return {start, start + nameW, true}; } -inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int nameW = kColName) { - if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; +inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) { + if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer || + lm.lineKind == LineKind::ArrayElementSeparator) return {}; int ind = kFoldCol + lm.depth * 3; // Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)] @@ -518,20 +540,20 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int nameW if (lm.isContinuation) { int prefixW = isHexPad - ? (kColType + kSepWidth + 8 + kSepWidth) - : (kColType + nameW + 4); + ? (typeW + kSepWidth + 8 + kSepWidth) + : (typeW + nameW + 4); int start = ind + prefixW; return {start, start + valWidth, true}; } if (lm.lineKind != LineKind::Field) return {}; int start = isHexPad - ? (ind + kColType + kSepWidth + 8 + kSepWidth) - : (ind + kColType + kSepWidth + nameW + kSepWidth); + ? (ind + typeW + kSepWidth + 8 + kSepWidth) + : (ind + typeW + kSepWidth + nameW + kSepWidth); return {start, start + valWidth, true}; } -inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int nameW = kColName) { +inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) { if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; int ind = kFoldCol + lm.depth * 3; @@ -541,13 +563,13 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int nameW = int start; if (lm.isContinuation) { int prefixW = isHexPad - ? (kColType + kSepWidth + 8 + kSepWidth) - : (kColType + nameW + 4); + ? (typeW + kSepWidth + 8 + kSepWidth) + : (typeW + nameW + 4); start = ind + prefixW + valWidth; } else { start = isHexPad - ? (ind + kColType + kSepWidth + 8 + kSepWidth + valWidth) - : (ind + kColType + kSepWidth + nameW + kSepWidth + valWidth); + ? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth) + : (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth); } return {start, lineLength, start < lineLength}; } @@ -579,6 +601,39 @@ inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& line return {baseIdx, endPos, true}; } +// ── Array navigation spans ── +// Line format: "uint32_t[16] name { <0/16>" + +inline ColumnSpan arrayPrevSpanFor(const LineMeta& lm, const QString& lineText) { + if (!lm.isArrayHeader) return {}; + int lt = lineText.lastIndexOf('<'); + if (lt < 0) return {}; + return {lt, lt + 1, true}; +} + +inline ColumnSpan arrayIndexSpanFor(const LineMeta& lm, const QString& lineText) { + if (!lm.isArrayHeader) return {}; + int lt = lineText.lastIndexOf('<'); + int slash = lineText.indexOf('/', lt); + if (lt < 0 || slash < 0) return {}; + return {lt + 1, slash, true}; +} + +inline ColumnSpan arrayCountSpanFor(const LineMeta& lm, const QString& lineText) { + if (!lm.isArrayHeader) return {}; + int slash = lineText.lastIndexOf('/'); + int gt = lineText.indexOf('>', slash); + if (slash < 0 || gt < 0) return {}; + return {slash + 1, gt, true}; +} + +inline ColumnSpan arrayNextSpanFor(const LineMeta& lm, const QString& lineText) { + if (!lm.isArrayHeader) return {}; + int gt = lineText.lastIndexOf('>'); + if (gt < 0) return {}; + return {gt, gt + 1, true}; +} + // ── ViewState ── struct ViewState { @@ -592,7 +647,8 @@ struct ViewState { namespace fmt { using TypeNameFn = QString (*)(NodeKind); void setTypeNameProvider(TypeNameFn fn); - QString typeName(NodeKind kind); + QString typeName(NodeKind kind, int colType = kColType); + QString typeNameRaw(NodeKind kind); // Unpadded type name for width calculation QString fmtInt8(int8_t v); QString fmtInt16(int16_t v); QString fmtInt32(int32_t v); @@ -608,11 +664,13 @@ namespace fmt { QString fmtPointer64(uint64_t v); QString fmtNodeLine(const Node& node, const Provider& prov, uint64_t addr, int depth, int subLine = 0, - const QString& comment = {}, int colName = kColName); + const QString& comment = {}, int colType = kColType, int colName = kColName); QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation); QString fmtStructHeader(const Node& node, int depth); QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress); QString fmtStructFooter(const Node& node, int depth, int totalSize = -1); + QString fmtArrayHeader(const Node& node, int depth, int viewIdx); + QString arrayTypeName(NodeKind elemKind, int count); QString validateBaseAddress(const QString& text); QString indent(int depth); QString readValue(const Node& node, const Provider& prov, diff --git a/src/editor.cpp b/src/editor.cpp index 94bff40..fd37b71 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -26,6 +26,9 @@ static constexpr int IND_HEX_DIM = 9; static constexpr int IND_BASE_ADDR = 10; // Green color for base address static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like) +// Footer selection ID: set high bit to distinguish footer-only selections from node selections +static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL; + static QString g_fontName = "Consolas"; static QFont editorFont() { @@ -388,9 +391,15 @@ void RcxEditor::applySelectionOverlay(const QSet& selIds) { m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen); for (int i = 0; i < m_meta.size(); i++) { - if (selIds.contains(m_meta[i].nodeId)) { + uint64_t nodeId = m_meta[i].nodeId; + bool isFooter = (m_meta[i].lineKind == LineKind::Footer); + + // Footers check for footerId, non-footers check for plain nodeId + uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId; + if (selIds.contains(checkId)) { m_sci->markerAdd(i, M_SELECTED); - paintEditableSpans(i); + if (!isFooter) + paintEditableSpans(i); } } @@ -406,10 +415,25 @@ void RcxEditor::applyHoverHighlight() { if (m_editState.active) return; if (!m_hoverInside) return; if (m_hoveredNodeId == 0) return; - if (m_currentSelIds.contains(m_hoveredNodeId)) return; - for (int i = 0; i < m_meta.size(); i++) { - if (m_meta[i].nodeId == m_hoveredNodeId) - m_sci->markerAdd(i, M_HOVER); + + // Check if hovered line is a footer - footers highlight independently + bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() && + m_meta[m_hoveredLine].lineKind == LineKind::Footer); + + // Check if the hovered item is already selected (using appropriate ID) + uint64_t checkId = hoveringFooter ? (m_hoveredNodeId | kFooterIdBit) : m_hoveredNodeId; + if (m_currentSelIds.contains(checkId)) return; + + if (hoveringFooter) { + // Footer: only highlight this specific line + m_sci->markerAdd(m_hoveredLine, M_HOVER); + } else { + // Non-footer: highlight all matching lines except footers + for (int i = 0; i < m_meta.size(); i++) { + if (m_meta[i].nodeId == m_hoveredNodeId && + m_meta[i].lineKind != LineKind::Footer) + m_sci->markerAdd(i, M_HOVER); + } } } @@ -449,9 +473,9 @@ int RcxEditor::currentNodeIndex() const { // ── Column span computation ── -ColumnSpan RcxEditor::typeSpan(const LineMeta& lm) { return typeSpanFor(lm); } -ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int nameW) { return nameSpanFor(lm, nameW); } -ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int nameW) { return valueSpanFor(lm, lineLength, nameW); } +ColumnSpan RcxEditor::typeSpan(const LineMeta& lm, int typeW) { return typeSpanFor(lm, typeW); } +ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int typeW, int nameW) { return nameSpanFor(lm, typeW, nameW); } +ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int typeW, int nameW) { return valueSpanFor(lm, lineLength, typeW, nameW); } // ── Multi-selection ── @@ -538,9 +562,24 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) { int ind = kFoldCol + lm.depth * 3; int typeEnd = lineText.indexOf(' ', ind); if (typeEnd <= ind || typeEnd >= bracePos) return {}; + + // Don't allow editing array element names like "[0]", "[1]", etc. + QString name = lineText.mid(typeEnd + 1, bracePos - typeEnd - 1).trimmed(); + if (name.startsWith('[') && name.endsWith(']')) + return {}; + return {typeEnd + 1, bracePos, true}; } +// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {" +static ColumnSpan arrayHeaderTypeSpan(const LineMeta& lm, const QString& lineText) { + if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {}; + int ind = kFoldCol + lm.depth * 3; + int typeEnd = lineText.indexOf(' ', ind); + if (typeEnd <= ind) return {}; + return {ind, typeEnd, true}; +} + RcxEditor::NormalizedSpan RcxEditor::normalizeSpan( const ColumnSpan& raw, const QString& lineText, EditTarget target, bool skipPrefixes) const @@ -589,14 +628,24 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, QString lineText = getLineText(m_sci, line); int textLen = lineText.size(); + // Use per-line effective widths (set during compose based on containing scope) + int typeW = lm->effectiveTypeW; + int nameW = lm->effectiveNameW; + ColumnSpan s; switch (t) { - case EditTarget::Type: s = typeSpan(*lm); break; - case EditTarget::Name: s = nameSpan(*lm, m_layout.nameW); break; - case EditTarget::Value: s = valueSpan(*lm, textLen, m_layout.nameW); break; + case EditTarget::Type: s = typeSpan(*lm, typeW); break; + case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break; + case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break; case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break; + case EditTarget::ArrayIndex: + case EditTarget::ArrayCount: + break; // Array navigation removed } + // Fallback spans for header lines + if (!s.valid && t == EditTarget::Type) + s = arrayHeaderTypeSpan(*lm, lineText); if (!s.valid && t == EditTarget::Name) s = headerNameSpan(*lm, lineText); @@ -640,8 +689,7 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const { static bool hitTestTarget(QsciScintilla* sci, const QVector& meta, const QPoint& viewportPos, - int& outLine, EditTarget& outTarget, - int nameW = kColName) + int& outLine, EditTarget& outTarget) { long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, (unsigned long)viewportPos.x(), (long)viewportPos.y()); @@ -656,18 +704,29 @@ static bool hitTestTarget(QsciScintilla* sci, int textLen = lineText.size(); const LineMeta& lm = meta[line]; - ColumnSpan ts = RcxEditor::typeSpan(lm); - ColumnSpan ns = RcxEditor::nameSpan(lm, nameW); - ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, nameW); - ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers - if (!ns.valid) - ns = headerNameSpan(lm, lineText); + // Array element separators are not interactive + if (lm.lineKind == LineKind::ArrayElementSeparator) return false; + + // Use per-line effective widths from LineMeta + int typeW = lm.effectiveTypeW; + int nameW = lm.effectiveNameW; auto inSpan = [&](const ColumnSpan& s) { return s.valid && col >= s.start && col < s.end; }; + ColumnSpan ts = RcxEditor::typeSpan(lm, typeW); + ColumnSpan ns = RcxEditor::nameSpan(lm, typeW, nameW); + ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, typeW, nameW); + ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers + + // Fallback spans for header lines + if (!ts.valid) + ts = arrayHeaderTypeSpan(lm, lineText); + if (!ns.valid) + ns = headerNameSpan(lm, lineText); + if (inSpan(bs)) outTarget = EditTarget::BaseAddress; else if (inSpan(ts)) outTarget = EditTarget::Type; else if (inSpan(ns)) outTarget = EditTarget::Name; @@ -701,12 +760,17 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { const LineMeta* lm = metaForLine(m_editState.line); if (lm) { QString lineText = getLineText(m_sci, h.line); + // Use per-line effective widths + int typeW = lm->effectiveTypeW; + int nameW = lm->effectiveNameW; ColumnSpan raw; switch (m_editState.target) { - case EditTarget::Type: raw = typeSpan(*lm); break; - case EditTarget::Name: raw = nameSpan(*lm, m_layout.nameW); break; - case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), m_layout.nameW); break; + case EditTarget::Type: raw = typeSpan(*lm, typeW); break; + case EditTarget::Name: raw = nameSpan(*lm, typeW, nameW); break; + case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), typeW, nameW); break; case EditTarget::BaseAddress: raw = baseAddressSpanFor(*lm, lineText); break; + case EditTarget::ArrayIndex: raw = arrayIndexSpanFor(*lm, lineText); break; + case EditTarget::ArrayCount: raw = arrayCountSpanFor(*lm, lineText); break; } if (raw.valid && h.col >= raw.start && h.col < raw.end) { // Within raw span but outside trimmed text → move cursor to end @@ -732,8 +796,9 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { m_hoverInside = true; auto h = hitTest(me->pos()); uint64_t newHoverId = (h.line >= 0) ? h.nodeId : 0; - if (newHoverId != m_hoveredNodeId) { + if (newHoverId != m_hoveredNodeId || h.line != m_hoveredLine) { m_hoveredNodeId = newHoverId; + m_hoveredLine = h.line; applyHoverHighlight(); } @@ -746,9 +811,9 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)); // Single-click on editable token of already-selected node → edit - if (alreadySelected && plain) { - int tLine; EditTarget t; - if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t, m_layout.nameW)) { + int tLine; EditTarget t; + if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) { + if (alreadySelected && plain) { m_pendingClickNodeId = 0; return beginInlineEdit(t, tLine); } @@ -824,7 +889,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { && event->type() == QEvent::MouseButtonDblClick) { auto* me = static_cast(event); int line; EditTarget t; - if (hitTestTarget(m_sci, m_meta, me->pos(), line, t, m_layout.nameW)) { + if (hitTestTarget(m_sci, m_meta, me->pos(), line, t)) { m_pendingClickNodeId = 0; // cancel deferred selection change return beginInlineEdit(t, line); } @@ -856,6 +921,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { } else if (event->type() == QEvent::Leave) { m_hoverInside = false; m_hoveredNodeId = 0; + m_hoveredLine = -1; applyHoverHighlight(); } else if (event->type() == QEvent::Wheel) { m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos()); @@ -866,8 +932,10 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { || event->type() == QEvent::Wheel) { auto h = hitTest(m_lastHoverPos); uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0; - if (newHoverId != m_hoveredNodeId) { + int newHoverLine = (m_hoverInside && h.line >= 0) ? h.line : -1; + if (newHoverId != m_hoveredNodeId || newHoverLine != m_hoveredLine) { m_hoveredNodeId = newHoverId; + m_hoveredLine = newHoverLine; applyHoverHighlight(); } } @@ -948,6 +1016,7 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { bool RcxEditor::beginInlineEdit(EditTarget target, int line) { if (m_editState.active) return false; m_hoveredNodeId = 0; + m_hoveredLine = -1; applyHoverHighlight(); // Clear editable-token color hints (de-emphasize non-active tokens) clearIndicatorLine(IND_EDITABLE, m_hintLine); @@ -982,7 +1051,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { // Store fixed comment column position for value editing if (target == EditTarget::Value) { - ColumnSpan cs = commentSpanFor(*lm, lineText.size(), m_layout.nameW); + ColumnSpan cs = commentSpanFor(*lm, lineText.size(), lm->effectiveTypeW, lm->effectiveNameW); m_editState.commentCol = cs.valid ? cs.start : -1; m_editState.lastValidationOk = true; // original value is always valid } else { @@ -1172,7 +1241,8 @@ void RcxEditor::updateTypeListFilter() { void RcxEditor::paintEditableSpans(int line) { NormalizedSpan norm; - for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value}) { + for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value, + EditTarget::BaseAddress}) { if (resolvedSpanFor(line, t, norm)) fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); } @@ -1191,13 +1261,21 @@ void RcxEditor::updateEditableIndicators(int line) { return; } + // Helper to check if a line's node is selected (handles footer IDs) + auto isLineSelected = [this](const LineMeta* lm) -> bool { + if (!lm) return false; + bool isFooter = (lm->lineKind == LineKind::Footer); + uint64_t checkId = isFooter ? (lm->nodeId | kFooterIdBit) : lm->nodeId; + return m_currentSelIds.contains(checkId); + }; + // If new line is selected, its indicators are managed by applySelectionOverlay // But we still need to clear the old non-selected hint line const LineMeta* newLm = metaForLine(line); - if (newLm && m_currentSelIds.contains(newLm->nodeId)) { + if (isLineSelected(newLm)) { if (m_hintLine >= 0) { const LineMeta* oldLm = metaForLine(m_hintLine); - if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId)) + if (!isLineSelected(oldLm)) clearIndicatorLine(IND_EDITABLE, m_hintLine); } m_hintLine = line; @@ -1207,7 +1285,7 @@ void RcxEditor::updateEditableIndicators(int line) { // Clear old cursor line (only if not a selected node) if (m_hintLine >= 0) { const LineMeta* oldLm = metaForLine(m_hintLine); - if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId)) + if (!isLineSelected(oldLm)) clearIndicatorLine(IND_EDITABLE, m_hintLine); } @@ -1251,9 +1329,9 @@ void RcxEditor::applyHoverCursor() { } int line; EditTarget t; - bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t, m_layout.nameW); + bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t); - // Apply hover span indicator (blue text like a link) + // Apply hover span indicator (blue text like a link) for editable spans if (tokenHit) { NormalizedSpan span; if (resolvedSpanFor(line, t, span)) { diff --git a/src/editor.h b/src/editor.h index 4531b9b..d8b3422 100644 --- a/src/editor.h +++ b/src/editor.h @@ -24,9 +24,9 @@ public: int currentNodeIndex() const; // ── Column span computation ── - static ColumnSpan typeSpan(const LineMeta& lm); - static ColumnSpan nameSpan(const LineMeta& lm, int nameW = kColName); - static ColumnSpan valueSpan(const LineMeta& lm, int lineLength, int nameW = kColName); + static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType); + static ColumnSpan nameSpan(const LineMeta& lm, int typeW = kColType, int nameW = kColName); + static ColumnSpan valueSpan(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName); // ── Multi-selection ── QSet selectedNodeIndices() const; @@ -65,6 +65,7 @@ private: bool m_hoverInside = false; bool m_cursorOverridden = false; uint64_t m_hoveredNodeId = 0; + int m_hoveredLine = -1; QSet m_currentSelIds; int m_hoverSpanLine = -1; // Line with hover span indicator // ── Drag selection ── diff --git a/src/format.cpp b/src/format.cpp index 3177925..b462ba3 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -28,10 +28,27 @@ static TypeNameFn g_typeNameFn = nullptr; void setTypeNameProvider(TypeNameFn fn) { g_typeNameFn = fn; } -QString typeName(NodeKind kind) { - if (g_typeNameFn) return fit(g_typeNameFn(kind), COL_TYPE); +// Unpadded type name for width calculation +QString typeNameRaw(NodeKind kind) { + if (g_typeNameFn) return g_typeNameFn(kind); auto* m = kindMeta(kind); - return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), COL_TYPE); + return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"); +} + +QString typeName(NodeKind kind, int colType) { + if (g_typeNameFn) return fit(g_typeNameFn(kind), colType); + auto* m = kindMeta(kind); + return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), colType); +} + +// Array type string: "uint32_t[16]" or "char[64]" +QString arrayTypeName(NodeKind elemKind, int count) { + auto* m = kindMeta(elemKind); + QString elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"); + // char[] for UInt8, wchar_t[] for UInt16 + if (elemKind == NodeKind::UInt8) elem = QStringLiteral("char"); + else if (elemKind == NodeKind::UInt16) elem = QStringLiteral("wchar_t"); + return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]"); } // ── Value formatting ── @@ -95,12 +112,15 @@ QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddres return header + QStringLiteral("base: ") + baseHex; } -QString fmtStructFooter(const Node& node, int depth, int totalSize) { - QString s = indent(depth) + QStringLiteral("};"); - if (totalSize > 0) - s += QStringLiteral(" // sizeof(") + node.name + QStringLiteral(")=0x") - + QString::number(totalSize, 16).toUpper(); - return s; +QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) { + return indent(depth) + QStringLiteral("};"); +} + +// ── Array header ── +// Format: "uint32_t[16] myArray {" (like struct header, no fixed columns) +QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/) { + QString type = arrayTypeName(node.elementKind, node.arrayLen); + return indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" {"); } // ── Hex / ASCII preview ── @@ -230,12 +250,12 @@ 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 colName) { + const QString& comment, int colType, int colName) { QString ind = indent(depth); - QString type = typeName(node.kind); + QString type = typeName(node.kind, colType); QString name = fit(node.name, colName); // Blank prefix for continuation lines (same width as type+sep+name+sep) - const int prefixW = COL_TYPE + colName + 4; // 2 seps × 2 chars + const int prefixW = colType + colName + 4; // 2 seps × 2 chars // Comment suffix (padded or empty) QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ') @@ -268,7 +288,7 @@ QString fmtNodeLine(const Node& node, const Provider& prov, QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1 if (subLine == 0) return ind + type + SEP + ascii + SEP + hex + cmtSuffix; - return ind + QString(COL_TYPE + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix; + return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix; } // Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns const int sz = sizeForKind(node.kind); diff --git a/src/main.cpp b/src/main.cpp index 2aea8ba..94665a2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -260,187 +260,281 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { void MainWindow::newFile() { auto* doc = new RcxDocument(this); - // Autoload self as binary data - doc->loadData(QCoreApplication::applicationFilePath()); - doc->tree.baseAddress = 0; + // ══════════════════════════════════════════════════════════════════════════ + // PE Header Demo - Realistic PE32+ (64-bit) executable structure + // ══════════════════════════════════════════════════════════════════════════ + // Layout: + // 0x000: DOS Header (64 bytes) + // 0x040: DOS Stub (64 bytes padding) + // 0x080: PE Signature (4 bytes) + // 0x084: File Header (20 bytes) + // 0x098: Optional Header PE32+ (240 bytes) + // - Standard fields (24 bytes) + // - Windows fields (88 bytes) + // - Data Directories (16 * 8 = 128 bytes) + // 0x188: Section Headers (4 * 40 = 160 bytes) + // 0x228: End of headers (total 552 bytes) + // ══════════════════════════════════════════════════════════════════════════ - // Read e_lfanew to find PE header offset - uint32_t lfanew = doc->provider->readU32(0x3C); - if (lfanew < 0x40 || lfanew >= (uint32_t)doc->provider->size()) - lfanew = 0x40; - uint32_t pe = lfanew; // PE signature - uint32_t fh = pe + 4; // IMAGE_FILE_HEADER - uint32_t oh = fh + 20; // IMAGE_OPTIONAL_HEADER (PE32+) + QByteArray peData(0x300, '\0'); // 768 bytes + char* d = peData.data(); - Node root; - root.kind = NodeKind::Struct; - root.name = "PE_HEADER"; - root.parentId = 0; - root.offset = 0; - int ri = doc->tree.addNode(root); - uint64_t rootId = doc->tree.nodes[ri].id; - - auto add = [&](NodeKind k, const QString& name, int off) { - Node n; - n.kind = k; - n.name = name; - n.offset = off; - n.parentId = rootId; - doc->tree.addNode(n); - }; - - // ── IMAGE_DOS_HEADER (0x00 – 0x3F) ── - add(NodeKind::UInt16, "e_magic", 0x00); - add(NodeKind::UInt16, "e_cblp", 0x02); - add(NodeKind::UInt16, "e_cp", 0x04); - add(NodeKind::UInt16, "e_crlc", 0x06); - add(NodeKind::UInt16, "e_cparhdr", 0x08); - add(NodeKind::UInt16, "e_minalloc", 0x0A); - add(NodeKind::UInt16, "e_maxalloc", 0x0C); - add(NodeKind::UInt16, "e_ss", 0x0E); - add(NodeKind::UInt16, "e_sp", 0x10); - add(NodeKind::UInt16, "e_csum", 0x12); - add(NodeKind::UInt16, "e_ip", 0x14); - add(NodeKind::UInt16, "e_cs", 0x16); - add(NodeKind::UInt16, "e_lfarlc", 0x18); - add(NodeKind::UInt16, "e_ovno", 0x1A); - add(NodeKind::Hex64, "e_res", 0x1C); - add(NodeKind::UInt16, "e_oemid", 0x24); - add(NodeKind::UInt16, "e_oeminfo", 0x26); - add(NodeKind::Hex64, "e_res2_0", 0x28); - add(NodeKind::Hex64, "e_res2_1", 0x30); - add(NodeKind::Hex32, "e_res2_2", 0x38); - add(NodeKind::UInt32, "e_lfanew", 0x3C); - - // ── DOS Stub (0x40 to PE signature) — fill with Hex nodes ── - { - int cursor = 0x40; - while (cursor + 8 <= (int)pe) { - add(NodeKind::Hex64, - QString("stub_%1").arg(cursor, 4, 16, QChar('0')), - cursor); - cursor += 8; - } - if (cursor + 4 <= (int)pe) { - add(NodeKind::Hex32, - QString("stub_%1").arg(cursor, 4, 16, QChar('0')), - cursor); - cursor += 4; - } - if (cursor + 2 <= (int)pe) { - add(NodeKind::Hex16, - QString("stub_%1").arg(cursor, 4, 16, QChar('0')), - cursor); - cursor += 2; - } - if (cursor + 1 <= (int)pe) { - add(NodeKind::Hex8, - QString("stub_%1").arg(cursor, 4, 16, QChar('0')), - cursor); - cursor += 1; - } - } + // ── DOS Header (IMAGE_DOS_HEADER) ── + d[0x00] = 'M'; d[0x01] = 'Z'; // e_magic + *(uint16_t*)(d + 0x02) = 0x0090; // e_cblp (bytes on last page) + *(uint16_t*)(d + 0x04) = 0x0003; // e_cp (pages in file) + *(uint16_t*)(d + 0x06) = 0x0000; // e_crlc (relocations) + *(uint16_t*)(d + 0x08) = 0x0004; // e_cparhdr (header size in paragraphs) + *(uint16_t*)(d + 0x0A) = 0x0000; // e_minalloc + *(uint16_t*)(d + 0x0C) = 0xFFFF; // e_maxalloc + *(uint16_t*)(d + 0x0E) = 0x0000; // e_ss + *(uint16_t*)(d + 0x10) = 0x00B8; // e_sp + *(uint16_t*)(d + 0x12) = 0x0000; // e_csum + *(uint16_t*)(d + 0x14) = 0x0000; // e_ip + *(uint16_t*)(d + 0x16) = 0x0000; // e_cs + *(uint16_t*)(d + 0x18) = 0x0040; // e_lfarlc + *(uint16_t*)(d + 0x1A) = 0x0000; // e_ovno + // e_res[4] at 0x1C-0x23 (zeroed) + *(uint16_t*)(d + 0x24) = 0x0000; // e_oemid + *(uint16_t*)(d + 0x26) = 0x0000; // e_oeminfo + // e_res2[10] at 0x28-0x3B (zeroed) + *(uint32_t*)(d + 0x3C) = 0x00000080; // e_lfanew → PE header at 0x80 // ── PE Signature ── - add(NodeKind::UInt32, "Signature", pe); + const int peOff = 0x80; + d[peOff+0] = 'P'; d[peOff+1] = 'E'; d[peOff+2] = 0; d[peOff+3] = 0; - // ── IMAGE_FILE_HEADER (nested struct) ── - { - Node fhStruct; - fhStruct.kind = NodeKind::Struct; - fhStruct.name = "IMAGE_FILE_HEADER"; - fhStruct.parentId = rootId; - fhStruct.offset = fh; - int fi = doc->tree.addNode(fhStruct); - uint64_t fhId = doc->tree.nodes[fi].id; + // ── File Header (IMAGE_FILE_HEADER) ── + const int fhOff = peOff + 4; // 0x84 + *(uint16_t*)(d + fhOff + 0) = 0x8664; // Machine (AMD64) + *(uint16_t*)(d + fhOff + 2) = 0x0004; // NumberOfSections + *(uint32_t*)(d + fhOff + 4) = 0x65A3B2C1; // TimeDateStamp + *(uint32_t*)(d + fhOff + 8) = 0x00000000; // PointerToSymbolTable + *(uint32_t*)(d + fhOff + 12) = 0x00000000; // NumberOfSymbols + *(uint16_t*)(d + fhOff + 16) = 0x00F0; // SizeOfOptionalHeader (240) + *(uint16_t*)(d + fhOff + 18) = 0x0022; // Characteristics (EXECUTABLE|LARGE_ADDRESS_AWARE) - auto addFH = [&](NodeKind k, const QString& name, int off) { - Node n; - n.kind = k; - n.name = name; - n.offset = off; - n.parentId = fhId; - doc->tree.addNode(n); - }; + // ── Optional Header PE32+ (IMAGE_OPTIONAL_HEADER64) ── + const int ohOff = fhOff + 20; // 0x98 + *(uint16_t*)(d + ohOff + 0) = 0x020B; // Magic (PE32+) + *(uint8_t*)(d + ohOff + 2) = 0x0E; // MajorLinkerVersion + *(uint8_t*)(d + ohOff + 3) = 0x00; // MinorLinkerVersion + *(uint32_t*)(d + ohOff + 4) = 0x00012000; // SizeOfCode + *(uint32_t*)(d + ohOff + 8) = 0x00008000; // SizeOfInitializedData + *(uint32_t*)(d + ohOff + 12) = 0x00000000; // SizeOfUninitializedData + *(uint32_t*)(d + ohOff + 16) = 0x00001000; // AddressOfEntryPoint + *(uint32_t*)(d + ohOff + 20) = 0x00001000; // BaseOfCode - addFH(NodeKind::UInt16, "Machine", 0x00); - addFH(NodeKind::UInt16, "NumberOfSections", 0x02); - addFH(NodeKind::UInt32, "TimeDateStamp", 0x04); - addFH(NodeKind::UInt32, "PtrToSymbolTable", 0x08); - addFH(NodeKind::UInt32, "NumberOfSymbols", 0x0C); - addFH(NodeKind::UInt16, "SizeOfOptionalHeader", 0x10); - addFH(NodeKind::UInt16, "Characteristics", 0x12); + // Windows-specific fields (PE32+) + *(uint64_t*)(d + ohOff + 24) = 0x0000000140000000ULL; // ImageBase + *(uint32_t*)(d + ohOff + 32) = 0x00001000; // SectionAlignment + *(uint32_t*)(d + ohOff + 36) = 0x00000200; // FileAlignment + *(uint16_t*)(d + ohOff + 40) = 0x0006; // MajorOperatingSystemVersion + *(uint16_t*)(d + ohOff + 42) = 0x0000; // MinorOperatingSystemVersion + *(uint16_t*)(d + ohOff + 44) = 0x0000; // MajorImageVersion + *(uint16_t*)(d + ohOff + 46) = 0x0000; // MinorImageVersion + *(uint16_t*)(d + ohOff + 48) = 0x0006; // MajorSubsystemVersion + *(uint16_t*)(d + ohOff + 50) = 0x0000; // MinorSubsystemVersion + *(uint32_t*)(d + ohOff + 52) = 0x00000000; // Win32VersionValue + *(uint32_t*)(d + ohOff + 56) = 0x00025000; // SizeOfImage + *(uint32_t*)(d + ohOff + 60) = 0x00000200; // SizeOfHeaders + *(uint32_t*)(d + ohOff + 64) = 0x00000000; // CheckSum + *(uint16_t*)(d + ohOff + 68) = 0x0003; // Subsystem (CONSOLE) + *(uint16_t*)(d + ohOff + 70) = 0x8160; // DllCharacteristics (DYNAMIC_BASE|NX_COMPAT|TERMINAL_SERVER_AWARE) + *(uint64_t*)(d + ohOff + 72) = 0x0000000000100000ULL; // SizeOfStackReserve + *(uint64_t*)(d + ohOff + 80) = 0x0000000000001000ULL; // SizeOfStackCommit + *(uint64_t*)(d + ohOff + 88) = 0x0000000000100000ULL; // SizeOfHeapReserve + *(uint64_t*)(d + ohOff + 96) = 0x0000000000001000ULL; // SizeOfHeapCommit + *(uint32_t*)(d + ohOff + 104) = 0x00000000; // LoaderFlags + *(uint32_t*)(d + ohOff + 108) = 0x00000010; // NumberOfRvaAndSizes (16) + + // ── Data Directories (16 entries × 8 bytes) ── + const int ddOff = ohOff + 112; // 0x108 + // Each entry: VirtualAddress (4) + Size (4) + struct { uint32_t rva; uint32_t size; } dataDirs[16] = { + {0x00000000, 0x00000000}, // 0: Export + {0x00014000, 0x000000A0}, // 1: Import + {0x00000000, 0x00000000}, // 2: Resource + {0x00000000, 0x00000000}, // 3: Exception + {0x00000000, 0x00000000}, // 4: Security + {0x00000000, 0x00000000}, // 5: BaseReloc + {0x00013000, 0x00000038}, // 6: Debug + {0x00000000, 0x00000000}, // 7: Architecture + {0x00000000, 0x00000000}, // 8: GlobalPtr + {0x00000000, 0x00000000}, // 9: TLS + {0x00000000, 0x00000000}, // 10: LoadConfig + {0x00000000, 0x00000000}, // 11: BoundImport + {0x00014050, 0x00000048}, // 12: IAT + {0x00000000, 0x00000000}, // 13: DelayImport + {0x00000000, 0x00000000}, // 14: CLR + {0x00000000, 0x00000000}, // 15: Reserved + }; + for (int i = 0; i < 16; i++) { + *(uint32_t*)(d + ddOff + i*8 + 0) = dataDirs[i].rva; + *(uint32_t*)(d + ddOff + i*8 + 4) = dataDirs[i].size; } - // ── IMAGE_OPTIONAL_HEADER64 (nested struct) ── - { - Node ohStruct; - ohStruct.kind = NodeKind::Struct; - ohStruct.name = "IMAGE_OPTIONAL_HEADER64"; - ohStruct.parentId = rootId; - ohStruct.offset = oh; - int oi = doc->tree.addNode(ohStruct); - uint64_t ohId = doc->tree.nodes[oi].id; - - auto addOH = [&](NodeKind k, const QString& name, int off) { - Node n; - n.kind = k; - n.name = name; - n.offset = off; - n.parentId = ohId; - doc->tree.addNode(n); - }; - - addOH(NodeKind::UInt16, "Magic", 0x00); - addOH(NodeKind::UInt8, "MajorLinkerVersion", 0x02); - addOH(NodeKind::UInt8, "MinorLinkerVersion", 0x03); - addOH(NodeKind::UInt32, "SizeOfCode", 0x04); - addOH(NodeKind::UInt32, "SizeOfInitData", 0x08); - addOH(NodeKind::UInt32, "SizeOfUninitData", 0x0C); - addOH(NodeKind::UInt32, "AddressOfEntryPoint", 0x10); - addOH(NodeKind::UInt32, "BaseOfCode", 0x14); - addOH(NodeKind::UInt64, "ImageBase", 0x18); - addOH(NodeKind::UInt32, "SectionAlignment", 0x20); - addOH(NodeKind::UInt32, "FileAlignment", 0x24); - addOH(NodeKind::UInt16, "MajorOSVersion", 0x28); - addOH(NodeKind::UInt16, "MinorOSVersion", 0x2A); - addOH(NodeKind::UInt16, "MajorImageVersion", 0x2C); - addOH(NodeKind::UInt16, "MinorImageVersion", 0x2E); - addOH(NodeKind::UInt16, "MajorSubsysVersion", 0x30); - addOH(NodeKind::UInt16, "MinorSubsysVersion", 0x32); - addOH(NodeKind::UInt32, "Win32VersionValue", 0x34); - addOH(NodeKind::UInt32, "SizeOfImage", 0x38); - addOH(NodeKind::UInt32, "SizeOfHeaders", 0x3C); - addOH(NodeKind::UInt32, "CheckSum", 0x40); - addOH(NodeKind::UInt16, "Subsystem", 0x44); - addOH(NodeKind::UInt16, "DllCharacteristics", 0x46); - addOH(NodeKind::UInt64, "SizeOfStackReserve", 0x48); - addOH(NodeKind::UInt64, "SizeOfStackCommit", 0x50); - addOH(NodeKind::UInt64, "SizeOfHeapReserve", 0x58); - addOH(NodeKind::UInt64, "SizeOfHeapCommit", 0x60); - addOH(NodeKind::UInt32, "LoaderFlags", 0x68); - addOH(NodeKind::UInt32, "NumberOfRvaAndSizes", 0x6C); - - // Data directories (16 entries × 8 bytes) - static const char* dirNames[] = { - "Export", "Import", "Resource", "Exception", - "Security", "BaseReloc", "Debug", "Architecture", - "GlobalPtr", "TLS", "LoadConfig", "BoundImport", - "IAT", "DelayImport", "CLR", "Reserved" - }; - for (int i = 0; i < 16; i++) { - int doff = 0x70 + i * 8; - addOH(NodeKind::UInt32, QString("%1_RVA").arg(dirNames[i]), doff); - addOH(NodeKind::UInt32, QString("%1_Size").arg(dirNames[i]), doff + 4); - } + // ── Section Headers (4 sections × 40 bytes) ── + const int shOff = ddOff + 128; // 0x188 + struct SectionDef { const char* name; uint32_t vsize; uint32_t vaddr; uint32_t rawsz; uint32_t rawptr; uint32_t chars; }; + SectionDef sections[4] = { + {".text", 0x00011234, 0x00001000, 0x00011400, 0x00000200, 0x60000020}, // CODE|EXECUTE|READ + {".rdata", 0x00002ABC, 0x00013000, 0x00002C00, 0x00011600, 0x40000040}, // INITIALIZED|READ + {".data", 0x00001000, 0x00016000, 0x00000400, 0x00014200, 0xC0000040}, // INITIALIZED|READ|WRITE + {".pdata", 0x00000800, 0x00017000, 0x00000800, 0x00014600, 0x40000040}, // INITIALIZED|READ + }; + for (int i = 0; i < 4; i++) { + int off = shOff + i * 40; + memcpy(d + off, sections[i].name, 8); // Name[8] + *(uint32_t*)(d + off + 8) = sections[i].vsize; // VirtualSize + *(uint32_t*)(d + off + 12) = sections[i].vaddr; // VirtualAddress + *(uint32_t*)(d + off + 16) = sections[i].rawsz; // SizeOfRawData + *(uint32_t*)(d + off + 20) = sections[i].rawptr; // PointerToRawData + *(uint32_t*)(d + off + 24) = 0x00000000; // PointerToRelocations + *(uint32_t*)(d + off + 28) = 0x00000000; // PointerToLinenumbers + *(uint16_t*)(d + off + 32) = 0x0000; // NumberOfRelocations + *(uint16_t*)(d + off + 34) = 0x0000; // NumberOfLinenumbers + *(uint32_t*)(d + off + 36) = sections[i].chars; // Characteristics } - // ── Fill with Hex64 until 0x6000 for stress testing ── - int padStart = oh + 0xF0; // end of optional header - for (int off = padStart; off < 0x6000; off += 8) { - add(NodeKind::Hex64, - QString("data_%1").arg(off, 4, 16, QChar('0')), - off); + doc->loadData(peData); + doc->tree.baseAddress = 0x140000000; // Typical 64-bit image base + + // ══════════════════════════════════════════════════════════════════════════ + // Build Node Tree + // ══════════════════════════════════════════════════════════════════════════ + + auto addField = [&](uint64_t parent, int offset, NodeKind kind, const QString& name) -> uint64_t { + Node n; + n.kind = kind; + n.name = name; + n.parentId = parent; + n.offset = offset; + int idx = doc->tree.addNode(n); + return doc->tree.nodes[idx].id; + }; + + auto addStruct = [&](uint64_t parent, int offset, const QString& name) -> uint64_t { + Node n; + n.kind = NodeKind::Struct; + n.name = name; + n.parentId = parent; + n.offset = offset; + int idx = doc->tree.addNode(n); + return doc->tree.nodes[idx].id; + }; + + auto addArray = [&](uint64_t parent, int offset, const QString& name, int count, NodeKind elemKind) -> uint64_t { + Node n; + n.kind = NodeKind::Array; + n.name = name; + n.parentId = parent; + n.offset = offset; + n.arrayLen = count; + n.elementKind = elemKind; + int idx = doc->tree.addNode(n); + return doc->tree.nodes[idx].id; + }; + + // ── Root: IMAGE_DOS_HEADER ── + uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER"); + addField(dosId, 0x00, NodeKind::UInt16, "e_magic"); + addField(dosId, 0x02, NodeKind::UInt16, "e_cblp"); + addField(dosId, 0x04, NodeKind::UInt16, "e_cp"); + addField(dosId, 0x06, NodeKind::UInt16, "e_crlc"); + addField(dosId, 0x08, NodeKind::UInt16, "e_cparhdr"); + addField(dosId, 0x0A, NodeKind::UInt16, "e_minalloc"); + addField(dosId, 0x0C, NodeKind::UInt16, "e_maxalloc"); + addField(dosId, 0x0E, NodeKind::UInt16, "e_ss"); + addField(dosId, 0x10, NodeKind::UInt16, "e_sp"); + addField(dosId, 0x12, NodeKind::UInt16, "e_csum"); + addField(dosId, 0x14, NodeKind::UInt16, "e_ip"); + addField(dosId, 0x16, NodeKind::UInt16, "e_cs"); + addField(dosId, 0x18, NodeKind::UInt16, "e_lfarlc"); + addField(dosId, 0x1A, NodeKind::UInt16, "e_ovno"); + addField(dosId, 0x3C, NodeKind::UInt32, "e_lfanew"); + + // ── PE Signature ── + addField(0, peOff, NodeKind::UInt32, "PE_Signature"); + + // ── IMAGE_FILE_HEADER ── + uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER"); + addField(fhId, 0, NodeKind::UInt16, "Machine"); + addField(fhId, 2, NodeKind::UInt16, "NumberOfSections"); + addField(fhId, 4, NodeKind::UInt32, "TimeDateStamp"); + addField(fhId, 8, NodeKind::UInt32, "PointerToSymbolTable"); + addField(fhId, 12, NodeKind::UInt32, "NumberOfSymbols"); + addField(fhId, 16, NodeKind::UInt16, "SizeOfOptionalHeader"); + addField(fhId, 18, NodeKind::UInt16, "Characteristics"); + + // ── IMAGE_OPTIONAL_HEADER64 ── + uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64"); + addField(ohId, 0, NodeKind::UInt16, "Magic"); + addField(ohId, 2, NodeKind::UInt8, "MajorLinkerVersion"); + addField(ohId, 3, NodeKind::UInt8, "MinorLinkerVersion"); + addField(ohId, 4, NodeKind::UInt32, "SizeOfCode"); + addField(ohId, 8, NodeKind::UInt32, "SizeOfInitializedData"); + addField(ohId, 12, NodeKind::UInt32, "SizeOfUninitializedData"); + addField(ohId, 16, NodeKind::UInt32, "AddressOfEntryPoint"); + addField(ohId, 20, NodeKind::UInt32, "BaseOfCode"); + addField(ohId, 24, NodeKind::UInt64, "ImageBase"); + addField(ohId, 32, NodeKind::UInt32, "SectionAlignment"); + addField(ohId, 36, NodeKind::UInt32, "FileAlignment"); + addField(ohId, 40, NodeKind::UInt16, "MajorOSVersion"); + addField(ohId, 42, NodeKind::UInt16, "MinorOSVersion"); + addField(ohId, 44, NodeKind::UInt16, "MajorImageVersion"); + addField(ohId, 46, NodeKind::UInt16, "MinorImageVersion"); + addField(ohId, 48, NodeKind::UInt16, "MajorSubsystemVersion"); + addField(ohId, 50, NodeKind::UInt16, "MinorSubsystemVersion"); + addField(ohId, 52, NodeKind::UInt32, "Win32VersionValue"); + addField(ohId, 56, NodeKind::UInt32, "SizeOfImage"); + addField(ohId, 60, NodeKind::UInt32, "SizeOfHeaders"); + addField(ohId, 64, NodeKind::UInt32, "CheckSum"); + addField(ohId, 68, NodeKind::UInt16, "Subsystem"); + addField(ohId, 70, NodeKind::UInt16, "DllCharacteristics"); + addField(ohId, 72, NodeKind::UInt64, "SizeOfStackReserve"); + addField(ohId, 80, NodeKind::UInt64, "SizeOfStackCommit"); + addField(ohId, 88, NodeKind::UInt64, "SizeOfHeapReserve"); + addField(ohId, 96, NodeKind::UInt64, "SizeOfHeapCommit"); + addField(ohId, 104, NodeKind::UInt32, "LoaderFlags"); + addField(ohId, 108, NodeKind::UInt32, "NumberOfRvaAndSizes"); + + // ── Data Directories Array (16 entries) ── + uint64_t ddArrId = addArray(ohId, 112, "DataDirectory", 16, NodeKind::Struct); + const char* ddNames[16] = { + "Export", "Import", "Resource", "Exception", + "Security", "BaseReloc", "Debug", "Architecture", + "GlobalPtr", "TLS", "LoadConfig", "BoundImport", + "IAT", "DelayImport", "CLR", "Reserved" + }; + for (int i = 0; i < 16; i++) { + uint64_t entryId = addStruct(ddArrId, i * 8, QString("[%1] %2").arg(i).arg(ddNames[i])); + addField(entryId, 0, NodeKind::UInt32, "VirtualAddress"); + addField(entryId, 4, NodeKind::UInt32, "Size"); + } + + // ── Section Headers Array (4 sections) ── + uint64_t shArrId = addArray(0, shOff, "SectionHeaders", 4, NodeKind::Struct); + const char* secNames[4] = {".text", ".rdata", ".data", ".pdata"}; + for (int i = 0; i < 4; i++) { + uint64_t secId = addStruct(shArrId, i * 40, QString("[%1] %2").arg(i).arg(secNames[i])); + // Name is 8 bytes - show as UTF8 string + Node nameNode; + nameNode.kind = NodeKind::UTF8; + nameNode.name = "Name"; + nameNode.parentId = secId; + nameNode.offset = 0; + nameNode.strLen = 8; + doc->tree.addNode(nameNode); + addField(secId, 8, NodeKind::UInt32, "VirtualSize"); + addField(secId, 12, NodeKind::UInt32, "VirtualAddress"); + addField(secId, 16, NodeKind::UInt32, "SizeOfRawData"); + addField(secId, 20, NodeKind::UInt32, "PointerToRawData"); + addField(secId, 24, NodeKind::UInt32, "PointerToRelocations"); + addField(secId, 28, NodeKind::UInt32, "PointerToLinenumbers"); + addField(secId, 32, NodeKind::UInt16, "NumberOfRelocations"); + addField(secId, 34, NodeKind::UInt16, "NumberOfLinenumbers"); + addField(secId, 36, NodeKind::UInt32, "Characteristics"); } createTab(doc); diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index fa8f17c..190481e 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -608,7 +608,7 @@ private slots: QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion) } - void testStructFooterSizeof() { + void testStructFooterSimple() { NodeTree tree; tree.baseAddress = 0; @@ -627,13 +627,6 @@ private slots: f1.offset = 0; tree.addNode(f1); - Node f2; - f2.kind = NodeKind::UInt64; - f2.name = "b"; - f2.parentId = rootId; - f2.offset = 4; - tree.addNode(f2); - NullProvider prov; ComposeResult result = compose(tree, prov); @@ -641,9 +634,10 @@ private slots: int lastLine = result.meta.size() - 1; QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer); - // Footer text should contain sizeof(Sized)=0xC (4+8=12=0xC) + // Footer text should just be "};" (no sizeof) QString footerText = result.text.split('\n').last(); - QVERIFY(footerText.contains("sizeof(Sized)=0xC")); + QVERIFY(footerText.contains("};")); + QVERIFY(!footerText.contains("sizeof")); } void testLineMetaHasNodeId() { @@ -669,115 +663,6 @@ private slots: } } - void testSizeofUpdatesAfterDelete() { - // Test that sizeof recalculates after deleting a node - NodeTree tree; - tree.baseAddress = 0; - - Node root; - root.kind = NodeKind::Struct; - root.name = "Test"; - root.parentId = 0; - root.offset = 0; - int ri = tree.addNode(root); - uint64_t rootId = tree.nodes[ri].id; - - Node f1; - f1.kind = NodeKind::UInt32; - f1.name = "a"; - f1.parentId = rootId; - f1.offset = 0; - tree.addNode(f1); - - Node f2; - f2.kind = NodeKind::UInt64; - f2.name = "b"; - f2.parentId = rootId; - f2.offset = 4; - int f2i = tree.addNode(f2); - uint64_t f2Id = tree.nodes[f2i].id; - - NullProvider prov; - - // First compose: sizeof should be 0xC (4+8=12) - ComposeResult result1 = compose(tree, prov); - QString footer1 = result1.text.split('\n').last(); - QVERIFY2(footer1.contains("sizeof(Test)=0xC"), - qPrintable("Before delete: " + footer1)); - - // Delete the second field - int idx = tree.indexOfId(f2Id); - QVERIFY(idx >= 0); - tree.nodes.remove(idx); - tree.invalidateIdCache(); - - // Second compose: sizeof should be 0x4 (only UInt32 remains) - ComposeResult result2 = compose(tree, prov); - QString footer2 = result2.text.split('\n').last(); - QVERIFY2(footer2.contains("sizeof(Test)=0x4"), - qPrintable("After delete: " + footer2)); - } - - void testNestedStructSizeofUpdates() { - // Test nested struct sizeof updates when child is deleted - NodeTree tree; - tree.baseAddress = 0; - - // Root struct - Node root; - root.kind = NodeKind::Struct; - root.name = "Root"; - root.parentId = 0; - root.offset = 0; - int ri = tree.addNode(root); - uint64_t rootId = tree.nodes[ri].id; - - // Nested struct (like IMAGE_FILE_HEADER) - Node nested; - nested.kind = NodeKind::Struct; - nested.name = "Nested"; - nested.parentId = rootId; - nested.offset = 0; - int ni = tree.addNode(nested); - uint64_t nestedId = tree.nodes[ni].id; - - // Field in nested struct - Node f1; - f1.kind = NodeKind::UInt32; - f1.name = "a"; - f1.parentId = nestedId; - f1.offset = 0; - tree.addNode(f1); - - Node f2; - f2.kind = NodeKind::UInt32; - f2.name = "b"; - f2.parentId = nestedId; - f2.offset = 4; - int f2i = tree.addNode(f2); - uint64_t f2Id = tree.nodes[f2i].id; - - NullProvider prov; - - // First compose - ComposeResult result1 = compose(tree, prov); - // Find nested struct footer - QString text1 = result1.text; - QVERIFY2(text1.contains("sizeof(Nested)=0x8"), - qPrintable("Before delete nested sizeof: " + text1)); - - // Delete field from nested struct - int idx = tree.indexOfId(f2Id); - QVERIFY(idx >= 0); - tree.nodes.remove(idx); - tree.invalidateIdCache(); - - // Second compose - nested sizeof should update - ComposeResult result2 = compose(tree, prov); - QString text2 = result2.text; - QVERIFY2(text2.contains("sizeof(Nested)=0x4"), - qPrintable("After delete nested sizeof: " + text2)); - } }; QTEST_MAIN(TestCompose) diff --git a/tests/test_core.cpp b/tests/test_core.cpp index 38a15ee..fdb4e68 100644 --- a/tests/test_core.cpp +++ b/tests/test_core.cpp @@ -326,17 +326,17 @@ private slots: auto ts = rcx::typeSpanFor(lm); QVERIFY(ts.valid); QCOMPARE(ts.start, 6); - QCOMPARE(ts.end, 16); // 6 + 10 + QCOMPARE(ts.end, 20); // 6 + 14 (kColType) auto ns = rcx::nameSpanFor(lm); QVERIFY(ns.valid); - QCOMPARE(ns.start, 18); // 6 + 10 + 2 - QCOMPARE(ns.end, 42); // 18 + 24 + QCOMPARE(ns.start, 22); // 6 + 14 + 2 + QCOMPARE(ns.end, 44); // 22 + 22 (kColName) - auto vs = rcx::valueSpanFor(lm, 60); + auto vs = rcx::valueSpanFor(lm, 100); QVERIFY(vs.valid); - QCOMPARE(vs.start, 44); // 18 + 24 + 2 - QCOMPARE(vs.end, 60); + QCOMPARE(vs.start, 46); // 22 + 22 + 2 + QCOMPARE(vs.end, 78); // 46 + 32 (kColValue) } void testColumnSpan_continuation() { @@ -349,10 +349,10 @@ private slots: QVERIFY(!rcx::typeSpanFor(lm).valid); QVERIFY(!rcx::nameSpanFor(lm).valid); - auto vs = rcx::valueSpanFor(lm, 60); + auto vs = rcx::valueSpanFor(lm, 100); QVERIFY(vs.valid); - QCOMPARE(vs.start, 6 + 10 + 24 + 4); // kFoldCol+indent + COL_TYPE + COL_NAME + 4 - QCOMPARE(vs.end, 60); + QCOMPARE(vs.start, 6 + 14 + 22 + 4); // kFoldCol+indent + kColType(14) + kColName(22) + 4 + QCOMPARE(vs.end, 46 + 32); // start + kColValue } void testColumnSpan_headerFooter() { @@ -382,17 +382,17 @@ private slots: auto ts = rcx::typeSpanFor(lm); QVERIFY(ts.valid); QCOMPARE(ts.start, 3); - QCOMPARE(ts.end, 13); // 3 + 10 + QCOMPARE(ts.end, 17); // 3 + 14 (kColType) auto ns = rcx::nameSpanFor(lm); QVERIFY(ns.valid); - QCOMPARE(ns.start, 15); // 3 + 10 + 2 - QCOMPARE(ns.end, 39); // 15 + 24 + QCOMPARE(ns.start, 19); // 3 + 14 + 2 + QCOMPARE(ns.end, 41); // 19 + 22 (kColName) - auto vs = rcx::valueSpanFor(lm, 50); + auto vs = rcx::valueSpanFor(lm, 100); QVERIFY(vs.valid); - QCOMPARE(vs.start, 41); // 15 + 24 + 2 - QCOMPARE(vs.end, 50); + QCOMPARE(vs.start, 43); // 19 + 22 + 2 + QCOMPARE(vs.end, 75); // 43 + 32 (kColValue) } void testNodeIdJsonRoundTrip() { diff --git a/tests/test_format.cpp b/tests/test_format.cpp index 3c783fd..9fcc259 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -9,12 +9,13 @@ private slots: void testTypeName() { QString s = fmt::typeName(NodeKind::Float); QVERIFY(s.trimmed() == "float"); - QCOMPARE(s.size(), 10); // COL_TYPE + QCOMPARE(s.size(), 14); // kColType } void testFmtInt32() { - QCOMPARE(fmt::fmtInt32(-42), QString("-42")); - QCOMPARE(fmt::fmtInt32(0), QString("0")); + // fmtInt32 outputs hex representation (0xffffffd6 for -42) + QCOMPARE(fmt::fmtInt32(-42), QString("0xffffffd6")); + QCOMPARE(fmt::fmtInt32(0), QString("0x0")); } void testFmtFloat() { @@ -224,25 +225,15 @@ private slots: QVERIFY(!ok); } - void testFmtStructFooterWithSize() { + void testFmtStructFooterSimple() { Node n; n.kind = NodeKind::Struct; n.name = "Test"; - // With size - QString s1 = fmt::fmtStructFooter(n, 0, 0x14); - QVERIFY(s1.contains("};")); - QVERIFY(s1.contains("sizeof(Test)=0x14")); - - // Size 0 → no sizeof - QString s2 = fmt::fmtStructFooter(n, 0, 0); - QVERIFY(s2.contains("};")); - QVERIFY(!s2.contains("sizeof")); - - // Default (no size arg) → no sizeof - QString s3 = fmt::fmtStructFooter(n, 0); - QVERIFY(s3.contains("};")); - QVERIFY(!s3.contains("sizeof")); + // Footer is always just "};" (no sizeof comment) + QString s = fmt::fmtStructFooter(n, 0, 0x14); + QVERIFY(s.contains("};")); + QVERIFY(!s.contains("sizeof")); // No sizeof comment } };