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 <noreply@anthropic.com>
This commit is contained in:
IChooChoose
2026-02-05 06:26:00 -07:00
committed by sysadmin
parent 04252a3c96
commit 4d35db224e
11 changed files with 727 additions and 430 deletions

21
LICENSE Normal file
View File

@@ -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.

View File

@@ -16,12 +16,24 @@ struct ComposeState {
QSet<uint64_t> visiting; // cycle detection for struct recursion QSet<uint64_t> visiting; // cycle detection for struct recursion
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
int currentLine = 0; 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 // Precomputed for O(1) lookups
QHash<uint64_t, QVector<int>> childMap; QHash<uint64_t, QVector<int>> childMap;
QVector<int64_t> absOffsets; // indexed by node index QVector<int64_t> absOffsets; // indexed by node index
// Per-scope column widths (containerId -> width for direct children)
QHash<uint64_t, int> scopeTypeW;
QHash<uint64_t, int> 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) { void emitLine(const QString& lineText, LineMeta lm) {
if (currentLine > 0) text += '\n'; if (currentLine > 0) text += '\n';
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other // 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, void composeLeaf(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, const Provider& prov, int nodeIdx,
int depth, uint64_t absAddr) { int depth, uint64_t absAddr, uint64_t scopeId) {
const Node& node = tree.nodes[nodeIdx]; 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 // Line count: padding wraps at 8 bytes per line
int numLines; int numLines;
if (node.kind == NodeKind::Padding) { 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.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont);
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth); lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
lm.foldLevel = computeFoldLevel(depth, false); lm.foldLevel = computeFoldLevel(depth, false);
lm.effectiveTypeW = typeW;
lm.effectiveNameW = nameW;
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub, QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
/*comment=*/{}, state.nameW); /*comment=*/{}, typeW, nameW);
state.emitLine(lineText, lm); 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) // Forward declarations (base/rootId default to 0 = use precomputed offsets)
void composeNode(ComposeState& state, const NodeTree& tree, void composeNode(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, int depth, 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, void composeParent(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, int depth, 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, void composeParent(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, int depth, 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]; const Node& node = tree.nodes[nodeIdx];
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId); 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); 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; LineMeta lm;
lm.nodeIdx = nodeIdx; lm.nodeIdx = nodeIdx;
lm.nodeId = node.id; lm.nodeId = node.id;
@@ -170,28 +206,46 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.foldCollapsed = node.collapsed; lm.foldCollapsed = node.collapsed;
lm.foldLevel = computeFoldLevel(depth, true); lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = (1u << M_STRUCT_BG); 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;
QString headerText = lm.isRootHeader if (node.kind == NodeKind::Array) {
? fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress) // Array header with navigation: "uint32_t[16] name { <0/16>"
: fmt::fmtStructHeader(node, depth); 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); state.emitLine(headerText, lm);
} }
if (!node.collapsed) { if (!node.collapsed || isArrayChild) {
QVector<int> children = state.childMap.value(node.id); QVector<int> children = state.childMap.value(node.id);
std::sort(children.begin(), children.end(), [&](int a, int b) { std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset; 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) { 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; LineMeta lm;
lm.nodeIdx = nodeIdx; lm.nodeIdx = nodeIdx;
lm.nodeId = node.id; lm.nodeId = node.id;
@@ -210,10 +264,15 @@ void composeParent(ComposeState& state, const NodeTree& tree,
void composeNode(ComposeState& state, const NodeTree& tree, void composeNode(ComposeState& state, const NodeTree& tree,
const Provider& prov, int nodeIdx, int depth, 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]; const Node& node = tree.nodes[nodeIdx];
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId); 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 // Pointer deref expansion
if ((node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) if ((node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64)
&& node.refId != 0) { && node.refId != 0) {
@@ -229,7 +288,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.foldCollapsed = node.collapsed; lm.foldCollapsed = node.collapsed;
lm.foldLevel = computeFoldLevel(depth, true); lm.foldLevel = computeFoldLevel(depth, true);
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth); 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) { if (!node.collapsed) {
int sz = node.byteSize(); int sz = node.byteSize();
@@ -257,9 +318,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
} }
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) { 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 { } 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++) for (int i = 0; i < tree.nodes.size(); i++)
state.absOffsets[i] = tree.computeOffset(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 // Compute effective name column width from longest name
int maxNameLen = kMinNameW; int maxNameLen = kMinNameW;
for (const Node& node : tree.nodes) { 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); 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<int> roots = state.childMap.value(0); QVector<int> roots = state.childMap.value(0);
std::sort(roots.begin(), roots.end(), [&](int a, int b) { std::sort(roots.begin(), roots.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset; 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); 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<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const { QSet<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const {

View File

@@ -11,6 +11,9 @@
namespace rcx { 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::RcxDocument(QObject* parent) RcxDocument::RcxDocument(QObject* parent)
@@ -137,9 +140,33 @@ void RcxController::connectEditor(RcxEditor* editor) {
break; break;
} }
case EditTarget::Type: { case EditTarget::Type: {
bool ok; // Check for array type syntax: "type[count]" e.g. "int32_t[10]"
NodeKind k = kindFromTypeName(text, &ok); int bracketPos = text.indexOf('[');
if (ok) changeNodeKind(nodeIdx, k); 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; break;
} }
case EditTarget::Value: case EditTarget::Value:
@@ -197,6 +224,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
} }
break; 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.) // Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
refresh(); refresh();
@@ -640,20 +671,30 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
bool ctrl = mods & Qt::ControlModifier; bool ctrl = mods & Qt::ControlModifier;
bool shift = mods & Qt::ShiftModifier; 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) { if (!ctrl && !shift) {
m_selIds.clear(); m_selIds.clear();
m_selIds.insert(nodeId); m_selIds.insert(selId);
m_anchorLine = line; m_anchorLine = line;
} else if (ctrl && !shift) { } else if (ctrl && !shift) {
if (m_selIds.contains(nodeId)) if (m_selIds.contains(selId))
m_selIds.remove(nodeId); m_selIds.remove(selId);
else else
m_selIds.insert(nodeId); m_selIds.insert(selId);
m_anchorLine = line; m_anchorLine = line;
} else if (shift && !ctrl) { } else if (shift && !ctrl) {
if (m_anchorLine < 0) { if (m_anchorLine < 0) {
m_selIds.clear(); m_selIds.clear();
m_selIds.insert(nodeId); m_selIds.insert(selId);
m_anchorLine = line; m_anchorLine = line;
} else { } else {
m_selIds.clear(); m_selIds.clear();
@@ -661,19 +702,19 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
int to = qMax(m_anchorLine, line); int to = qMax(m_anchorLine, line);
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) { for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
uint64_t nid = m_lastResult.meta[i].nodeId; 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 } else { // Ctrl+Shift
if (m_anchorLine < 0) { if (m_anchorLine < 0) {
m_selIds.insert(nodeId); m_selIds.insert(selId);
m_anchorLine = line; m_anchorLine = line;
} else { } else {
int from = qMin(m_anchorLine, line); int from = qMin(m_anchorLine, line);
int to = qMax(m_anchorLine, line); int to = qMax(m_anchorLine, line);
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) { for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
uint64_t nid = m_lastResult.meta[i].nodeId; 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) { if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin(); 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); if (idx >= 0) emit nodeSelected(idx);
} }
} }

View File

@@ -235,16 +235,19 @@ struct Node {
QString name; QString name;
uint64_t parentId = 0; // 0 = root (no parent) uint64_t parentId = 0; // 0 = root (no parent)
int offset = 0; int offset = 0;
int arrayLen = 0; int arrayLen = 1; // Array: element count
int strLen = 64; int strLen = 64;
bool collapsed = false; bool collapsed = false;
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr 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 { int byteSize() const {
switch (kind) { switch (kind) {
case NodeKind::UTF8: return strLen; case NodeKind::UTF8: return strLen;
case NodeKind::UTF16: return strLen * 2; case NodeKind::UTF16: return strLen * 2;
case NodeKind::Padding: return qMax(1, arrayLen); case NodeKind::Padding: return qMax(1, arrayLen);
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
default: return sizeForKind(kind); default: return sizeForKind(kind);
} }
} }
@@ -260,6 +263,7 @@ struct Node {
o["strLen"] = strLen; o["strLen"] = strLen;
o["collapsed"] = collapsed; o["collapsed"] = collapsed;
o["refId"] = QString::number(refId); o["refId"] = QString::number(refId);
o["elementKind"] = kindToString(elementKind);
return o; return o;
} }
static Node fromJson(const QJsonObject& o) { static Node fromJson(const QJsonObject& o) {
@@ -269,12 +273,19 @@ struct Node {
n.name = o["name"].toString(); n.name = o["name"].toString();
n.parentId = o["parentId"].toString("0").toULongLong(); n.parentId = o["parentId"].toString("0").toULongLong();
n.offset = o["offset"].toInt(0); 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.strLen = o["strLen"].toInt(64);
n.collapsed = o["collapsed"].toBool(false); n.collapsed = o["collapsed"].toBool(false);
n.refId = o["refId"].toString("0").toULongLong(); n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
return n; 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 ── // ── NodeTree ──
@@ -415,7 +426,7 @@ struct NodeTree {
// ── LineMeta ── // ── LineMeta ──
enum class LineKind : uint8_t { enum class LineKind : uint8_t {
Header, Field, Continuation, Footer Header, Field, Continuation, Footer, ArrayElementSeparator
}; };
struct LineMeta { struct LineMeta {
@@ -428,15 +439,23 @@ struct LineMeta {
bool foldCollapsed = false; bool foldCollapsed = false;
bool isContinuation = false; bool isContinuation = false;
bool isRootHeader = false; // true for top-level struct headers (base address editable) bool isRootHeader = false; // true for top-level struct headers (base address editable)
bool isArrayHeader = false; // true for array headers (has <idx/count> nav)
LineKind lineKind = LineKind::Field; LineKind lineKind = LineKind::Field;
NodeKind nodeKind = NodeKind::Int32; 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; QString offsetText;
uint32_t markerMask = 0; 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 ── // ── Layout Info ──
struct LayoutInfo { struct LayoutInfo {
int typeW = 14; // Effective type column width (default = kColType)
int nameW = 22; // Effective name column width (default = kColName) int nameW = 22; // Effective name column width (default = kColName)
}; };
@@ -476,30 +495,32 @@ struct ColumnSpan {
bool valid = false; 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) // Column layout constants (shared with format.cpp span computation)
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line 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 kColName = 22;
inline constexpr int kColValue = 32; inline constexpr int kColValue = 32;
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits 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 kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
inline constexpr int kSepWidth = 2; 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 kMinNameW = 8; // Minimum name column width (matches ASCII preview)
inline constexpr int kMaxNameW = 22; // Maximum name column width (= kColName) 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 {}; if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
int ind = kFoldCol + lm.depth * 3; 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 {}; if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
int ind = kFoldCol + lm.depth * 3; 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) // Hex/Padding: ASCII preview takes the name column position (8 chars)
if (isHexPreview(lm.nodeKind)) if (isHexPreview(lm.nodeKind))
@@ -508,8 +529,9 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int nameW = kColName) {
return {start, start + nameW, true}; return {start, start + nameW, true};
} }
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int nameW = kColName) { inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) {
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
int ind = kFoldCol + lm.depth * 3; int ind = kFoldCol + lm.depth * 3;
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)] // 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) { if (lm.isContinuation) {
int prefixW = isHexPad int prefixW = isHexPad
? (kColType + kSepWidth + 8 + kSepWidth) ? (typeW + kSepWidth + 8 + kSepWidth)
: (kColType + nameW + 4); : (typeW + nameW + 4);
int start = ind + prefixW; int start = ind + prefixW;
return {start, start + valWidth, true}; return {start, start + valWidth, true};
} }
if (lm.lineKind != LineKind::Field) return {}; if (lm.lineKind != LineKind::Field) return {};
int start = isHexPad int start = isHexPad
? (ind + kColType + kSepWidth + 8 + kSepWidth) ? (ind + typeW + kSepWidth + 8 + kSepWidth)
: (ind + kColType + kSepWidth + nameW + kSepWidth); : (ind + typeW + kSepWidth + nameW + kSepWidth);
return {start, start + valWidth, true}; 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 {}; if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
int ind = kFoldCol + lm.depth * 3; int ind = kFoldCol + lm.depth * 3;
@@ -541,13 +563,13 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int nameW =
int start; int start;
if (lm.isContinuation) { if (lm.isContinuation) {
int prefixW = isHexPad int prefixW = isHexPad
? (kColType + kSepWidth + 8 + kSepWidth) ? (typeW + kSepWidth + 8 + kSepWidth)
: (kColType + nameW + 4); : (typeW + nameW + 4);
start = ind + prefixW + valWidth; start = ind + prefixW + valWidth;
} else { } else {
start = isHexPad start = isHexPad
? (ind + kColType + kSepWidth + 8 + kSepWidth + valWidth) ? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth)
: (ind + kColType + kSepWidth + nameW + kSepWidth + valWidth); : (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth);
} }
return {start, lineLength, start < lineLength}; return {start, lineLength, start < lineLength};
} }
@@ -579,6 +601,39 @@ inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& line
return {baseIdx, endPos, true}; 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 ── // ── ViewState ──
struct ViewState { struct ViewState {
@@ -592,7 +647,8 @@ struct ViewState {
namespace fmt { namespace fmt {
using TypeNameFn = QString (*)(NodeKind); using TypeNameFn = QString (*)(NodeKind);
void setTypeNameProvider(TypeNameFn fn); 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 fmtInt8(int8_t v);
QString fmtInt16(int16_t v); QString fmtInt16(int16_t v);
QString fmtInt32(int32_t v); QString fmtInt32(int32_t v);
@@ -608,11 +664,13 @@ namespace fmt {
QString fmtPointer64(uint64_t v); QString fmtPointer64(uint64_t v);
QString fmtNodeLine(const Node& node, const Provider& prov, QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine = 0, 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 fmtOffsetMargin(int64_t relativeOffset, bool isContinuation);
QString fmtStructHeader(const Node& node, int depth); QString fmtStructHeader(const Node& node, int depth);
QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress); QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress);
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1); 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 validateBaseAddress(const QString& text);
QString indent(int depth); QString indent(int depth);
QString readValue(const Node& node, const Provider& prov, QString readValue(const Node& node, const Provider& prov,

View File

@@ -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_BASE_ADDR = 10; // Green color for base address
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like) 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 QString g_fontName = "Consolas";
static QFont editorFont() { static QFont editorFont() {
@@ -388,9 +391,15 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
for (int i = 0; i < m_meta.size(); i++) { 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); 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_editState.active) return;
if (!m_hoverInside) return; if (!m_hoverInside) return;
if (m_hoveredNodeId == 0) return; if (m_hoveredNodeId == 0) return;
if (m_currentSelIds.contains(m_hoveredNodeId)) return;
for (int i = 0; i < m_meta.size(); i++) { // Check if hovered line is a footer - footers highlight independently
if (m_meta[i].nodeId == m_hoveredNodeId) bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
m_sci->markerAdd(i, M_HOVER); 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 ── // ── Column span computation ──
ColumnSpan RcxEditor::typeSpan(const LineMeta& lm) { return typeSpanFor(lm); } ColumnSpan RcxEditor::typeSpan(const LineMeta& lm, int typeW) { return typeSpanFor(lm, typeW); }
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int nameW) { return nameSpanFor(lm, nameW); } ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int typeW, int nameW) { return nameSpanFor(lm, typeW, nameW); }
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int nameW) { return valueSpanFor(lm, lineLength, nameW); } ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int typeW, int nameW) { return valueSpanFor(lm, lineLength, typeW, nameW); }
// ── Multi-selection ── // ── Multi-selection ──
@@ -538,9 +562,24 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
int ind = kFoldCol + lm.depth * 3; int ind = kFoldCol + lm.depth * 3;
int typeEnd = lineText.indexOf(' ', ind); int typeEnd = lineText.indexOf(' ', ind);
if (typeEnd <= ind || typeEnd >= bracePos) return {}; 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}; 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( RcxEditor::NormalizedSpan RcxEditor::normalizeSpan(
const ColumnSpan& raw, const QString& lineText, const ColumnSpan& raw, const QString& lineText,
EditTarget target, bool skipPrefixes) const EditTarget target, bool skipPrefixes) const
@@ -589,14 +628,24 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
QString lineText = getLineText(m_sci, line); QString lineText = getLineText(m_sci, line);
int textLen = lineText.size(); 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; ColumnSpan s;
switch (t) { switch (t) {
case EditTarget::Type: s = typeSpan(*lm); break; case EditTarget::Type: s = typeSpan(*lm, typeW); break;
case EditTarget::Name: s = nameSpan(*lm, m_layout.nameW); break; case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break;
case EditTarget::Value: s = valueSpan(*lm, textLen, m_layout.nameW); break; case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break;
case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); 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) if (!s.valid && t == EditTarget::Name)
s = headerNameSpan(*lm, lineText); s = headerNameSpan(*lm, lineText);
@@ -640,8 +689,7 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
static bool hitTestTarget(QsciScintilla* sci, static bool hitTestTarget(QsciScintilla* sci,
const QVector<LineMeta>& meta, const QVector<LineMeta>& meta,
const QPoint& viewportPos, const QPoint& viewportPos,
int& outLine, EditTarget& outTarget, int& outLine, EditTarget& outTarget)
int nameW = kColName)
{ {
long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE, long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
(unsigned long)viewportPos.x(), (long)viewportPos.y()); (unsigned long)viewportPos.x(), (long)viewportPos.y());
@@ -656,18 +704,29 @@ static bool hitTestTarget(QsciScintilla* sci,
int textLen = lineText.size(); int textLen = lineText.size();
const LineMeta& lm = meta[line]; 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) // Array element separators are not interactive
ns = headerNameSpan(lm, lineText); 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) { auto inSpan = [&](const ColumnSpan& s) {
return s.valid && col >= s.start && col < s.end; 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; if (inSpan(bs)) outTarget = EditTarget::BaseAddress;
else if (inSpan(ts)) outTarget = EditTarget::Type; else if (inSpan(ts)) outTarget = EditTarget::Type;
else if (inSpan(ns)) outTarget = EditTarget::Name; 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); const LineMeta* lm = metaForLine(m_editState.line);
if (lm) { if (lm) {
QString lineText = getLineText(m_sci, h.line); QString lineText = getLineText(m_sci, h.line);
// Use per-line effective widths
int typeW = lm->effectiveTypeW;
int nameW = lm->effectiveNameW;
ColumnSpan raw; ColumnSpan raw;
switch (m_editState.target) { switch (m_editState.target) {
case EditTarget::Type: raw = typeSpan(*lm); break; case EditTarget::Type: raw = typeSpan(*lm, typeW); break;
case EditTarget::Name: raw = nameSpan(*lm, m_layout.nameW); break; case EditTarget::Name: raw = nameSpan(*lm, typeW, nameW); break;
case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), m_layout.nameW); break; case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), typeW, nameW); break;
case EditTarget::BaseAddress: raw = baseAddressSpanFor(*lm, lineText); 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) { if (raw.valid && h.col >= raw.start && h.col < raw.end) {
// Within raw span but outside trimmed text → move cursor to 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; m_hoverInside = true;
auto h = hitTest(me->pos()); auto h = hitTest(me->pos());
uint64_t newHoverId = (h.line >= 0) ? h.nodeId : 0; 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_hoveredNodeId = newHoverId;
m_hoveredLine = h.line;
applyHoverHighlight(); applyHoverHighlight();
} }
@@ -746,9 +811,9 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)); bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
// Single-click on editable token of already-selected node → edit // Single-click on editable token of already-selected node → edit
if (alreadySelected && plain) { int tLine; EditTarget t;
int tLine; EditTarget t; if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) {
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t, m_layout.nameW)) { if (alreadySelected && plain) {
m_pendingClickNodeId = 0; m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine); return beginInlineEdit(t, tLine);
} }
@@ -824,7 +889,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
&& event->type() == QEvent::MouseButtonDblClick) { && event->type() == QEvent::MouseButtonDblClick) {
auto* me = static_cast<QMouseEvent*>(event); auto* me = static_cast<QMouseEvent*>(event);
int line; EditTarget t; 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 m_pendingClickNodeId = 0; // cancel deferred selection change
return beginInlineEdit(t, line); return beginInlineEdit(t, line);
} }
@@ -856,6 +921,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
} else if (event->type() == QEvent::Leave) { } else if (event->type() == QEvent::Leave) {
m_hoverInside = false; m_hoverInside = false;
m_hoveredNodeId = 0; m_hoveredNodeId = 0;
m_hoveredLine = -1;
applyHoverHighlight(); applyHoverHighlight();
} else if (event->type() == QEvent::Wheel) { } else if (event->type() == QEvent::Wheel) {
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos()); m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
@@ -866,8 +932,10 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|| event->type() == QEvent::Wheel) { || event->type() == QEvent::Wheel) {
auto h = hitTest(m_lastHoverPos); auto h = hitTest(m_lastHoverPos);
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0; 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_hoveredNodeId = newHoverId;
m_hoveredLine = newHoverLine;
applyHoverHighlight(); applyHoverHighlight();
} }
} }
@@ -948,6 +1016,7 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
bool RcxEditor::beginInlineEdit(EditTarget target, int line) { bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
if (m_editState.active) return false; if (m_editState.active) return false;
m_hoveredNodeId = 0; m_hoveredNodeId = 0;
m_hoveredLine = -1;
applyHoverHighlight(); applyHoverHighlight();
// Clear editable-token color hints (de-emphasize non-active tokens) // Clear editable-token color hints (de-emphasize non-active tokens)
clearIndicatorLine(IND_EDITABLE, m_hintLine); clearIndicatorLine(IND_EDITABLE, m_hintLine);
@@ -982,7 +1051,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
// Store fixed comment column position for value editing // Store fixed comment column position for value editing
if (target == EditTarget::Value) { 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.commentCol = cs.valid ? cs.start : -1;
m_editState.lastValidationOk = true; // original value is always valid m_editState.lastValidationOk = true; // original value is always valid
} else { } else {
@@ -1172,7 +1241,8 @@ void RcxEditor::updateTypeListFilter() {
void RcxEditor::paintEditableSpans(int line) { void RcxEditor::paintEditableSpans(int line) {
NormalizedSpan norm; 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)) if (resolvedSpanFor(line, t, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
} }
@@ -1191,13 +1261,21 @@ void RcxEditor::updateEditableIndicators(int line) {
return; 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 // If new line is selected, its indicators are managed by applySelectionOverlay
// But we still need to clear the old non-selected hint line // But we still need to clear the old non-selected hint line
const LineMeta* newLm = metaForLine(line); const LineMeta* newLm = metaForLine(line);
if (newLm && m_currentSelIds.contains(newLm->nodeId)) { if (isLineSelected(newLm)) {
if (m_hintLine >= 0) { if (m_hintLine >= 0) {
const LineMeta* oldLm = metaForLine(m_hintLine); const LineMeta* oldLm = metaForLine(m_hintLine);
if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId)) if (!isLineSelected(oldLm))
clearIndicatorLine(IND_EDITABLE, m_hintLine); clearIndicatorLine(IND_EDITABLE, m_hintLine);
} }
m_hintLine = line; m_hintLine = line;
@@ -1207,7 +1285,7 @@ void RcxEditor::updateEditableIndicators(int line) {
// Clear old cursor line (only if not a selected node) // Clear old cursor line (only if not a selected node)
if (m_hintLine >= 0) { if (m_hintLine >= 0) {
const LineMeta* oldLm = metaForLine(m_hintLine); const LineMeta* oldLm = metaForLine(m_hintLine);
if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId)) if (!isLineSelected(oldLm))
clearIndicatorLine(IND_EDITABLE, m_hintLine); clearIndicatorLine(IND_EDITABLE, m_hintLine);
} }
@@ -1251,9 +1329,9 @@ void RcxEditor::applyHoverCursor() {
} }
int line; EditTarget t; 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) { if (tokenHit) {
NormalizedSpan span; NormalizedSpan span;
if (resolvedSpanFor(line, t, span)) { if (resolvedSpanFor(line, t, span)) {

View File

@@ -24,9 +24,9 @@ public:
int currentNodeIndex() const; int currentNodeIndex() const;
// ── Column span computation ── // ── Column span computation ──
static ColumnSpan typeSpan(const LineMeta& lm); static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
static ColumnSpan nameSpan(const LineMeta& lm, int nameW = kColName); static ColumnSpan nameSpan(const LineMeta& lm, int typeW = kColType, int nameW = kColName);
static ColumnSpan valueSpan(const LineMeta& lm, int lineLength, int nameW = kColName); static ColumnSpan valueSpan(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName);
// ── Multi-selection ── // ── Multi-selection ──
QSet<int> selectedNodeIndices() const; QSet<int> selectedNodeIndices() const;
@@ -65,6 +65,7 @@ private:
bool m_hoverInside = false; bool m_hoverInside = false;
bool m_cursorOverridden = false; bool m_cursorOverridden = false;
uint64_t m_hoveredNodeId = 0; uint64_t m_hoveredNodeId = 0;
int m_hoveredLine = -1;
QSet<uint64_t> m_currentSelIds; QSet<uint64_t> m_currentSelIds;
int m_hoverSpanLine = -1; // Line with hover span indicator int m_hoverSpanLine = -1; // Line with hover span indicator
// ── Drag selection ── // ── Drag selection ──

View File

@@ -28,10 +28,27 @@ static TypeNameFn g_typeNameFn = nullptr;
void setTypeNameProvider(TypeNameFn fn) { g_typeNameFn = fn; } void setTypeNameProvider(TypeNameFn fn) { g_typeNameFn = fn; }
QString typeName(NodeKind kind) { // Unpadded type name for width calculation
if (g_typeNameFn) return fit(g_typeNameFn(kind), COL_TYPE); QString typeNameRaw(NodeKind kind) {
if (g_typeNameFn) return g_typeNameFn(kind);
auto* m = kindMeta(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 ── // ── Value formatting ──
@@ -95,12 +112,15 @@ QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddres
return header + QStringLiteral("base: ") + baseHex; return header + QStringLiteral("base: ") + baseHex;
} }
QString fmtStructFooter(const Node& node, int depth, int totalSize) { QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
QString s = indent(depth) + QStringLiteral("};"); return indent(depth) + QStringLiteral("};");
if (totalSize > 0) }
s += QStringLiteral(" // sizeof(") + node.name + QStringLiteral(")=0x")
+ QString::number(totalSize, 16).toUpper(); // ── Array header ──
return s; // 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 ── // ── Hex / ASCII preview ──
@@ -230,12 +250,12 @@ QString readValue(const Node& node, const Provider& prov,
QString fmtNodeLine(const Node& node, const Provider& prov, QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine, uint64_t addr, int depth, int subLine,
const QString& comment, int colName) { const QString& comment, int colType, int colName) {
QString ind = indent(depth); QString ind = indent(depth);
QString type = typeName(node.kind); QString type = typeName(node.kind, colType);
QString name = fit(node.name, colName); QString name = fit(node.name, colName);
// Blank prefix for continuation lines (same width as type+sep+name+sep) // 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) // Comment suffix (padded or empty)
QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ') 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 QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
if (subLine == 0) if (subLine == 0)
return ind + type + SEP + ascii + SEP + hex + cmtSuffix; 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 // Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
const int sz = sizeForKind(node.kind); const int sz = sizeForKind(node.kind);

View File

@@ -260,187 +260,281 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
void MainWindow::newFile() { void MainWindow::newFile() {
auto* doc = new RcxDocument(this); auto* doc = new RcxDocument(this);
// Autoload self as binary data // ══════════════════════════════════════════════════════════════════════════
doc->loadData(QCoreApplication::applicationFilePath()); // PE Header Demo - Realistic PE32+ (64-bit) executable structure
doc->tree.baseAddress = 0; // ══════════════════════════════════════════════════════════════════════════
// 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 QByteArray peData(0x300, '\0'); // 768 bytes
uint32_t lfanew = doc->provider->readU32(0x3C); char* d = peData.data();
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+)
Node root; // ── DOS Header (IMAGE_DOS_HEADER) ──
root.kind = NodeKind::Struct; d[0x00] = 'M'; d[0x01] = 'Z'; // e_magic
root.name = "PE_HEADER"; *(uint16_t*)(d + 0x02) = 0x0090; // e_cblp (bytes on last page)
root.parentId = 0; *(uint16_t*)(d + 0x04) = 0x0003; // e_cp (pages in file)
root.offset = 0; *(uint16_t*)(d + 0x06) = 0x0000; // e_crlc (relocations)
int ri = doc->tree.addNode(root); *(uint16_t*)(d + 0x08) = 0x0004; // e_cparhdr (header size in paragraphs)
uint64_t rootId = doc->tree.nodes[ri].id; *(uint16_t*)(d + 0x0A) = 0x0000; // e_minalloc
*(uint16_t*)(d + 0x0C) = 0xFFFF; // e_maxalloc
auto add = [&](NodeKind k, const QString& name, int off) { *(uint16_t*)(d + 0x0E) = 0x0000; // e_ss
Node n; *(uint16_t*)(d + 0x10) = 0x00B8; // e_sp
n.kind = k; *(uint16_t*)(d + 0x12) = 0x0000; // e_csum
n.name = name; *(uint16_t*)(d + 0x14) = 0x0000; // e_ip
n.offset = off; *(uint16_t*)(d + 0x16) = 0x0000; // e_cs
n.parentId = rootId; *(uint16_t*)(d + 0x18) = 0x0040; // e_lfarlc
doc->tree.addNode(n); *(uint16_t*)(d + 0x1A) = 0x0000; // e_ovno
}; // e_res[4] at 0x1C-0x23 (zeroed)
*(uint16_t*)(d + 0x24) = 0x0000; // e_oemid
// ── IMAGE_DOS_HEADER (0x00 0x3F) ── *(uint16_t*)(d + 0x26) = 0x0000; // e_oeminfo
add(NodeKind::UInt16, "e_magic", 0x00); // e_res2[10] at 0x28-0x3B (zeroed)
add(NodeKind::UInt16, "e_cblp", 0x02); *(uint32_t*)(d + 0x3C) = 0x00000080; // e_lfanew → PE header at 0x80
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;
}
}
// ── PE Signature ── // ── 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) ── // ── File Header (IMAGE_FILE_HEADER) ──
{ const int fhOff = peOff + 4; // 0x84
Node fhStruct; *(uint16_t*)(d + fhOff + 0) = 0x8664; // Machine (AMD64)
fhStruct.kind = NodeKind::Struct; *(uint16_t*)(d + fhOff + 2) = 0x0004; // NumberOfSections
fhStruct.name = "IMAGE_FILE_HEADER"; *(uint32_t*)(d + fhOff + 4) = 0x65A3B2C1; // TimeDateStamp
fhStruct.parentId = rootId; *(uint32_t*)(d + fhOff + 8) = 0x00000000; // PointerToSymbolTable
fhStruct.offset = fh; *(uint32_t*)(d + fhOff + 12) = 0x00000000; // NumberOfSymbols
int fi = doc->tree.addNode(fhStruct); *(uint16_t*)(d + fhOff + 16) = 0x00F0; // SizeOfOptionalHeader (240)
uint64_t fhId = doc->tree.nodes[fi].id; *(uint16_t*)(d + fhOff + 18) = 0x0022; // Characteristics (EXECUTABLE|LARGE_ADDRESS_AWARE)
auto addFH = [&](NodeKind k, const QString& name, int off) { // ── Optional Header PE32+ (IMAGE_OPTIONAL_HEADER64) ──
Node n; const int ohOff = fhOff + 20; // 0x98
n.kind = k; *(uint16_t*)(d + ohOff + 0) = 0x020B; // Magic (PE32+)
n.name = name; *(uint8_t*)(d + ohOff + 2) = 0x0E; // MajorLinkerVersion
n.offset = off; *(uint8_t*)(d + ohOff + 3) = 0x00; // MinorLinkerVersion
n.parentId = fhId; *(uint32_t*)(d + ohOff + 4) = 0x00012000; // SizeOfCode
doc->tree.addNode(n); *(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); // Windows-specific fields (PE32+)
addFH(NodeKind::UInt16, "NumberOfSections", 0x02); *(uint64_t*)(d + ohOff + 24) = 0x0000000140000000ULL; // ImageBase
addFH(NodeKind::UInt32, "TimeDateStamp", 0x04); *(uint32_t*)(d + ohOff + 32) = 0x00001000; // SectionAlignment
addFH(NodeKind::UInt32, "PtrToSymbolTable", 0x08); *(uint32_t*)(d + ohOff + 36) = 0x00000200; // FileAlignment
addFH(NodeKind::UInt32, "NumberOfSymbols", 0x0C); *(uint16_t*)(d + ohOff + 40) = 0x0006; // MajorOperatingSystemVersion
addFH(NodeKind::UInt16, "SizeOfOptionalHeader", 0x10); *(uint16_t*)(d + ohOff + 42) = 0x0000; // MinorOperatingSystemVersion
addFH(NodeKind::UInt16, "Characteristics", 0x12); *(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) ── // ── Section Headers (4 sections × 40 bytes) ──
{ const int shOff = ddOff + 128; // 0x188
Node ohStruct; struct SectionDef { const char* name; uint32_t vsize; uint32_t vaddr; uint32_t rawsz; uint32_t rawptr; uint32_t chars; };
ohStruct.kind = NodeKind::Struct; SectionDef sections[4] = {
ohStruct.name = "IMAGE_OPTIONAL_HEADER64"; {".text", 0x00011234, 0x00001000, 0x00011400, 0x00000200, 0x60000020}, // CODE|EXECUTE|READ
ohStruct.parentId = rootId; {".rdata", 0x00002ABC, 0x00013000, 0x00002C00, 0x00011600, 0x40000040}, // INITIALIZED|READ
ohStruct.offset = oh; {".data", 0x00001000, 0x00016000, 0x00000400, 0x00014200, 0xC0000040}, // INITIALIZED|READ|WRITE
int oi = doc->tree.addNode(ohStruct); {".pdata", 0x00000800, 0x00017000, 0x00000800, 0x00014600, 0x40000040}, // INITIALIZED|READ
uint64_t ohId = doc->tree.nodes[oi].id; };
for (int i = 0; i < 4; i++) {
auto addOH = [&](NodeKind k, const QString& name, int off) { int off = shOff + i * 40;
Node n; memcpy(d + off, sections[i].name, 8); // Name[8]
n.kind = k; *(uint32_t*)(d + off + 8) = sections[i].vsize; // VirtualSize
n.name = name; *(uint32_t*)(d + off + 12) = sections[i].vaddr; // VirtualAddress
n.offset = off; *(uint32_t*)(d + off + 16) = sections[i].rawsz; // SizeOfRawData
n.parentId = ohId; *(uint32_t*)(d + off + 20) = sections[i].rawptr; // PointerToRawData
doc->tree.addNode(n); *(uint32_t*)(d + off + 24) = 0x00000000; // PointerToRelocations
}; *(uint32_t*)(d + off + 28) = 0x00000000; // PointerToLinenumbers
*(uint16_t*)(d + off + 32) = 0x0000; // NumberOfRelocations
addOH(NodeKind::UInt16, "Magic", 0x00); *(uint16_t*)(d + off + 34) = 0x0000; // NumberOfLinenumbers
addOH(NodeKind::UInt8, "MajorLinkerVersion", 0x02); *(uint32_t*)(d + off + 36) = sections[i].chars; // Characteristics
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);
}
} }
// ── Fill with Hex64 until 0x6000 for stress testing ── doc->loadData(peData);
int padStart = oh + 0xF0; // end of optional header doc->tree.baseAddress = 0x140000000; // Typical 64-bit image base
for (int off = padStart; off < 0x6000; off += 8) {
add(NodeKind::Hex64, // ══════════════════════════════════════════════════════════════════════════
QString("data_%1").arg(off, 4, 16, QChar('0')), // Build Node Tree
off); // ══════════════════════════════════════════════════════════════════════════
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); createTab(doc);

View File

@@ -608,7 +608,7 @@ private slots:
QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion) QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion)
} }
void testStructFooterSizeof() { void testStructFooterSimple() {
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -627,13 +627,6 @@ private slots:
f1.offset = 0; f1.offset = 0;
tree.addNode(f1); tree.addNode(f1);
Node f2;
f2.kind = NodeKind::UInt64;
f2.name = "b";
f2.parentId = rootId;
f2.offset = 4;
tree.addNode(f2);
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
@@ -641,9 +634,10 @@ private slots:
int lastLine = result.meta.size() - 1; int lastLine = result.meta.size() - 1;
QCOMPARE(result.meta[lastLine].lineKind, LineKind::Footer); 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(); QString footerText = result.text.split('\n').last();
QVERIFY(footerText.contains("sizeof(Sized)=0xC")); QVERIFY(footerText.contains("};"));
QVERIFY(!footerText.contains("sizeof"));
} }
void testLineMetaHasNodeId() { 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) QTEST_MAIN(TestCompose)

View File

@@ -326,17 +326,17 @@ private slots:
auto ts = rcx::typeSpanFor(lm); auto ts = rcx::typeSpanFor(lm);
QVERIFY(ts.valid); QVERIFY(ts.valid);
QCOMPARE(ts.start, 6); QCOMPARE(ts.start, 6);
QCOMPARE(ts.end, 16); // 6 + 10 QCOMPARE(ts.end, 20); // 6 + 14 (kColType)
auto ns = rcx::nameSpanFor(lm); auto ns = rcx::nameSpanFor(lm);
QVERIFY(ns.valid); QVERIFY(ns.valid);
QCOMPARE(ns.start, 18); // 6 + 10 + 2 QCOMPARE(ns.start, 22); // 6 + 14 + 2
QCOMPARE(ns.end, 42); // 18 + 24 QCOMPARE(ns.end, 44); // 22 + 22 (kColName)
auto vs = rcx::valueSpanFor(lm, 60); auto vs = rcx::valueSpanFor(lm, 100);
QVERIFY(vs.valid); QVERIFY(vs.valid);
QCOMPARE(vs.start, 44); // 18 + 24 + 2 QCOMPARE(vs.start, 46); // 22 + 22 + 2
QCOMPARE(vs.end, 60); QCOMPARE(vs.end, 78); // 46 + 32 (kColValue)
} }
void testColumnSpan_continuation() { void testColumnSpan_continuation() {
@@ -349,10 +349,10 @@ private slots:
QVERIFY(!rcx::typeSpanFor(lm).valid); QVERIFY(!rcx::typeSpanFor(lm).valid);
QVERIFY(!rcx::nameSpanFor(lm).valid); QVERIFY(!rcx::nameSpanFor(lm).valid);
auto vs = rcx::valueSpanFor(lm, 60); auto vs = rcx::valueSpanFor(lm, 100);
QVERIFY(vs.valid); QVERIFY(vs.valid);
QCOMPARE(vs.start, 6 + 10 + 24 + 4); // kFoldCol+indent + COL_TYPE + COL_NAME + 4 QCOMPARE(vs.start, 6 + 14 + 22 + 4); // kFoldCol+indent + kColType(14) + kColName(22) + 4
QCOMPARE(vs.end, 60); QCOMPARE(vs.end, 46 + 32); // start + kColValue
} }
void testColumnSpan_headerFooter() { void testColumnSpan_headerFooter() {
@@ -382,17 +382,17 @@ private slots:
auto ts = rcx::typeSpanFor(lm); auto ts = rcx::typeSpanFor(lm);
QVERIFY(ts.valid); QVERIFY(ts.valid);
QCOMPARE(ts.start, 3); QCOMPARE(ts.start, 3);
QCOMPARE(ts.end, 13); // 3 + 10 QCOMPARE(ts.end, 17); // 3 + 14 (kColType)
auto ns = rcx::nameSpanFor(lm); auto ns = rcx::nameSpanFor(lm);
QVERIFY(ns.valid); QVERIFY(ns.valid);
QCOMPARE(ns.start, 15); // 3 + 10 + 2 QCOMPARE(ns.start, 19); // 3 + 14 + 2
QCOMPARE(ns.end, 39); // 15 + 24 QCOMPARE(ns.end, 41); // 19 + 22 (kColName)
auto vs = rcx::valueSpanFor(lm, 50); auto vs = rcx::valueSpanFor(lm, 100);
QVERIFY(vs.valid); QVERIFY(vs.valid);
QCOMPARE(vs.start, 41); // 15 + 24 + 2 QCOMPARE(vs.start, 43); // 19 + 22 + 2
QCOMPARE(vs.end, 50); QCOMPARE(vs.end, 75); // 43 + 32 (kColValue)
} }
void testNodeIdJsonRoundTrip() { void testNodeIdJsonRoundTrip() {

View File

@@ -9,12 +9,13 @@ private slots:
void testTypeName() { void testTypeName() {
QString s = fmt::typeName(NodeKind::Float); QString s = fmt::typeName(NodeKind::Float);
QVERIFY(s.trimmed() == "float"); QVERIFY(s.trimmed() == "float");
QCOMPARE(s.size(), 10); // COL_TYPE QCOMPARE(s.size(), 14); // kColType
} }
void testFmtInt32() { void testFmtInt32() {
QCOMPARE(fmt::fmtInt32(-42), QString("-42")); // fmtInt32 outputs hex representation (0xffffffd6 for -42)
QCOMPARE(fmt::fmtInt32(0), QString("0")); QCOMPARE(fmt::fmtInt32(-42), QString("0xffffffd6"));
QCOMPARE(fmt::fmtInt32(0), QString("0x0"));
} }
void testFmtFloat() { void testFmtFloat() {
@@ -224,25 +225,15 @@ private slots:
QVERIFY(!ok); QVERIFY(!ok);
} }
void testFmtStructFooterWithSize() { void testFmtStructFooterSimple() {
Node n; Node n;
n.kind = NodeKind::Struct; n.kind = NodeKind::Struct;
n.name = "Test"; n.name = "Test";
// With size // Footer is always just "};" (no sizeof comment)
QString s1 = fmt::fmtStructFooter(n, 0, 0x14); QString s = fmt::fmtStructFooter(n, 0, 0x14);
QVERIFY(s1.contains("};")); QVERIFY(s.contains("};"));
QVERIFY(s1.contains("sizeof(Test)=0x14")); QVERIFY(!s.contains("sizeof")); // No sizeof comment
// 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"));
} }
}; };