Fix 13 logic bugs and UI issues across editor, controller, and core

Round 1: Fix updateCommandRow offset, structTypeName undo, changeNodeKind
macro, shift-click kCommandRowId leak, type filter byte-vs-column bug.
Round 2: Move kFooterIdBit to core.h, add RcxEditor destructor for cursor
cleanup, defer refresh during batch ops, use newline separator in type
picker, narrow selection on double-click edit, clear hover on keyboard
scroll, guard 0x prefix from deletion, cap array count at 100k.
This commit is contained in:
DreamTeam2026
2026-02-06 12:57:01 -07:00
committed by sysadmin
parent e36d1591ba
commit 6852e0915e
15 changed files with 2221 additions and 130 deletions

View File

@@ -65,6 +65,14 @@ uint32_t computeMarkers(const Node& node, const Provider& /*prov*/,
return mask;
}
static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
if (refId == 0) return {};
int refIdx = tree.indexOfId(refId);
if (refIdx < 0) return {};
const Node& ref = tree.nodes[refIdx];
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
}
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
if (tree.baseAddress && ptr >= tree.baseAddress) return ptr - tree.baseAddress;
return ptr;
@@ -113,6 +121,14 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
numLines = linesForKind(node.kind);
}
// Resolve pointer target name for display
QString ptrTypeOverride;
QString ptrTargetName;
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
ptrTargetName = resolvePointerTarget(tree, node.refId);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
}
for (int sub = 0; sub < numLines; sub++) {
bool isCont = (sub > 0);
@@ -129,9 +145,10 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
lm.foldLevel = computeFoldLevel(depth, false);
lm.effectiveTypeW = typeW;
lm.effectiveNameW = nameW;
lm.pointerTargetName = ptrTargetName;
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
/*comment=*/{}, typeW, nameW);
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
state.emitLine(lineText, lm);
}
}
@@ -269,15 +286,19 @@ void composeNode(ComposeState& state, const NodeTree& tree,
int typeW = state.effectiveTypeW(scopeId);
int nameW = state.effectiveNameW(scopeId);
// Pointer deref expansion
// Pointer deref expansion — single fold header merges pointer + struct header
if ((node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64)
&& node.refId != 0) {
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
// Emit merged fold header: "ptr64<Type> Name {" (expanded) or "ptr64<Type> Name -> val" (collapsed)
{
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Field;
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
lm.nodeKind = node.kind;
lm.foldHead = true;
@@ -286,8 +307,12 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
lm.effectiveTypeW = typeW;
lm.effectiveNameW = nameW;
state.emitLine(fmt::fmtNodeLine(node, prov, absAddr, depth, 0, {}, typeW, nameW), lm);
lm.pointerTargetName = ptrTargetName;
state.emitLine(fmt::fmtPointerHeader(node, depth, node.collapsed,
prov, absAddr, ptrTypeOverride,
typeW, nameW), lm);
}
if (!node.collapsed) {
int sz = node.byteSize();
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
@@ -302,13 +327,31 @@ void composeNode(ComposeState& state, const NodeTree& tree,
if (refIdx >= 0) {
const Node& ref = tree.nodes[refIdx];
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
// isArrayChild=true skips header/footer, emits children only
// depth (not depth+1): pointer header replaces struct header,
// so children should be at depth+1, not depth+2
composeParent(state, tree, prov, refIdx,
depth + 1, pBase, ref.id);
depth, pBase, ref.id,
/*isArrayChild=*/true);
}
state.ptrVisiting.remove(key);
}
}
}
// Footer for pointer fold
{
LineMeta lm;
lm.nodeIdx = nodeIdx;
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Footer;
lm.nodeKind = node.kind;
lm.offsetText.clear();
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), lm);
}
}
return;
}
@@ -334,21 +377,22 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
for (int i = 0; i < tree.nodes.size(); i++)
state.absOffsets[i] = tree.computeOffset(i);
// Helper: compute the display type string for a node (for width calculation)
auto nodeTypeName = [&](const Node& n) -> QString {
if (n.kind == NodeKind::Array)
return fmt::arrayTypeName(n.elementKind, n.arrayLen);
if (n.kind == NodeKind::Struct)
return fmt::structTypeName(n);
if (n.kind == NodeKind::Pointer32 || n.kind == NodeKind::Pointer64)
return fmt::pointerTypeName(n.kind, resolvePointerTarget(tree, n.refId));
return fmt::typeNameRaw(n.kind);
};
// Compute effective type column width from longest type name
// Include struct/array headers which use "struct TypeName" or "type[count]" format
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 if (node.kind == NodeKind::Struct) {
// Struct type: "struct TypeName" or "struct"
typeName = fmt::structTypeName(node);
} else {
typeName = fmt::typeNameRaw(node.kind);
}
maxTypeLen = qMax(maxTypeLen, (int)typeName.size());
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
}
state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW);
@@ -373,16 +417,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
for (int childIdx : state.childMap.value(container.id)) {
const Node& child = tree.nodes[childIdx];
// Type width - include struct/array headers too (they now use columnar layout)
QString childTypeName;
if (child.kind == NodeKind::Array)
childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen);
else if (child.kind == NodeKind::Struct)
childTypeName = fmt::structTypeName(child);
else
childTypeName = fmt::typeNameRaw(child.kind);
scopeMaxType = qMax(scopeMaxType, (int)childTypeName.size());
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
// Name width (skip hex/padding, but include containers)
if (!isHexPreview(child.kind)) {
@@ -401,16 +436,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
int rootMaxName = kMinNameW;
for (int childIdx : state.childMap.value(0)) {
const Node& child = tree.nodes[childIdx];
// Type width - include struct/array headers
QString childTypeName;
if (child.kind == NodeKind::Array)
childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen);
else if (child.kind == NodeKind::Struct)
childTypeName = fmt::structTypeName(child);
else
childTypeName = fmt::typeNameRaw(child.kind);
rootMaxType = qMax(rootMaxType, (int)childTypeName.size());
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
// Name width (skip hex/padding, include containers)
if (!isHexPreview(child.kind)) {

View File

@@ -20,9 +20,6 @@
namespace rcx {
// Footer selection ID: set high bit to distinguish footer-only selections from node selections
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static QString elide(QString s, int max) {
if (max <= 0) return {};
if (s.size() <= max) return s;
@@ -223,10 +220,14 @@ void RcxController::connectEditor(RcxEditor* editor) {
auto& node = m_doc->tree.nodes[nodeIdx];
if (node.kind != NodeKind::Struct)
changeNodeKind(nodeIdx, NodeKind::Struct);
// Set the struct type name via rename of structTypeName
int idx = m_doc->tree.indexOfId(node.id);
if (idx >= 0)
m_doc->tree.nodes[idx].structTypeName = text;
if (idx >= 0) {
QString oldTypeName = m_doc->tree.nodes[idx].structTypeName;
if (oldTypeName != text) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeStructTypeName{node.id, oldTypeName, text}));
}
}
}
}
}
@@ -305,6 +306,53 @@ void RcxController::connectEditor(RcxEditor* editor) {
}
break;
}
case EditTarget::ArrayElementType: {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break;
const Node& node = m_doc->tree.nodes[nodeIdx];
if (node.kind != NodeKind::Array) break;
bool ok;
NodeKind elemKind = kindFromTypeName(text, &ok);
if (ok && elemKind != node.elementKind) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{node.id,
node.elementKind, elemKind,
node.arrayLen, node.arrayLen}));
}
break;
}
case EditTarget::ArrayElementCount: {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break;
const Node& node = m_doc->tree.nodes[nodeIdx];
if (node.kind != NodeKind::Array) break;
bool ok;
int newLen = text.toInt(&ok);
if (ok && newLen > 0 && newLen <= 100000 && newLen != node.arrayLen) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{node.id,
node.elementKind, node.elementKind,
node.arrayLen, newLen}));
}
break;
}
case EditTarget::PointerTarget: {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break;
Node& node = m_doc->tree.nodes[nodeIdx];
if (node.kind != NodeKind::Pointer32 && node.kind != NodeKind::Pointer64) break;
// Find the struct with matching name or structTypeName
uint64_t newRefId = 0;
for (const auto& n : m_doc->tree.nodes) {
if (n.kind == NodeKind::Struct &&
(n.structTypeName == text || n.name == text)) {
newRefId = n.id;
break;
}
}
if (newRefId != node.refId) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{node.id, node.refId, newRefId}));
}
break;
}
case EditTarget::ArrayIndex:
case EditTarget::ArrayCount:
// Array navigation removed - these cases are unreachable
@@ -367,6 +415,10 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
uint64_t parentId = node.parentId;
int baseOffset = node.offset + newSize;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Change type"));
// Push type change with no offset adjustments
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeKind{node.id, node.kind, newKind, {}}));
@@ -386,6 +438,10 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
padOffset += padSize;
gap -= padSize;
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
} else {
// Same size or larger: adjust sibling offsets as before
int delta = newSize - oldSize;
@@ -551,10 +607,19 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
if (tree.nodes[idx].viewIndex >= tree.nodes[idx].arrayLen)
tree.nodes[idx].viewIndex = qMax(0, tree.nodes[idx].arrayLen - 1);
}
} else if constexpr (std::is_same_v<T, cmd::ChangePointerRef>) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0)
tree.nodes[idx].refId = isUndo ? c.oldRefId : c.newRefId;
} else if constexpr (std::is_same_v<T, cmd::ChangeStructTypeName>) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0)
tree.nodes[idx].structTypeName = isUndo ? c.oldName : c.newName;
}
}, command);
refresh();
if (!m_suppressRefresh)
refresh();
}
void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
@@ -745,12 +810,15 @@ void RcxController::batchRemoveNodes(const QVector<int>& nodeIndices) {
m_selIds.clear();
m_anchorLine = -1;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QString("Delete %1 nodes").arg(idSet.size()));
for (uint64_t id : idSet) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) removeNode(idx);
}
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
}
void RcxController::batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind) {
@@ -766,12 +834,15 @@ void RcxController::batchChangeKind(const QVector<int>& nodeIndices, NodeKind ne
m_selIds.clear();
m_anchorLine = -1;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QString("Change type of %1 nodes").arg(idSet.size()));
for (uint64_t id : idSet) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) changeNodeKind(idx, newKind);
}
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
}
void RcxController::handleNodeClick(RcxEditor* source, int line,
@@ -811,7 +882,7 @@ 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(effectiveId(i, nid));
if (nid != 0 && nid != kCommandRowId) m_selIds.insert(effectiveId(i, nid));
}
}
} else { // Ctrl+Shift
@@ -823,7 +894,7 @@ 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(effectiveId(i, nid));
if (nid != 0 && nid != kCommandRowId) m_selIds.insert(effectiveId(i, nid));
}
}
}
@@ -869,7 +940,7 @@ void RcxController::updateCommandRow() {
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
if (idx >= 0) {
const auto& node = m_doc->tree.nodes[idx];
uint64_t addr = m_doc->tree.baseAddress + node.offset;
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(idx);
sym = m_doc->provider->getSymbol(addr);
}
}

View File

@@ -94,6 +94,7 @@ private:
ComposeResult m_lastResult;
QSet<uint64_t> m_selIds;
int m_anchorLine = -1;
bool m_suppressRefresh = false;
void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);

View File

@@ -71,7 +71,7 @@ inline constexpr KindMeta kKindMeta[] = {
{NodeKind::Double, "Double", "double", 8, 1, 8, KF_None},
{NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None},
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None},
{NodeKind::Pointer64, "Pointer64", "void*", 8, 1, 8, KF_None},
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None},
{NodeKind::Vec2, "Vec2", "Vec2", 8, 2, 4, KF_Vector},
{NodeKind::Vec3, "Vec3", "Vec3", 12, 3, 4, KF_Vector},
{NodeKind::Vec4, "Vec4", "Vec4", 16, 4, 4, KF_Vector},
@@ -378,6 +378,7 @@ enum class LineKind : uint8_t {
static constexpr uint64_t kCommandRowId = UINT64_MAX;
static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
struct LineMeta {
int nodeIdx = -1;
@@ -400,6 +401,7 @@ struct LineMeta {
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
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
};
inline bool isSyntheticLine(const LineMeta& lm) {
@@ -437,12 +439,15 @@ namespace cmd {
struct ChangeArrayMeta { uint64_t nodeId;
NodeKind oldElementKind, newElementKind;
int oldArrayLen, newArrayLen; };
struct ChangePointerRef { uint64_t nodeId;
uint64_t oldRefId, newRefId; };
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
}
using Command = std::variant<
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
cmd::ChangeArrayMeta
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName
>;
// ── Column spans (for inline editing) ──
@@ -453,7 +458,8 @@ struct ColumnSpan {
bool valid = false;
};
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount };
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
ArrayElementType, ArrayElementCount, PointerTarget };
// Column layout constants (shared with format.cpp span computation)
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
@@ -555,6 +561,52 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
return {start, end, true};
}
// ── Array element type/count spans (within type column of array headers) ──
// Line format: " int32_t[10] name {"
// arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10"
inline ColumnSpan arrayElemTypeSpanFor(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {};
int ind = kFoldCol + lm.depth * 3;
// Find '[' in the type portion
int bracket = lineText.indexOf('[', ind);
if (bracket <= ind) return {};
return {ind, bracket, true};
}
inline ColumnSpan arrayElemCountSpanFor(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {};
int ind = kFoldCol + lm.depth * 3;
int openBracket = lineText.indexOf('[', ind);
int closeBracket = lineText.indexOf(']', openBracket);
if (openBracket < 0 || closeBracket < 0 || closeBracket <= openBracket + 1) return {};
return {openBracket + 1, closeBracket, true};
}
// ── Pointer kind/target spans (within type column of pointer fields) ──
// Line format: " ptr64<void> name -> 0x..."
// pointerKindSpan covers "ptr64" or "ptr32", pointerTargetSpan covers the target name inside <>
inline ColumnSpan pointerKindSpanFor(const LineMeta& lm, const QString& lineText) {
if ((lm.lineKind != LineKind::Field && lm.lineKind != LineKind::Header) || lm.isContinuation) return {};
if (lm.nodeKind != NodeKind::Pointer32 && lm.nodeKind != NodeKind::Pointer64) return {};
int ind = kFoldCol + lm.depth * 3;
// Find '<' in the type portion
int lt = lineText.indexOf('<', ind);
if (lt <= ind) return {};
return {ind, lt, true};
}
inline ColumnSpan pointerTargetSpanFor(const LineMeta& lm, const QString& lineText) {
if ((lm.lineKind != LineKind::Field && lm.lineKind != LineKind::Header) || lm.isContinuation) return {};
if (lm.nodeKind != NodeKind::Pointer32 && lm.nodeKind != NodeKind::Pointer64) return {};
int ind = kFoldCol + lm.depth * 3;
int lt = lineText.indexOf('<', ind);
int gt = lineText.indexOf('>', lt);
if (lt < 0 || gt < 0 || gt <= lt + 1) return {};
return {lt + 1, gt, true};
}
// ── Array navigation spans ──
// Line format: "uint32_t[16] name { <0/16>"
@@ -619,13 +671,18 @@ 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 colType = kColType, int colName = kColName);
const QString& comment = {}, int colType = kColType, int colName = kColName,
const QString& typeOverride = {});
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation);
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName);
QString structTypeName(const Node& node); // Full type string for struct headers
QString arrayTypeName(NodeKind elemKind, int count);
QString pointerTypeName(NodeKind kind, const QString& targetName);
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
const Provider& prov, uint64_t addr,
const QString& ptrTypeName, int colType = kColType, int colName = kColName);
QString validateBaseAddress(const QString& text);
QString indent(int depth);
QString readValue(const Node& node, const Provider& prov,

View File

@@ -28,9 +28,6 @@ static constexpr int IND_BASE_ADDR = 10; // Green color for base address
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
// 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() {
@@ -80,7 +77,9 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
connect(m_sci, &QsciScintilla::userListActivated,
this, [this](int id, const QString& text) {
if (!m_editState.active) return;
if (id == 1 && m_editState.target == EditTarget::Type) {
if (id == 1 && (m_editState.target == EditTarget::Type
|| m_editState.target == EditTarget::ArrayElementType
|| m_editState.target == EditTarget::PointerTarget)) {
auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
}
@@ -94,14 +93,21 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
if (m_updatingComment) return; // Skip queuing during comment update
if (m_editState.target == EditTarget::Value)
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
if (m_editState.target == EditTarget::Type)
if (m_editState.target == EditTarget::Type || m_editState.target == EditTarget::ArrayElementType)
QTimer::singleShot(0, this, &RcxEditor::updateTypeListFilter);
if (m_editState.target == EditTarget::PointerTarget)
QTimer::singleShot(0, this, &RcxEditor::updatePointerTargetFilter);
});
connect(m_sci, &QsciScintilla::selectionChanged,
this, &RcxEditor::clampEditSelection);
}
RcxEditor::~RcxEditor() {
if (m_cursorOverridden)
QApplication::restoreOverrideCursor();
}
void RcxEditor::setupScintilla() {
m_sci->setFont(editorFont());
@@ -723,6 +729,10 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
if (lm->nodeIdx < 0) return false;
// Padding: reject value editing (hex bytes are display-only)
if (t == EditTarget::Value && lm->nodeKind == NodeKind::Padding)
return false;
QString lineText = getLineText(m_sci, line);
int textLen = lineText.size();
@@ -739,13 +749,24 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
case EditTarget::ArrayIndex:
case EditTarget::ArrayCount:
break; // Array navigation removed
case EditTarget::ArrayElementType:
s = arrayElemTypeSpanFor(*lm, lineText); break;
case EditTarget::ArrayElementCount:
s = arrayElemCountSpanFor(*lm, lineText); break;
case EditTarget::PointerTarget:
s = pointerTargetSpanFor(*lm, lineText); break;
case EditTarget::Source: break;
}
// Fallback spans for header lines
if (!s.valid && t == EditTarget::Type) {
// For pointer fields, the full type span acts as "kind" span
// For array headers, fall back to the full type[count] span
s = arrayHeaderTypeSpan(*lm, lineText);
if (!s.valid)
s = headerTypeNameSpan(*lm, lineText);
if (!s.valid)
s = pointerKindSpanFor(*lm, lineText);
}
if (!s.valid && t == EditTarget::Name)
s = headerNameSpan(*lm, lineText);
@@ -829,6 +850,22 @@ static bool hitTestTarget(QsciScintilla* sci,
ColumnSpan ns = RcxEditor::nameSpan(lm, typeW, nameW);
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, typeW, nameW);
// Pointer fields/headers: check sub-spans within type column first
if (lm.nodeKind == NodeKind::Pointer32 || lm.nodeKind == NodeKind::Pointer64) {
ColumnSpan ptrTarget = pointerTargetSpanFor(lm, lineText);
ColumnSpan ptrKind = pointerKindSpanFor(lm, lineText);
if (inSpan(ptrTarget)) { outTarget = EditTarget::PointerTarget; outLine = line; return true; }
if (inSpan(ptrKind)) { outTarget = EditTarget::Type; outLine = line; return true; }
}
// Array headers: check element type and count sub-spans first
if (lm.isArrayHeader) {
ColumnSpan elemType = arrayElemTypeSpanFor(lm, lineText);
ColumnSpan elemCount = arrayElemCountSpanFor(lm, lineText);
if (inSpan(elemCount)) { outTarget = EditTarget::ArrayElementCount; outLine = line; return true; }
if (inSpan(elemType)) { outTarget = EditTarget::ArrayElementType; outLine = line; return true; }
}
// Fallback spans for header lines
if (!ts.valid) {
ts = arrayHeaderTypeSpan(lm, lineText);
@@ -843,6 +880,10 @@ static bool hitTestTarget(QsciScintilla* sci,
else if (inSpan(vs)) outTarget = EditTarget::Value;
else return false;
// Padding nodes: hex bytes are display-only, not editable
if (outTarget == EditTarget::Value && lm.nodeKind == NodeKind::Padding)
return false;
outLine = line;
return true;
}
@@ -852,7 +893,14 @@ static bool hitTestTarget(QsciScintilla* sci,
bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
if (obj == m_sci && event->type() == QEvent::KeyPress) {
auto* ke = static_cast<QKeyEvent*>(event);
return m_editState.active ? handleEditKey(ke) : handleNormalKey(ke);
bool handled = m_editState.active ? handleEditKey(ke) : handleNormalKey(ke);
if (!handled && !m_editState.active) {
// Clear hover on keyboard navigation (stale after scroll)
m_hoveredNodeId = 0;
m_hoveredLine = -1;
applyHoverHighlight();
}
return handled;
}
if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonPress
&& m_editState.active) {
@@ -882,6 +930,9 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
case EditTarget::Source: raw = commandRowSrcSpan(lineText); break;
case EditTarget::ArrayIndex: raw = arrayIndexSpanFor(*lm, lineText); break;
case EditTarget::ArrayCount: raw = arrayCountSpanFor(*lm, lineText); break;
case EditTarget::ArrayElementType: raw = arrayElemTypeSpanFor(*lm, lineText); break;
case EditTarget::ArrayElementCount: raw = arrayElemCountSpanFor(*lm, lineText); break;
case EditTarget::PointerTarget: raw = pointerTargetSpanFor(*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
@@ -1009,6 +1060,10 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
int line; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), line, t)) {
m_pendingClickNodeId = 0; // cancel deferred selection change
// Narrow selection to this node before editing
auto h = hitTest(me->pos());
if (h.nodeId != 0 && h.nodeId != kCommandRowId)
emit nodeClicked(h.line, h.nodeId, Qt::NoModifier);
return beginInlineEdit(t, line);
}
}
@@ -1079,12 +1134,15 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
case Qt::Key_Enter:
return beginInlineEdit(EditTarget::Value);
case Qt::Key_Tab: {
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value};
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
EditTarget::PointerTarget};
constexpr int N = 6;
int start = 0;
for (int i = 0; i < 3; i++)
if (order[i] == m_lastTabTarget) { start = (i + 1) % 3; break; }
for (int i = 0; i < 3; i++) {
EditTarget t = order[(start + i) % 3];
for (int i = 0; i < N; i++)
if (order[i] == m_lastTabTarget) { start = (i + 1) % N; break; }
for (int i = 0; i < N; i++) {
EditTarget t = order[(start + i) % N];
if (beginInlineEdit(t)) { m_lastTabTarget = t; return true; }
}
return true;
@@ -1127,7 +1185,14 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
case Qt::Key_Backspace: {
int line, col;
m_sci->getCursorPosition(&line, &col);
if (col <= m_editState.spanStart) return true;
int minCol = m_editState.spanStart;
// Don't allow backing into "0x" prefix
if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) {
QString lineText = getLineText(m_sci, m_editState.line);
if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
minCol = m_editState.spanStart + 2;
}
if (col <= minCol) return true;
return false;
}
case Qt::Key_Right: {
@@ -1136,9 +1201,17 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
if (col >= editEndCol()) return true; // block past end
return false;
}
case Qt::Key_Home:
m_sci->setCursorPosition(m_editState.line, m_editState.spanStart);
case Qt::Key_Home: {
int home = m_editState.spanStart;
// Skip "0x" prefix for hex values
if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) {
QString lineText = getLineText(m_sci, m_editState.line);
if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
home = m_editState.spanStart + 2;
}
m_sci->setCursorPosition(m_editState.line, home);
return true;
}
case Qt::Key_End:
m_sci->setCursorPosition(m_editState.line, editEndCol());
return true;
@@ -1169,6 +1242,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
if (lm->nodeIdx < 0 && !(lm->lineKind == LineKind::CommandRow &&
(target == EditTarget::BaseAddress || target == EditTarget::Source)))
return false;
// Padding: reject value editing (display-only hex bytes)
if (target == EditTarget::Value && lm->nodeKind == NodeKind::Padding)
return false;
QString lineText;
NormalizedSpan norm;
@@ -1202,8 +1278,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1);
m_sci->setReadOnly(false);
// Switch to I-beam for editing (skip for Type/Source which use popup pickers)
if (target != EditTarget::Type && target != EditTarget::Source) {
// Switch to I-beam for editing (skip for picker-based targets)
if (target != EditTarget::Type && target != EditTarget::Source
&& target != EditTarget::ArrayElementType && target != EditTarget::PointerTarget) {
if (m_cursorOverridden) {
QApplication::changeOverrideCursor(Qt::IBeamCursor);
} else {
@@ -1233,10 +1310,12 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
if (target == EditTarget::Value)
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
if (target == EditTarget::Type)
if (target == EditTarget::Type || target == EditTarget::ArrayElementType)
QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete);
if (target == EditTarget::Source)
QTimer::singleShot(0, this, &RcxEditor::showSourcePicker);
if (target == EditTarget::PointerTarget)
QTimer::singleShot(0, this, &RcxEditor::showPointerTargetPicker);
return true;
}
@@ -1321,7 +1400,8 @@ void RcxEditor::cancelInlineEdit() {
// ── Type picker (user list) ──
void RcxEditor::showTypeAutocomplete() {
if (!m_editState.active || m_editState.target != EditTarget::Type)
if (!m_editState.active ||
(m_editState.target != EditTarget::Type && m_editState.target != EditTarget::ArrayElementType))
return;
// Replace original type with spaces (keeps layout, clears for typing)
int len = m_editState.original.size();
@@ -1338,7 +1418,8 @@ void RcxEditor::showTypeAutocomplete() {
}
void RcxEditor::showTypeListFiltered(const QString& filter) {
if (!m_editState.active || m_editState.target != EditTarget::Type)
if (!m_editState.active ||
(m_editState.target != EditTarget::Type && m_editState.target != EditTarget::ArrayElementType))
return;
// Combine native types with custom (struct) type names
@@ -1358,8 +1439,8 @@ void RcxEditor::showTypeListFiltered(const QString& filter) {
if (filtered.isEmpty()) return; // No matches - keep list hidden
// Show user list (id=1 for types) - selection handled by userListActivated signal
QByteArray list = filtered.join(' ').toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' ');
QByteArray list = filtered.join('\n').toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
(uintptr_t)1, list.constData());
// Arrow cursor for popup is handled by applyHoverCursor() via isListActive()
@@ -1389,15 +1470,15 @@ void RcxEditor::showSourcePicker() {
}
void RcxEditor::updateTypeListFilter() {
if (!m_editState.active || m_editState.target != EditTarget::Type)
if (!m_editState.active ||
(m_editState.target != EditTarget::Type && m_editState.target != EditTarget::ArrayElementType))
return;
// Get currently typed text from line
QString lineText = getLineText(m_sci, m_editState.line);
long curPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)m_editState.line);
int col = (int)(curPos - lineStart);
int col = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN,
(unsigned long)curPos);
// Extract text from spanStart to cursor
int len = col - m_editState.spanStart;
@@ -1410,6 +1491,68 @@ void RcxEditor::updateTypeListFilter() {
showTypeListFiltered(typed);
}
// ── Pointer target picker ──
void RcxEditor::showPointerTargetPicker() {
if (!m_editState.active || m_editState.target != EditTarget::PointerTarget)
return;
// Replace original target with spaces (keeps layout, clears for typing)
int len = m_editState.original.size();
QString spaces(len, ' ');
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
m_editState.posStart, m_editState.posEnd);
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
(uintptr_t)0, spaces.toUtf8().constData());
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
showPointerTargetListFiltered(QString());
}
void RcxEditor::showPointerTargetListFiltered(const QString& filter) {
if (!m_editState.active || m_editState.target != EditTarget::PointerTarget)
return;
// Build list: "void" + all struct type names
QStringList all;
all << QStringLiteral("void");
for (const QString& ct : m_customTypeNames) {
if (!all.contains(ct))
all << ct;
}
all.sort(Qt::CaseInsensitive);
// Ensure "void" is always first
all.removeAll(QStringLiteral("void"));
all.prepend(QStringLiteral("void"));
QStringList filtered;
for (const QString& t : all) {
if (filter.isEmpty() || t.startsWith(filter, Qt::CaseInsensitive))
filtered << t;
}
if (filtered.isEmpty()) return;
QByteArray list = filtered.join('\n').toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
(uintptr_t)1, list.constData());
}
void RcxEditor::updatePointerTargetFilter() {
if (!m_editState.active || m_editState.target != EditTarget::PointerTarget)
return;
QString lineText = getLineText(m_sci, m_editState.line);
long curPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
int col = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN,
(unsigned long)curPos);
int len = col - m_editState.spanStart;
if (len <= 0) {
showPointerTargetListFiltered(QString());
return;
}
QString typed = lineText.mid(m_editState.spanStart, len);
showPointerTargetListFiltered(typed);
}
// ── Editable-field text-color indicator ──
void RcxEditor::paintEditableSpans(int line) {
@@ -1425,7 +1568,9 @@ void RcxEditor::paintEditableSpans(int line) {
return;
}
NormalizedSpan norm;
for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value}) {
for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value,
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
EditTarget::PointerTarget}) {
if (resolvedSpanFor(line, t, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
}
@@ -1479,11 +1624,10 @@ void RcxEditor::updateEditableIndicators(int line) {
// ── Hover cursor ──
void RcxEditor::applyHoverCursor() {
// Clear previous hover span indicator
if (m_hoverSpanLine >= 0) {
clearIndicatorLine(IND_HOVER_SPAN, m_hoverSpanLine);
m_hoverSpanLine = -1;
}
// Clear previous hover span indicators
for (int ln : m_hoverSpanLines)
clearIndicatorLine(IND_HOVER_SPAN, ln);
m_hoverSpanLines.clear();
// Edit mode handles its own cursor (I-beam)
if (m_editState.active)
@@ -1511,22 +1655,48 @@ void RcxEditor::applyHoverCursor() {
return;
}
auto h = hitTest(m_lastHoverPos);
int line; EditTarget t;
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t);
// Apply hover span indicator (blue text like a link) for editable spans
if (tokenHit) {
// For hex preview nodes, check if cursor is in the data area (ASCII or hex bytes)
int hoverLine = h.line;
bool inHexDataArea = false;
uint64_t hoverNodeId = 0;
if (hoverLine >= 0 && hoverLine < m_meta.size()
&& isHexPreview(m_meta[hoverLine].nodeKind)) {
hoverNodeId = m_meta[hoverLine].nodeId;
if (hoverNodeId != 0 && h.col >= 0) {
int ind = kFoldCol + m_meta[hoverLine].depth * 3;
int typeW = m_meta[hoverLine].effectiveTypeW;
int dataStart = ind + typeW + kSepWidth;
inHexDataArea = (h.col >= dataStart);
}
}
// Apply hover span indicator
if (inHexDataArea) {
// Hex preview nodes: highlight ASCII + hex byte areas on ALL lines of this node
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId != hoverNodeId) continue;
int ind = kFoldCol + m_meta[i].depth * 3;
int typeW = m_meta[i].effectiveTypeW;
int asciiStart = ind + typeW + kSepWidth;
int hexEnd = asciiStart + 8 + kSepWidth + 23;
fillIndicatorCols(IND_HOVER_SPAN, i, asciiStart, hexEnd);
m_hoverSpanLines.append(i);
}
} else if (tokenHit) {
NormalizedSpan span;
if (resolvedSpanFor(line, t, span)) {
fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end);
m_hoverSpanLine = line;
m_hoverSpanLines.append(line);
}
}
// Also show pointer cursor for fold column on fold-head lines
bool interactive = tokenHit;
bool interactive = tokenHit || inHexDataArea;
if (!interactive) {
auto h = hitTest(m_lastHoverPos);
if (h.inFoldCol) interactive = true;
}

View File

@@ -13,6 +13,7 @@ class RcxEditor : public QWidget {
Q_OBJECT
public:
explicit RcxEditor(QWidget* parent = nullptr);
~RcxEditor() override;
void applyDocument(const ComposeResult& result);
@@ -71,7 +72,7 @@ private:
uint64_t m_hoveredNodeId = 0;
int m_hoveredLine = -1;
QSet<uint64_t> m_currentSelIds;
int m_hoverSpanLine = -1; // Line with hover span indicator
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
// ── Drag selection ──
bool m_dragging = false;
bool m_dragStarted = false; // true once drag threshold exceeded
@@ -134,6 +135,9 @@ private:
void showSourcePicker();
void showTypeListFiltered(const QString& filter);
void updateTypeListFilter();
void showPointerTargetPicker();
void showPointerTargetListFiltered(const QString& filter);
void updatePointerTargetFilter();
void paintEditableSpans(int line);
void updateEditableIndicators(int line);
void applyHoverCursor();

View File

@@ -51,6 +51,14 @@ QString arrayTypeName(NodeKind elemKind, int count) {
return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]");
}
// Pointer type string: "ptr64<void>" or "ptr64<StructName>"
QString pointerTypeName(NodeKind kind, const QString& targetName) {
auto* m = kindMeta(kind);
QString base = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
QString target = targetName.isEmpty() ? QStringLiteral("void") : targetName;
return base + QStringLiteral("<") + target + QStringLiteral(">");
}
// ── Value formatting ──
static QString hexVal(uint64_t v) {
@@ -132,6 +140,22 @@ QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collap
return ind + type + SEP + name + SEP + suffix;
}
// ── Pointer header (merged pointer + struct header) ──
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
const Provider& prov, uint64_t addr,
const QString& ptrTypeName, int colType, int colName) {
QString ind = indent(depth);
QString type = fit(ptrTypeName, colType);
QString name = fit(node.name, colName);
if (collapsed) {
// Collapsed: show pointer value instead of brace
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
return ind + type + SEP + name + SEP + val;
}
return ind + type + SEP + name + SEP + QStringLiteral("{");
}
// ── Hex / ASCII preview ──
static inline bool isAsciiPrintable(uint8_t c) { return c >= 0x20 && c <= 0x7E; }
@@ -277,9 +301,10 @@ QString readValue(const Node& node, const Provider& prov,
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine,
const QString& comment, int colType, int colName) {
const QString& comment, int colType, int colName,
const QString& typeOverride) {
QString ind = indent(depth);
QString type = typeName(node.kind, colType);
QString type = typeOverride.isEmpty() ? typeName(node.kind, colType) : fit(typeOverride, colType);
QString name = fit(node.name, colName);
// Blank prefix for continuation lines (same width as type+sep+name+sep)
const int prefixW = colType + colName + 2 * kSepWidth;

View File

@@ -271,10 +271,11 @@ void MainWindow::newFile() {
auto* doc = new RcxDocument(this);
// ══════════════════════════════════════════════════════════════════════════
// _PEB64 Demo — Process Environment Block (0x7D0 bytes)
// _PEB64 Demo — Process Environment Block + stub structs
// Buffer covers PEB (0x7D0) + _PEB_LDR_DATA (0x800) + _RTL_USER_PROCESS_PARAMETERS (0x900)
// ══════════════════════════════════════════════════════════════════════════
QByteArray pebData(0x7D0, '\0');
QByteArray pebData(0x940, '\0');
char* d = pebData.data();
auto w8 = [&](int off, uint8_t v) { d[off] = (char)v; };
@@ -286,8 +287,8 @@ void MainWindow::newFile() {
w8 (0x003, 0x04); // BitField
w64(0x008, 0xFFFFFFFFFFFFFFFFULL); // Mutant (-1)
w64(0x010, 0x00007FF6DE120000ULL); // ImageBaseAddress
w64(0x018, 0x00007FFE3B8B53C0ULL); // Ldr
w64(0x020, 0x000001A4C3E20F90ULL); // ProcessParameters
w64(0x018, 0x000000D87B5E5800ULL); // Ldr (baseAddress + 0x800)
w64(0x020, 0x000000D87B5E5900ULL); // ProcessParameters (baseAddress + 0x900)
w64(0x030, 0x000001A4C3D40000ULL); // ProcessHeap
w64(0x038, 0x00007FFE3B8D4260ULL); // FastPebLock
w32(0x050, 0x01); // CrossProcessFlags
@@ -329,6 +330,26 @@ void MainWindow::newFile() {
w64(0x398, 0x000000D87B5E5390ULL); // TppWorkerpList.Blink (self)
w64(0x7B8, 0x00007FFE38860000ULL); // LeapSecondData
// ── _PEB_LDR_DATA at offset 0x800 ──
w32(0x800, 0x48); // Length
w8 (0x804, 0x01); // Initialized
w64(0x808, 0x0000000000000000ULL); // SsHandle
w64(0x810, 0x000001A4C3D40100ULL); // InLoadOrderModuleList.Flink
w64(0x818, 0x000001A4C3D40200ULL); // InLoadOrderModuleList.Blink
w64(0x820, 0x000001A4C3D40110ULL); // InMemoryOrderModuleList.Flink
w64(0x828, 0x000001A4C3D40210ULL); // InMemoryOrderModuleList.Blink
// ── _RTL_USER_PROCESS_PARAMETERS at offset 0x900 ──
w32(0x900, 0x07B0); // MaximumLength
w32(0x904, 0x07B0); // Length
w32(0x908, 0x0001); // Flags (NORMALIZED)
w32(0x90C, 0x0000); // DebugFlags
w64(0x910, 0x0000000000000044ULL); // ConsoleHandle
w32(0x918, 0x0000); // ConsoleFlags
w64(0x920, 0x0000000000000008ULL); // StandardInput
w64(0x928, 0x000000000000000CULL); // StandardOutput
w64(0x930, 0x0000000000000010ULL); // StandardError
doc->loadData(pebData);
doc->tree.baseAddress = 0x000000D87B5E5000ULL;
@@ -359,7 +380,17 @@ void MainWindow::newFile() {
n.parentId = parent; n.offset = offset;
n.arrayLen = count; n.elementKind = elemKind;
n.collapsed = true;
doc->tree.addNode(n);
int idx = doc->tree.addNode(n);
uint64_t arrId = doc->tree.nodes[idx].id;
int elemSz = sizeForKind(elemKind);
if (elemSz > 0) {
for (int i = 0; i < count; i++) {
Node e; e.kind = elemKind;
e.name = QStringLiteral("[%1]").arg(i);
e.parentId = arrId; e.offset = i * elemSz;
doc->tree.addNode(e);
}
}
};
// Root struct (not collapsed so fields are visible on open)
@@ -375,8 +406,8 @@ void MainWindow::newFile() {
// 0x008 0x04F
addField(peb, 0x008, NodeKind::Pointer64, "Mutant");
addField(peb, 0x010, NodeKind::Pointer64, "ImageBaseAddress");
addField(peb, 0x018, NodeKind::Pointer64, "Ldr");
addField(peb, 0x020, NodeKind::Pointer64, "ProcessParameters");
uint64_t ldrPtrId = addField(peb, 0x018, NodeKind::Pointer64, "Ldr");
uint64_t ppPtrId = addField(peb, 0x020, NodeKind::Pointer64, "ProcessParameters");
addField(peb, 0x028, NodeKind::Pointer64, "SubSystemData");
addField(peb, 0x030, NodeKind::Pointer64, "ProcessHeap");
addField(peb, 0x038, NodeKind::Pointer64, "FastPebLock");
@@ -509,6 +540,45 @@ void MainWindow::newFile() {
addField(peb, 0x7C4, NodeKind::UInt32, "NtGlobalFlag2");
addField(peb, 0x7C8, NodeKind::UInt64, "ExtendedFeatureDisableMask");
// ── Stub structs for pointer deref demo ──
// _PEB_LDR_DATA (Ldr target)
uint64_t ldrData = addStruct(0, 0x800, "_PEB_LDR_DATA", "LdrData");
addField(ldrData, 0x00, NodeKind::UInt32, "Length");
addField(ldrData, 0x04, NodeKind::UInt8, "Initialized");
addPad (ldrData, 0x05, 3, "Pad");
addField(ldrData, 0x08, NodeKind::Pointer64, "SsHandle");
{
uint64_t le = addStruct(ldrData, 0x10, "LIST_ENTRY64", "InLoadOrderModuleList");
addField(le, 0, NodeKind::Pointer64, "Flink");
addField(le, 8, NodeKind::Pointer64, "Blink");
}
{
uint64_t le = addStruct(ldrData, 0x20, "LIST_ENTRY64", "InMemoryOrderModuleList");
addField(le, 0, NodeKind::Pointer64, "Flink");
addField(le, 8, NodeKind::Pointer64, "Blink");
}
// _RTL_USER_PROCESS_PARAMETERS (ProcessParameters target)
uint64_t procParams = addStruct(0, 0x900, "_RTL_USER_PROCESS_PARAMETERS", "ProcessParams");
addField(procParams, 0x00, NodeKind::UInt32, "MaximumLength");
addField(procParams, 0x04, NodeKind::UInt32, "Length");
addField(procParams, 0x08, NodeKind::UInt32, "Flags");
addField(procParams, 0x0C, NodeKind::UInt32, "DebugFlags");
addField(procParams, 0x10, NodeKind::Pointer64, "ConsoleHandle");
addField(procParams, 0x18, NodeKind::UInt32, "ConsoleFlags");
addPad (procParams, 0x1C, 4, "Pad");
addField(procParams, 0x20, NodeKind::Pointer64, "StandardInput");
addField(procParams, 0x28, NodeKind::Pointer64, "StandardOutput");
addField(procParams, 0x30, NodeKind::Pointer64, "StandardError");
// Wire up pointer refIds
{
int li = doc->tree.indexOfId(ldrPtrId);
if (li >= 0) doc->tree.nodes[li].refId = ldrData;
int pi = doc->tree.indexOfId(ppPtrId);
if (pi >= 0) doc->tree.nodes[pi].refId = procParams;
}
createTab(doc);
}

View File

@@ -35,6 +35,7 @@ public:
ProcessProvider& operator=(const ProcessProvider&) = delete;
int size() const override { return m_size; }
bool isReadable(uint64_t, int len) const override { return len >= 0; }
bool read(uint64_t addr, void* buf, int len) const override {
SIZE_T got = 0;

View File

@@ -42,7 +42,7 @@ public:
bool isValid() const { return size() > 0; }
bool isReadable(uint64_t addr, int len) const {
virtual bool isReadable(uint64_t addr, int len) const {
if (len <= 0) return (len == 0);
uint64_t ulen = (uint64_t)len;
return addr <= (uint64_t)size() && ulen <= (uint64_t)size() - addr;