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

View File

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

View File

@@ -11,6 +11,9 @@
namespace rcx {
// Footer selection ID: set high bit to distinguish footer-only selections from node selections
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
// ── RcxDocument ──
RcxDocument::RcxDocument(QObject* parent)
@@ -137,9 +140,33 @@ void RcxController::connectEditor(RcxEditor* editor) {
break;
}
case EditTarget::Type: {
bool ok;
NodeKind k = kindFromTypeName(text, &ok);
if (ok) changeNodeKind(nodeIdx, k);
// Check for array type syntax: "type[count]" e.g. "int32_t[10]"
int bracketPos = text.indexOf('[');
if (bracketPos > 0 && text.endsWith(']')) {
QString elemTypeName = text.left(bracketPos).trimmed();
QString countStr = text.mid(bracketPos + 1, text.size() - bracketPos - 2);
bool countOk;
int newCount = countStr.toInt(&countOk);
if (countOk && newCount > 0) {
bool typeOk;
NodeKind elemKind = kindFromTypeName(elemTypeName, &typeOk);
if (typeOk && nodeIdx < m_doc->tree.nodes.size()) {
Node& node = m_doc->tree.nodes[nodeIdx];
if (node.kind == NodeKind::Array) {
// Update element kind and count (no undo for now)
node.elementKind = elemKind;
node.arrayLen = newCount;
if (node.viewIndex >= newCount)
node.viewIndex = qMax(0, newCount - 1);
}
}
}
} else {
// Regular type change
bool ok;
NodeKind k = kindFromTypeName(text, &ok);
if (ok) changeNodeKind(nodeIdx, k);
}
break;
}
case EditTarget::Value:
@@ -197,6 +224,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
}
break;
}
case EditTarget::ArrayIndex:
case EditTarget::ArrayCount:
// Array navigation removed - these cases are unreachable
break;
}
// Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
refresh();
@@ -640,20 +671,30 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
bool ctrl = mods & Qt::ControlModifier;
bool shift = mods & Qt::ShiftModifier;
// Compute effective selection ID: footers use nodeId | kFooterIdBit
auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t {
if (ln >= 0 && ln < m_lastResult.meta.size() &&
m_lastResult.meta[ln].lineKind == LineKind::Footer)
return nid | kFooterIdBit;
return nid;
};
uint64_t selId = effectiveId(line, nodeId);
if (!ctrl && !shift) {
m_selIds.clear();
m_selIds.insert(nodeId);
m_selIds.insert(selId);
m_anchorLine = line;
} else if (ctrl && !shift) {
if (m_selIds.contains(nodeId))
m_selIds.remove(nodeId);
if (m_selIds.contains(selId))
m_selIds.remove(selId);
else
m_selIds.insert(nodeId);
m_selIds.insert(selId);
m_anchorLine = line;
} else if (shift && !ctrl) {
if (m_anchorLine < 0) {
m_selIds.clear();
m_selIds.insert(nodeId);
m_selIds.insert(selId);
m_anchorLine = line;
} else {
m_selIds.clear();
@@ -661,19 +702,19 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
int to = qMax(m_anchorLine, line);
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
uint64_t nid = m_lastResult.meta[i].nodeId;
if (nid != 0) m_selIds.insert(nid);
if (nid != 0) m_selIds.insert(effectiveId(i, nid));
}
}
} else { // Ctrl+Shift
if (m_anchorLine < 0) {
m_selIds.insert(nodeId);
m_selIds.insert(selId);
m_anchorLine = line;
} else {
int from = qMin(m_anchorLine, line);
int to = qMax(m_anchorLine, line);
for (int i = from; i <= to && i < m_lastResult.meta.size(); i++) {
uint64_t nid = m_lastResult.meta[i].nodeId;
if (nid != 0) m_selIds.insert(nid);
if (nid != 0) m_selIds.insert(effectiveId(i, nid));
}
}
}
@@ -682,7 +723,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin();
int idx = m_doc->tree.indexOfId(sid);
// Strip footer bit for node lookup
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
if (idx >= 0) emit nodeSelected(idx);
}
}

View File

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

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_HOVER_SPAN = 11; // Blue text on hover (link-like)
// Footer selection ID: set high bit to distinguish footer-only selections from node selections
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static QString g_fontName = "Consolas";
static QFont editorFont() {
@@ -388,9 +391,15 @@ void RcxEditor::applySelectionOverlay(const QSet<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);
paintEditableSpans(i);
if (!isFooter)
paintEditableSpans(i);
}
}
@@ -406,10 +415,25 @@ void RcxEditor::applyHoverHighlight() {
if (m_editState.active) return;
if (!m_hoverInside) return;
if (m_hoveredNodeId == 0) return;
if (m_currentSelIds.contains(m_hoveredNodeId)) return;
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId == m_hoveredNodeId)
m_sci->markerAdd(i, M_HOVER);
// Check if hovered line is a footer - footers highlight independently
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
// Check if the hovered item is already selected (using appropriate ID)
uint64_t checkId = hoveringFooter ? (m_hoveredNodeId | kFooterIdBit) : m_hoveredNodeId;
if (m_currentSelIds.contains(checkId)) return;
if (hoveringFooter) {
// Footer: only highlight this specific line
m_sci->markerAdd(m_hoveredLine, M_HOVER);
} else {
// Non-footer: highlight all matching lines except footers
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId == m_hoveredNodeId &&
m_meta[i].lineKind != LineKind::Footer)
m_sci->markerAdd(i, M_HOVER);
}
}
}
@@ -449,9 +473,9 @@ int RcxEditor::currentNodeIndex() const {
// ── Column span computation ──
ColumnSpan RcxEditor::typeSpan(const LineMeta& lm) { return typeSpanFor(lm); }
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int nameW) { return nameSpanFor(lm, nameW); }
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int nameW) { return valueSpanFor(lm, lineLength, nameW); }
ColumnSpan RcxEditor::typeSpan(const LineMeta& lm, int typeW) { return typeSpanFor(lm, typeW); }
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int typeW, int nameW) { return nameSpanFor(lm, typeW, nameW); }
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int typeW, int nameW) { return valueSpanFor(lm, lineLength, typeW, nameW); }
// ── Multi-selection ──
@@ -538,9 +562,24 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
int ind = kFoldCol + lm.depth * 3;
int typeEnd = lineText.indexOf(' ', ind);
if (typeEnd <= ind || typeEnd >= bracePos) return {};
// Don't allow editing array element names like "[0]", "[1]", etc.
QString name = lineText.mid(typeEnd + 1, bracePos - typeEnd - 1).trimmed();
if (name.startsWith('[') && name.endsWith(']'))
return {};
return {typeEnd + 1, bracePos, true};
}
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"
static ColumnSpan arrayHeaderTypeSpan(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {};
int ind = kFoldCol + lm.depth * 3;
int typeEnd = lineText.indexOf(' ', ind);
if (typeEnd <= ind) return {};
return {ind, typeEnd, true};
}
RcxEditor::NormalizedSpan RcxEditor::normalizeSpan(
const ColumnSpan& raw, const QString& lineText,
EditTarget target, bool skipPrefixes) const
@@ -589,14 +628,24 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
QString lineText = getLineText(m_sci, line);
int textLen = lineText.size();
// Use per-line effective widths (set during compose based on containing scope)
int typeW = lm->effectiveTypeW;
int nameW = lm->effectiveNameW;
ColumnSpan s;
switch (t) {
case EditTarget::Type: s = typeSpan(*lm); break;
case EditTarget::Name: s = nameSpan(*lm, m_layout.nameW); break;
case EditTarget::Value: s = valueSpan(*lm, textLen, m_layout.nameW); break;
case EditTarget::Type: s = typeSpan(*lm, typeW); break;
case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break;
case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break;
case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break;
case EditTarget::ArrayIndex:
case EditTarget::ArrayCount:
break; // Array navigation removed
}
// Fallback spans for header lines
if (!s.valid && t == EditTarget::Type)
s = arrayHeaderTypeSpan(*lm, lineText);
if (!s.valid && t == EditTarget::Name)
s = headerNameSpan(*lm, lineText);
@@ -640,8 +689,7 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
static bool hitTestTarget(QsciScintilla* sci,
const QVector<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)) {
int tLine; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) {
if (alreadySelected && plain) {
m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine);
}
@@ -824,7 +889,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
&& event->type() == QEvent::MouseButtonDblClick) {
auto* me = static_cast<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)) {

View File

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

View File

@@ -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);

View File

@@ -260,187 +260,281 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
void MainWindow::newFile() {
auto* doc = new RcxDocument(this);
// Autoload self as binary data
doc->loadData(QCoreApplication::applicationFilePath());
doc->tree.baseAddress = 0;
// ══════════════════════════════════════════════════════════════════════════
// PE Header Demo - Realistic PE32+ (64-bit) executable structure
// ══════════════════════════════════════════════════════════════════════════
// Layout:
// 0x000: DOS Header (64 bytes)
// 0x040: DOS Stub (64 bytes padding)
// 0x080: PE Signature (4 bytes)
// 0x084: File Header (20 bytes)
// 0x098: Optional Header PE32+ (240 bytes)
// - Standard fields (24 bytes)
// - Windows fields (88 bytes)
// - Data Directories (16 * 8 = 128 bytes)
// 0x188: Section Headers (4 * 40 = 160 bytes)
// 0x228: End of headers (total 552 bytes)
// ══════════════════════════════════════════════════════════════════════════
// Read e_lfanew to find PE header offset
uint32_t lfanew = doc->provider->readU32(0x3C);
if (lfanew < 0x40 || lfanew >= (uint32_t)doc->provider->size())
lfanew = 0x40;
uint32_t pe = lfanew; // PE signature
uint32_t fh = pe + 4; // IMAGE_FILE_HEADER
uint32_t oh = fh + 20; // IMAGE_OPTIONAL_HEADER (PE32+)
QByteArray peData(0x300, '\0'); // 768 bytes
char* d = peData.data();
Node root;
root.kind = NodeKind::Struct;
root.name = "PE_HEADER";
root.parentId = 0;
root.offset = 0;
int ri = doc->tree.addNode(root);
uint64_t rootId = doc->tree.nodes[ri].id;
auto add = [&](NodeKind k, const QString& name, int off) {
Node n;
n.kind = k;
n.name = name;
n.offset = off;
n.parentId = rootId;
doc->tree.addNode(n);
};
// ── IMAGE_DOS_HEADER (0x00 0x3F) ──
add(NodeKind::UInt16, "e_magic", 0x00);
add(NodeKind::UInt16, "e_cblp", 0x02);
add(NodeKind::UInt16, "e_cp", 0x04);
add(NodeKind::UInt16, "e_crlc", 0x06);
add(NodeKind::UInt16, "e_cparhdr", 0x08);
add(NodeKind::UInt16, "e_minalloc", 0x0A);
add(NodeKind::UInt16, "e_maxalloc", 0x0C);
add(NodeKind::UInt16, "e_ss", 0x0E);
add(NodeKind::UInt16, "e_sp", 0x10);
add(NodeKind::UInt16, "e_csum", 0x12);
add(NodeKind::UInt16, "e_ip", 0x14);
add(NodeKind::UInt16, "e_cs", 0x16);
add(NodeKind::UInt16, "e_lfarlc", 0x18);
add(NodeKind::UInt16, "e_ovno", 0x1A);
add(NodeKind::Hex64, "e_res", 0x1C);
add(NodeKind::UInt16, "e_oemid", 0x24);
add(NodeKind::UInt16, "e_oeminfo", 0x26);
add(NodeKind::Hex64, "e_res2_0", 0x28);
add(NodeKind::Hex64, "e_res2_1", 0x30);
add(NodeKind::Hex32, "e_res2_2", 0x38);
add(NodeKind::UInt32, "e_lfanew", 0x3C);
// ── DOS Stub (0x40 to PE signature) — fill with Hex nodes ──
{
int cursor = 0x40;
while (cursor + 8 <= (int)pe) {
add(NodeKind::Hex64,
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
cursor);
cursor += 8;
}
if (cursor + 4 <= (int)pe) {
add(NodeKind::Hex32,
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
cursor);
cursor += 4;
}
if (cursor + 2 <= (int)pe) {
add(NodeKind::Hex16,
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
cursor);
cursor += 2;
}
if (cursor + 1 <= (int)pe) {
add(NodeKind::Hex8,
QString("stub_%1").arg(cursor, 4, 16, QChar('0')),
cursor);
cursor += 1;
}
}
// ── DOS Header (IMAGE_DOS_HEADER) ──
d[0x00] = 'M'; d[0x01] = 'Z'; // e_magic
*(uint16_t*)(d + 0x02) = 0x0090; // e_cblp (bytes on last page)
*(uint16_t*)(d + 0x04) = 0x0003; // e_cp (pages in file)
*(uint16_t*)(d + 0x06) = 0x0000; // e_crlc (relocations)
*(uint16_t*)(d + 0x08) = 0x0004; // e_cparhdr (header size in paragraphs)
*(uint16_t*)(d + 0x0A) = 0x0000; // e_minalloc
*(uint16_t*)(d + 0x0C) = 0xFFFF; // e_maxalloc
*(uint16_t*)(d + 0x0E) = 0x0000; // e_ss
*(uint16_t*)(d + 0x10) = 0x00B8; // e_sp
*(uint16_t*)(d + 0x12) = 0x0000; // e_csum
*(uint16_t*)(d + 0x14) = 0x0000; // e_ip
*(uint16_t*)(d + 0x16) = 0x0000; // e_cs
*(uint16_t*)(d + 0x18) = 0x0040; // e_lfarlc
*(uint16_t*)(d + 0x1A) = 0x0000; // e_ovno
// e_res[4] at 0x1C-0x23 (zeroed)
*(uint16_t*)(d + 0x24) = 0x0000; // e_oemid
*(uint16_t*)(d + 0x26) = 0x0000; // e_oeminfo
// e_res2[10] at 0x28-0x3B (zeroed)
*(uint32_t*)(d + 0x3C) = 0x00000080; // e_lfanew → PE header at 0x80
// ── PE Signature ──
add(NodeKind::UInt32, "Signature", pe);
const int peOff = 0x80;
d[peOff+0] = 'P'; d[peOff+1] = 'E'; d[peOff+2] = 0; d[peOff+3] = 0;
// ── IMAGE_FILE_HEADER (nested struct) ──
{
Node fhStruct;
fhStruct.kind = NodeKind::Struct;
fhStruct.name = "IMAGE_FILE_HEADER";
fhStruct.parentId = rootId;
fhStruct.offset = fh;
int fi = doc->tree.addNode(fhStruct);
uint64_t fhId = doc->tree.nodes[fi].id;
// ── File Header (IMAGE_FILE_HEADER) ──
const int fhOff = peOff + 4; // 0x84
*(uint16_t*)(d + fhOff + 0) = 0x8664; // Machine (AMD64)
*(uint16_t*)(d + fhOff + 2) = 0x0004; // NumberOfSections
*(uint32_t*)(d + fhOff + 4) = 0x65A3B2C1; // TimeDateStamp
*(uint32_t*)(d + fhOff + 8) = 0x00000000; // PointerToSymbolTable
*(uint32_t*)(d + fhOff + 12) = 0x00000000; // NumberOfSymbols
*(uint16_t*)(d + fhOff + 16) = 0x00F0; // SizeOfOptionalHeader (240)
*(uint16_t*)(d + fhOff + 18) = 0x0022; // Characteristics (EXECUTABLE|LARGE_ADDRESS_AWARE)
auto addFH = [&](NodeKind k, const QString& name, int off) {
Node n;
n.kind = k;
n.name = name;
n.offset = off;
n.parentId = fhId;
doc->tree.addNode(n);
};
// ── Optional Header PE32+ (IMAGE_OPTIONAL_HEADER64) ──
const int ohOff = fhOff + 20; // 0x98
*(uint16_t*)(d + ohOff + 0) = 0x020B; // Magic (PE32+)
*(uint8_t*)(d + ohOff + 2) = 0x0E; // MajorLinkerVersion
*(uint8_t*)(d + ohOff + 3) = 0x00; // MinorLinkerVersion
*(uint32_t*)(d + ohOff + 4) = 0x00012000; // SizeOfCode
*(uint32_t*)(d + ohOff + 8) = 0x00008000; // SizeOfInitializedData
*(uint32_t*)(d + ohOff + 12) = 0x00000000; // SizeOfUninitializedData
*(uint32_t*)(d + ohOff + 16) = 0x00001000; // AddressOfEntryPoint
*(uint32_t*)(d + ohOff + 20) = 0x00001000; // BaseOfCode
addFH(NodeKind::UInt16, "Machine", 0x00);
addFH(NodeKind::UInt16, "NumberOfSections", 0x02);
addFH(NodeKind::UInt32, "TimeDateStamp", 0x04);
addFH(NodeKind::UInt32, "PtrToSymbolTable", 0x08);
addFH(NodeKind::UInt32, "NumberOfSymbols", 0x0C);
addFH(NodeKind::UInt16, "SizeOfOptionalHeader", 0x10);
addFH(NodeKind::UInt16, "Characteristics", 0x12);
// Windows-specific fields (PE32+)
*(uint64_t*)(d + ohOff + 24) = 0x0000000140000000ULL; // ImageBase
*(uint32_t*)(d + ohOff + 32) = 0x00001000; // SectionAlignment
*(uint32_t*)(d + ohOff + 36) = 0x00000200; // FileAlignment
*(uint16_t*)(d + ohOff + 40) = 0x0006; // MajorOperatingSystemVersion
*(uint16_t*)(d + ohOff + 42) = 0x0000; // MinorOperatingSystemVersion
*(uint16_t*)(d + ohOff + 44) = 0x0000; // MajorImageVersion
*(uint16_t*)(d + ohOff + 46) = 0x0000; // MinorImageVersion
*(uint16_t*)(d + ohOff + 48) = 0x0006; // MajorSubsystemVersion
*(uint16_t*)(d + ohOff + 50) = 0x0000; // MinorSubsystemVersion
*(uint32_t*)(d + ohOff + 52) = 0x00000000; // Win32VersionValue
*(uint32_t*)(d + ohOff + 56) = 0x00025000; // SizeOfImage
*(uint32_t*)(d + ohOff + 60) = 0x00000200; // SizeOfHeaders
*(uint32_t*)(d + ohOff + 64) = 0x00000000; // CheckSum
*(uint16_t*)(d + ohOff + 68) = 0x0003; // Subsystem (CONSOLE)
*(uint16_t*)(d + ohOff + 70) = 0x8160; // DllCharacteristics (DYNAMIC_BASE|NX_COMPAT|TERMINAL_SERVER_AWARE)
*(uint64_t*)(d + ohOff + 72) = 0x0000000000100000ULL; // SizeOfStackReserve
*(uint64_t*)(d + ohOff + 80) = 0x0000000000001000ULL; // SizeOfStackCommit
*(uint64_t*)(d + ohOff + 88) = 0x0000000000100000ULL; // SizeOfHeapReserve
*(uint64_t*)(d + ohOff + 96) = 0x0000000000001000ULL; // SizeOfHeapCommit
*(uint32_t*)(d + ohOff + 104) = 0x00000000; // LoaderFlags
*(uint32_t*)(d + ohOff + 108) = 0x00000010; // NumberOfRvaAndSizes (16)
// ── Data Directories (16 entries × 8 bytes) ──
const int ddOff = ohOff + 112; // 0x108
// Each entry: VirtualAddress (4) + Size (4)
struct { uint32_t rva; uint32_t size; } dataDirs[16] = {
{0x00000000, 0x00000000}, // 0: Export
{0x00014000, 0x000000A0}, // 1: Import
{0x00000000, 0x00000000}, // 2: Resource
{0x00000000, 0x00000000}, // 3: Exception
{0x00000000, 0x00000000}, // 4: Security
{0x00000000, 0x00000000}, // 5: BaseReloc
{0x00013000, 0x00000038}, // 6: Debug
{0x00000000, 0x00000000}, // 7: Architecture
{0x00000000, 0x00000000}, // 8: GlobalPtr
{0x00000000, 0x00000000}, // 9: TLS
{0x00000000, 0x00000000}, // 10: LoadConfig
{0x00000000, 0x00000000}, // 11: BoundImport
{0x00014050, 0x00000048}, // 12: IAT
{0x00000000, 0x00000000}, // 13: DelayImport
{0x00000000, 0x00000000}, // 14: CLR
{0x00000000, 0x00000000}, // 15: Reserved
};
for (int i = 0; i < 16; i++) {
*(uint32_t*)(d + ddOff + i*8 + 0) = dataDirs[i].rva;
*(uint32_t*)(d + ddOff + i*8 + 4) = dataDirs[i].size;
}
// ── IMAGE_OPTIONAL_HEADER64 (nested struct) ──
{
Node ohStruct;
ohStruct.kind = NodeKind::Struct;
ohStruct.name = "IMAGE_OPTIONAL_HEADER64";
ohStruct.parentId = rootId;
ohStruct.offset = oh;
int oi = doc->tree.addNode(ohStruct);
uint64_t ohId = doc->tree.nodes[oi].id;
auto addOH = [&](NodeKind k, const QString& name, int off) {
Node n;
n.kind = k;
n.name = name;
n.offset = off;
n.parentId = ohId;
doc->tree.addNode(n);
};
addOH(NodeKind::UInt16, "Magic", 0x00);
addOH(NodeKind::UInt8, "MajorLinkerVersion", 0x02);
addOH(NodeKind::UInt8, "MinorLinkerVersion", 0x03);
addOH(NodeKind::UInt32, "SizeOfCode", 0x04);
addOH(NodeKind::UInt32, "SizeOfInitData", 0x08);
addOH(NodeKind::UInt32, "SizeOfUninitData", 0x0C);
addOH(NodeKind::UInt32, "AddressOfEntryPoint", 0x10);
addOH(NodeKind::UInt32, "BaseOfCode", 0x14);
addOH(NodeKind::UInt64, "ImageBase", 0x18);
addOH(NodeKind::UInt32, "SectionAlignment", 0x20);
addOH(NodeKind::UInt32, "FileAlignment", 0x24);
addOH(NodeKind::UInt16, "MajorOSVersion", 0x28);
addOH(NodeKind::UInt16, "MinorOSVersion", 0x2A);
addOH(NodeKind::UInt16, "MajorImageVersion", 0x2C);
addOH(NodeKind::UInt16, "MinorImageVersion", 0x2E);
addOH(NodeKind::UInt16, "MajorSubsysVersion", 0x30);
addOH(NodeKind::UInt16, "MinorSubsysVersion", 0x32);
addOH(NodeKind::UInt32, "Win32VersionValue", 0x34);
addOH(NodeKind::UInt32, "SizeOfImage", 0x38);
addOH(NodeKind::UInt32, "SizeOfHeaders", 0x3C);
addOH(NodeKind::UInt32, "CheckSum", 0x40);
addOH(NodeKind::UInt16, "Subsystem", 0x44);
addOH(NodeKind::UInt16, "DllCharacteristics", 0x46);
addOH(NodeKind::UInt64, "SizeOfStackReserve", 0x48);
addOH(NodeKind::UInt64, "SizeOfStackCommit", 0x50);
addOH(NodeKind::UInt64, "SizeOfHeapReserve", 0x58);
addOH(NodeKind::UInt64, "SizeOfHeapCommit", 0x60);
addOH(NodeKind::UInt32, "LoaderFlags", 0x68);
addOH(NodeKind::UInt32, "NumberOfRvaAndSizes", 0x6C);
// Data directories (16 entries × 8 bytes)
static const char* dirNames[] = {
"Export", "Import", "Resource", "Exception",
"Security", "BaseReloc", "Debug", "Architecture",
"GlobalPtr", "TLS", "LoadConfig", "BoundImport",
"IAT", "DelayImport", "CLR", "Reserved"
};
for (int i = 0; i < 16; i++) {
int doff = 0x70 + i * 8;
addOH(NodeKind::UInt32, QString("%1_RVA").arg(dirNames[i]), doff);
addOH(NodeKind::UInt32, QString("%1_Size").arg(dirNames[i]), doff + 4);
}
// ── Section Headers (4 sections × 40 bytes) ──
const int shOff = ddOff + 128; // 0x188
struct SectionDef { const char* name; uint32_t vsize; uint32_t vaddr; uint32_t rawsz; uint32_t rawptr; uint32_t chars; };
SectionDef sections[4] = {
{".text", 0x00011234, 0x00001000, 0x00011400, 0x00000200, 0x60000020}, // CODE|EXECUTE|READ
{".rdata", 0x00002ABC, 0x00013000, 0x00002C00, 0x00011600, 0x40000040}, // INITIALIZED|READ
{".data", 0x00001000, 0x00016000, 0x00000400, 0x00014200, 0xC0000040}, // INITIALIZED|READ|WRITE
{".pdata", 0x00000800, 0x00017000, 0x00000800, 0x00014600, 0x40000040}, // INITIALIZED|READ
};
for (int i = 0; i < 4; i++) {
int off = shOff + i * 40;
memcpy(d + off, sections[i].name, 8); // Name[8]
*(uint32_t*)(d + off + 8) = sections[i].vsize; // VirtualSize
*(uint32_t*)(d + off + 12) = sections[i].vaddr; // VirtualAddress
*(uint32_t*)(d + off + 16) = sections[i].rawsz; // SizeOfRawData
*(uint32_t*)(d + off + 20) = sections[i].rawptr; // PointerToRawData
*(uint32_t*)(d + off + 24) = 0x00000000; // PointerToRelocations
*(uint32_t*)(d + off + 28) = 0x00000000; // PointerToLinenumbers
*(uint16_t*)(d + off + 32) = 0x0000; // NumberOfRelocations
*(uint16_t*)(d + off + 34) = 0x0000; // NumberOfLinenumbers
*(uint32_t*)(d + off + 36) = sections[i].chars; // Characteristics
}
// ── Fill with Hex64 until 0x6000 for stress testing ──
int padStart = oh + 0xF0; // end of optional header
for (int off = padStart; off < 0x6000; off += 8) {
add(NodeKind::Hex64,
QString("data_%1").arg(off, 4, 16, QChar('0')),
off);
doc->loadData(peData);
doc->tree.baseAddress = 0x140000000; // Typical 64-bit image base
// ══════════════════════════════════════════════════════════════════════════
// Build Node Tree
// ══════════════════════════════════════════════════════════════════════════
auto addField = [&](uint64_t parent, int offset, NodeKind kind, const QString& name) -> uint64_t {
Node n;
n.kind = kind;
n.name = name;
n.parentId = parent;
n.offset = offset;
int idx = doc->tree.addNode(n);
return doc->tree.nodes[idx].id;
};
auto addStruct = [&](uint64_t parent, int offset, const QString& name) -> uint64_t {
Node n;
n.kind = NodeKind::Struct;
n.name = name;
n.parentId = parent;
n.offset = offset;
int idx = doc->tree.addNode(n);
return doc->tree.nodes[idx].id;
};
auto addArray = [&](uint64_t parent, int offset, const QString& name, int count, NodeKind elemKind) -> uint64_t {
Node n;
n.kind = NodeKind::Array;
n.name = name;
n.parentId = parent;
n.offset = offset;
n.arrayLen = count;
n.elementKind = elemKind;
int idx = doc->tree.addNode(n);
return doc->tree.nodes[idx].id;
};
// ── Root: IMAGE_DOS_HEADER ──
uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER");
addField(dosId, 0x00, NodeKind::UInt16, "e_magic");
addField(dosId, 0x02, NodeKind::UInt16, "e_cblp");
addField(dosId, 0x04, NodeKind::UInt16, "e_cp");
addField(dosId, 0x06, NodeKind::UInt16, "e_crlc");
addField(dosId, 0x08, NodeKind::UInt16, "e_cparhdr");
addField(dosId, 0x0A, NodeKind::UInt16, "e_minalloc");
addField(dosId, 0x0C, NodeKind::UInt16, "e_maxalloc");
addField(dosId, 0x0E, NodeKind::UInt16, "e_ss");
addField(dosId, 0x10, NodeKind::UInt16, "e_sp");
addField(dosId, 0x12, NodeKind::UInt16, "e_csum");
addField(dosId, 0x14, NodeKind::UInt16, "e_ip");
addField(dosId, 0x16, NodeKind::UInt16, "e_cs");
addField(dosId, 0x18, NodeKind::UInt16, "e_lfarlc");
addField(dosId, 0x1A, NodeKind::UInt16, "e_ovno");
addField(dosId, 0x3C, NodeKind::UInt32, "e_lfanew");
// ── PE Signature ──
addField(0, peOff, NodeKind::UInt32, "PE_Signature");
// ── IMAGE_FILE_HEADER ──
uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER");
addField(fhId, 0, NodeKind::UInt16, "Machine");
addField(fhId, 2, NodeKind::UInt16, "NumberOfSections");
addField(fhId, 4, NodeKind::UInt32, "TimeDateStamp");
addField(fhId, 8, NodeKind::UInt32, "PointerToSymbolTable");
addField(fhId, 12, NodeKind::UInt32, "NumberOfSymbols");
addField(fhId, 16, NodeKind::UInt16, "SizeOfOptionalHeader");
addField(fhId, 18, NodeKind::UInt16, "Characteristics");
// ── IMAGE_OPTIONAL_HEADER64 ──
uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64");
addField(ohId, 0, NodeKind::UInt16, "Magic");
addField(ohId, 2, NodeKind::UInt8, "MajorLinkerVersion");
addField(ohId, 3, NodeKind::UInt8, "MinorLinkerVersion");
addField(ohId, 4, NodeKind::UInt32, "SizeOfCode");
addField(ohId, 8, NodeKind::UInt32, "SizeOfInitializedData");
addField(ohId, 12, NodeKind::UInt32, "SizeOfUninitializedData");
addField(ohId, 16, NodeKind::UInt32, "AddressOfEntryPoint");
addField(ohId, 20, NodeKind::UInt32, "BaseOfCode");
addField(ohId, 24, NodeKind::UInt64, "ImageBase");
addField(ohId, 32, NodeKind::UInt32, "SectionAlignment");
addField(ohId, 36, NodeKind::UInt32, "FileAlignment");
addField(ohId, 40, NodeKind::UInt16, "MajorOSVersion");
addField(ohId, 42, NodeKind::UInt16, "MinorOSVersion");
addField(ohId, 44, NodeKind::UInt16, "MajorImageVersion");
addField(ohId, 46, NodeKind::UInt16, "MinorImageVersion");
addField(ohId, 48, NodeKind::UInt16, "MajorSubsystemVersion");
addField(ohId, 50, NodeKind::UInt16, "MinorSubsystemVersion");
addField(ohId, 52, NodeKind::UInt32, "Win32VersionValue");
addField(ohId, 56, NodeKind::UInt32, "SizeOfImage");
addField(ohId, 60, NodeKind::UInt32, "SizeOfHeaders");
addField(ohId, 64, NodeKind::UInt32, "CheckSum");
addField(ohId, 68, NodeKind::UInt16, "Subsystem");
addField(ohId, 70, NodeKind::UInt16, "DllCharacteristics");
addField(ohId, 72, NodeKind::UInt64, "SizeOfStackReserve");
addField(ohId, 80, NodeKind::UInt64, "SizeOfStackCommit");
addField(ohId, 88, NodeKind::UInt64, "SizeOfHeapReserve");
addField(ohId, 96, NodeKind::UInt64, "SizeOfHeapCommit");
addField(ohId, 104, NodeKind::UInt32, "LoaderFlags");
addField(ohId, 108, NodeKind::UInt32, "NumberOfRvaAndSizes");
// ── Data Directories Array (16 entries) ──
uint64_t ddArrId = addArray(ohId, 112, "DataDirectory", 16, NodeKind::Struct);
const char* ddNames[16] = {
"Export", "Import", "Resource", "Exception",
"Security", "BaseReloc", "Debug", "Architecture",
"GlobalPtr", "TLS", "LoadConfig", "BoundImport",
"IAT", "DelayImport", "CLR", "Reserved"
};
for (int i = 0; i < 16; i++) {
uint64_t entryId = addStruct(ddArrId, i * 8, QString("[%1] %2").arg(i).arg(ddNames[i]));
addField(entryId, 0, NodeKind::UInt32, "VirtualAddress");
addField(entryId, 4, NodeKind::UInt32, "Size");
}
// ── Section Headers Array (4 sections) ──
uint64_t shArrId = addArray(0, shOff, "SectionHeaders", 4, NodeKind::Struct);
const char* secNames[4] = {".text", ".rdata", ".data", ".pdata"};
for (int i = 0; i < 4; i++) {
uint64_t secId = addStruct(shArrId, i * 40, QString("[%1] %2").arg(i).arg(secNames[i]));
// Name is 8 bytes - show as UTF8 string
Node nameNode;
nameNode.kind = NodeKind::UTF8;
nameNode.name = "Name";
nameNode.parentId = secId;
nameNode.offset = 0;
nameNode.strLen = 8;
doc->tree.addNode(nameNode);
addField(secId, 8, NodeKind::UInt32, "VirtualSize");
addField(secId, 12, NodeKind::UInt32, "VirtualAddress");
addField(secId, 16, NodeKind::UInt32, "SizeOfRawData");
addField(secId, 20, NodeKind::UInt32, "PointerToRawData");
addField(secId, 24, NodeKind::UInt32, "PointerToRelocations");
addField(secId, 28, NodeKind::UInt32, "PointerToLinenumbers");
addField(secId, 32, NodeKind::UInt16, "NumberOfRelocations");
addField(secId, 34, NodeKind::UInt16, "NumberOfLinenumbers");
addField(secId, 36, NodeKind::UInt32, "Characteristics");
}
createTab(doc);