mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||
151
src/compose.cpp
151
src/compose.cpp
@@ -16,12 +16,24 @@ struct ComposeState {
|
||||
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
||||
QSet<qulonglong> 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<uint64_t, QVector<int>> childMap;
|
||||
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) {
|
||||
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<int> children = state.childMap.value(node.id);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
// 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<int> 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<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const {
|
||||
|
||||
@@ -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: {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
102
src/core.h
102
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 <idx/count> 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,
|
||||
|
||||
142
src/editor.cpp
142
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,8 +391,14 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& 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);
|
||||
if (!isFooter)
|
||||
paintEditableSpans(i);
|
||||
}
|
||||
}
|
||||
@@ -406,11 +415,26 @@ void RcxEditor::applyHoverHighlight() {
|
||||
if (m_editState.active) return;
|
||||
if (!m_hoverInside) return;
|
||||
if (m_hoveredNodeId == 0) return;
|
||||
if (m_currentSelIds.contains(m_hoveredNodeId)) return;
|
||||
|
||||
// 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)
|
||||
if (m_meta[i].nodeId == m_hoveredNodeId &&
|
||||
m_meta[i].lineKind != LineKind::Footer)
|
||||
m_sci->markerAdd(i, M_HOVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ViewState RcxEditor::saveViewState() const {
|
||||
@@ -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<LineMeta>& 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)) {
|
||||
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<QMouseEvent*>(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)) {
|
||||
|
||||
@@ -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<int> 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<uint64_t> m_currentSelIds;
|
||||
int m_hoverSpanLine = -1; // Line with hover span indicator
|
||||
// ── Drag selection ──
|
||||
|
||||
@@ -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);
|
||||
|
||||
410
src/main.cpp
410
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
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
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);
|
||||
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;
|
||||
// ── 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
|
||||
}
|
||||
|
||||
auto addOH = [&](NodeKind k, const QString& name, int 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 = k;
|
||||
n.kind = kind;
|
||||
n.name = name;
|
||||
n.offset = off;
|
||||
n.parentId = ohId;
|
||||
doc->tree.addNode(n);
|
||||
n.parentId = parent;
|
||||
n.offset = offset;
|
||||
int idx = doc->tree.addNode(n);
|
||||
return doc->tree.nodes[idx].id;
|
||||
};
|
||||
|
||||
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);
|
||||
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;
|
||||
};
|
||||
|
||||
// Data directories (16 entries × 8 bytes)
|
||||
static const char* dirNames[] = {
|
||||
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++) {
|
||||
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);
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
// ── 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);
|
||||
// ── 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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user