Fix 7 verified bugs: ref invalidation, bounds check, double refresh, dangling pointer, undo bypass, overflow, hash collision

- BUG-1 (HIGH): Replace dangling QVector reference with local copies in applyTypePopupResult
- BUG-2 (MEDIUM): Add missing upper-bound check in EditTarget::Name handler
- BUG-5 (LOW): Remove redundant unconditional refresh() at end of applyTypePopupResult
- BUG-6 (LOW): Use QPointer for m_cachedPopup to auto-null on parent destruction
- BUG-7 (LOW): Rewrite materializeRefChildren to use undo macro (cmd::Insert + cmd::Collapse)
- BUG-8 (LOW): Guard against integer overflow in byteSize() and clamp arrayLen/strLen in fromJson
- BUG-9 (LOW): Use QPair<uint64_t,uint64_t> key in collectPointerRanges visited set
This commit is contained in:
IChooseYou
2026-02-15 08:14:07 -07:00
committed by sysadmin
parent 0ef9841f90
commit 4c6bb9564f
3 changed files with 180 additions and 87 deletions

View File

@@ -234,8 +234,9 @@ void RcxController::connectEditor(RcxEditor* editor) {
switch (target) { switch (target) {
case EditTarget::Name: { case EditTarget::Name: {
if (text.isEmpty()) break; if (text.isEmpty()) break;
if (nodeIdx >= m_doc->tree.nodes.size()) break;
const Node& node = m_doc->tree.nodes[nodeIdx]; const Node& node = m_doc->tree.nodes[nodeIdx];
// ASCII edit on Hex/Padding nodes // ASCII edit on Hex nodes
if (isHexPreview(node.kind)) { if (isHexPreview(node.kind)) {
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true); setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
} else { } else {
@@ -610,7 +611,7 @@ void RcxController::refresh() {
if (isHexPreview(node.kind)) { if (isHexPreview(node.kind)) {
// Per-byte tracking for hex preview nodes // Per-byte tracking for hex preview nodes
int lineOff = (node.kind == NodeKind::Padding) ? lm.subLine * 8 : 0; int lineOff = 0;
int byteCount = lm.lineByteCount; int byteCount = lm.lineByteCount;
for (int b = 0; b < byteCount; b++) { for (int b = 0; b < byteCount; b++) {
if (m_changedOffsets.contains(offset + lineOff + b)) { if (m_changedOffsets.contains(offset + lineOff + b)) {
@@ -807,7 +808,7 @@ void RcxController::materializeRefChildren(int nodeIdx) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& tree = m_doc->tree; auto& tree = m_doc->tree;
// Snapshot values before addNode invalidates references // Snapshot values before any mutation invalidates references
const uint64_t parentId = tree.nodes[nodeIdx].id; const uint64_t parentId = tree.nodes[nodeIdx].id;
const uint64_t refId = tree.nodes[nodeIdx].refId; const uint64_t refId = tree.nodes[nodeIdx].refId;
const NodeKind parentKind = tree.nodes[nodeIdx].kind; const NodeKind parentKind = tree.nodes[nodeIdx].kind;
@@ -816,29 +817,43 @@ void RcxController::materializeRefChildren(int nodeIdx) {
if (refId == 0) return; if (refId == 0) return;
if (!tree.childrenOf(parentId).isEmpty()) return; // already materialized if (!tree.childrenOf(parentId).isEmpty()) return; // already materialized
// Clone all children of the referenced struct as real children of this struct // Collect children to clone (copy by value to avoid reference invalidation)
QVector<int> refChildren = tree.childrenOf(refId); QVector<int> refChildren = tree.childrenOf(refId);
if (refChildren.isEmpty()) return;
QVector<Node> clones;
clones.reserve(refChildren.size());
for (int ci : refChildren) { for (int ci : refChildren) {
Node copy = tree.nodes[ci]; Node copy = tree.nodes[ci]; // copy by value before any mutation
copy.id = 0; // auto-assign new ID copy.id = tree.reserveId();
copy.parentId = parentId; copy.parentId = parentId;
copy.collapsed = true; // start collapsed copy.collapsed = true;
tree.addNode(copy); clones.append(copy);
}
// Wrap all mutations in an undo macro
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Materialize ref children"));
for (const Node& clone : clones) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::Insert{clone, {}}));
} }
tree.invalidateIdCache();
// Auto-expand the self-referential child (the one that was the cycle) // Auto-expand the self-referential child (the one that was the cycle)
// so the user gets expand in a single click // so the user gets expand in a single click
QVector<int> newChildren = tree.childrenOf(parentId); for (const Node& clone : clones) {
for (int ci : newChildren) { if (clone.kind == parentKind && clone.name == parentName && clone.refId == refId) {
auto& c = tree.nodes[ci]; m_doc->undoStack.push(new RcxCommand(this,
if (c.kind == parentKind && c.name == parentName && c.refId == refId) { cmd::Collapse{clone.id, true, false}));
c.collapsed = false;
break; break;
} }
} }
refresh(); m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
} }
void RcxController::applyCommand(const Command& command, bool isUndo) { void RcxController::applyCommand(const Command& command, bool isUndo) {
@@ -1101,23 +1116,23 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// Quick-convert suggestions for Hex nodes // Quick-convert suggestions for Hex nodes
bool addedQuickConvert = false; bool addedQuickConvert = false;
if (node.kind == NodeKind::Hex64) { if (node.kind == NodeKind::Hex64) {
menu.addAction(icon("symbol-numeric.svg"), "Change to uint64_t", [this, nodeId]() { menu.addAction("Change to uint64_t", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId); int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt64); if (ni >= 0) changeNodeKind(ni, NodeKind::UInt64);
}); });
menu.addAction(icon("symbol-numeric.svg"), "Change to uint32_t", [this, nodeId]() { menu.addAction("Change to uint32_t", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId); int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32); if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
}); });
addedQuickConvert = true; addedQuickConvert = true;
} else if (node.kind == NodeKind::Hex32) { } else if (node.kind == NodeKind::Hex32) {
menu.addAction(icon("symbol-numeric.svg"), "Change to uint32_t", [this, nodeId]() { menu.addAction("Change to uint32_t", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId); int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32); if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
}); });
addedQuickConvert = true; addedQuickConvert = true;
} else if (node.kind == NodeKind::Hex16) { } else if (node.kind == NodeKind::Hex16) {
menu.addAction(icon("symbol-numeric.svg"), "Change to int16_t", [this, nodeId]() { menu.addAction("Change to int16_t", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId); int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, NodeKind::Int16); if (ni >= 0) changeNodeKind(ni, NodeKind::Int16);
}); });
@@ -1127,7 +1142,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
menu.addSeparator(); menu.addSeparator();
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
&& node.kind != NodeKind::Padding
&& m_doc->provider->isWritable(); && m_doc->provider->isWritable();
if (isEditable) { if (isEditable) {
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() { menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {
@@ -1143,6 +1157,51 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
editor->beginInlineEdit(EditTarget::Type, line); editor->beginInlineEdit(EditTarget::Type, line);
}); });
// Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8)
if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
menu.addAction("Convert to &Hex", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
const Node& n = m_doc->tree.nodes[ni];
int totalSize = n.byteSize();
if (totalSize <= 0) return;
uint64_t parentId = n.parentId;
int baseOffset = n.offset;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Convert to Hex"));
// Remove the original node
QVector<Node> subtree;
subtree.append(n);
m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{nodeId, subtree, {}}));
// Insert hex nodes to fill the space (largest first)
int padOffset = baseOffset;
int gap = totalSize;
while (gap > 0) {
NodeKind padKind;
int padSize;
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
else { padKind = NodeKind::Hex8; padSize = 1; }
insertNode(parentId, padOffset, padKind,
QString("pad_%1").arg(padOffset, 2, 16, QChar('0')));
padOffset += padSize;
gap -= padSize;
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
});
}
menu.addSeparator(); menu.addSeparator();
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) { if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
@@ -1404,17 +1463,17 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
return tree.nodes[a].offset < tree.nodes[b].offset; return tree.nodes[a].offset < tree.nodes[b].offset;
}); });
// Separate into real nodes (non-Padding) and padding nodes // Separate into real nodes (non-hex) and hex filler nodes
struct NodeInfo { uint64_t id; int offset; int size; }; struct NodeInfo { uint64_t id; int offset; int size; };
QVector<NodeInfo> realNodes; QVector<NodeInfo> realNodes;
QVector<uint64_t> padIds; QVector<uint64_t> hexIds;
for (int ci : kids) { for (int ci : kids) {
const Node& child = tree.nodes[ci]; const Node& child = tree.nodes[ci];
int sz = (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) int sz = (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
? tree.structSpan(child.id) : child.byteSize(); ? tree.structSpan(child.id) : child.byteSize();
if (child.kind == NodeKind::Padding) if (isHexNode(child.kind))
padIds.append(child.id); hexIds.append(child.id);
else else
realNodes.append({child.id, child.offset, sz}); realNodes.append({child.id, child.offset, sz});
} }
@@ -1447,7 +1506,7 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
} }
// Check if anything actually changes // Check if anything actually changes
if (offChanges.isEmpty() && padIds.isEmpty() && padsNeeded.isEmpty()) if (offChanges.isEmpty() && hexIds.isEmpty() && padsNeeded.isEmpty())
return; return;
// Apply as undoable macro // Apply as undoable macro
@@ -1455,14 +1514,14 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
m_suppressRefresh = true; m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Realign to %1").arg(targetAlign)); m_doc->undoStack.beginMacro(QStringLiteral("Realign to %1").arg(targetAlign));
// 1. Remove all existing Padding nodes (no offset adjustments — we recompute) // 1. Remove all existing hex filler nodes (no offset adjustments — we recompute)
for (uint64_t pid : padIds) { for (uint64_t hid : hexIds) {
int idx = tree.indexOfId(pid); int idx = tree.indexOfId(hid);
if (idx < 0) continue; if (idx < 0) continue;
QVector<Node> subtree; QVector<Node> subtree;
subtree.append(tree.nodes[idx]); subtree.append(tree.nodes[idx]);
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{pid, subtree, {}})); cmd::Remove{hid, subtree, {}}));
} }
// 2. Reposition real nodes // 2. Reposition real nodes
@@ -1471,15 +1530,28 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
cmd::ChangeOffset{oc.id, oc.oldOff, oc.newOff})); cmd::ChangeOffset{oc.id, oc.oldOff, oc.newOff}));
} }
// 3. Insert new padding in gaps // 3. Insert hex nodes to fill gaps (largest first for alignment)
for (const auto& pi : padsNeeded) { for (const auto& pi : padsNeeded) {
int padOffset = pi.offset;
int gap = pi.size;
while (gap > 0) {
NodeKind padKind;
int padSize;
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
else { padKind = NodeKind::Hex8; padSize = 1; }
Node pad; Node pad;
pad.kind = NodeKind::Padding; pad.kind = padKind;
pad.parentId = structId; pad.parentId = structId;
pad.offset = pi.offset; pad.offset = padOffset;
pad.arrayLen = pi.size; pad.name = QString("pad_%1").arg(padOffset, 2, 16, QChar('0'));
pad.id = tree.reserveId(); pad.id = tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad})); m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad}));
padOffset += padSize;
gap -= padSize;
}
} }
m_doc->undoStack.endMacro(); m_doc->undoStack.endMacro();
@@ -1583,7 +1655,6 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) { auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
for (const auto& m : kKindMeta) { for (const auto& m : kKindMeta) {
if (m.kind == NodeKind::Padding) continue;
if (excludeStructArrayPad && if (excludeStructArrayPad &&
(m.kind == NodeKind::Struct || m.kind == NodeKind::Array)) (m.kind == NodeKind::Struct || m.kind == NodeKind::Array))
continue; continue;
@@ -1718,6 +1789,10 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
}); });
connect(popup, &TypeSelectorPopup::createNewTypeRequested, connect(popup, &TypeSelectorPopup::createNewTypeRequested,
this, [this, mode, nodeIdx]() { this, [this, mode, nodeIdx]() {
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Create new type"));
Node n; Node n;
n.kind = NodeKind::Struct; n.kind = NodeKind::Struct;
n.name = QString(); n.name = QString();
@@ -1725,6 +1800,16 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
n.offset = 0; n.offset = 0;
n.id = m_doc->tree.reserveId(); n.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n})); m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
// Populate with default hex nodes (8 x Hex64 = 64 bytes)
for (int i = 0; i < 8; i++) {
insertNode(n.id, i * 8, NodeKind::Hex64,
QString("field_%1").arg(i * 8, 2, 16, QChar('0')));
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
TypeEntry newEntry; TypeEntry newEntry;
newEntry.entryKind = TypeEntry::Composite; newEntry.entryKind = TypeEntry::Composite;
newEntry.structId = n.id; newEntry.structId = n.id;
@@ -1743,14 +1828,22 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
} }
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
const Node& node = m_doc->tree.nodes[nodeIdx];
// BUG-1 fix: Copy needed fields to locals before any mutation.
// changeNodeKind() can trigger insertNode() → addNode() → nodes.append(),
// which may reallocate the QVector, invalidating any reference into it.
const uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id;
const NodeKind nodeKind = m_doc->tree.nodes[nodeIdx].kind;
const NodeKind elemKind = m_doc->tree.nodes[nodeIdx].elementKind;
const uint64_t nodeRefId = m_doc->tree.nodes[nodeIdx].refId;
const int arrLen = m_doc->tree.nodes[nodeIdx].arrayLen;
// Parse the full text for modifiers (e.g. "int32_t[10]", "Ball*") // Parse the full text for modifiers (e.g. "int32_t[10]", "Ball*")
TypeSpec spec = parseTypeSpec(fullText); TypeSpec spec = parseTypeSpec(fullText);
if (mode == TypePopupMode::FieldType) { if (mode == TypePopupMode::FieldType) {
if (entry.entryKind == TypeEntry::Primitive) { if (entry.entryKind == TypeEntry::Primitive) {
if (entry.primitiveKind != node.kind) if (entry.primitiveKind != nodeKind)
changeNodeKind(nodeIdx, entry.primitiveKind); changeNodeKind(nodeIdx, entry.primitiveKind);
} else if (entry.entryKind == TypeEntry::Composite) { } else if (entry.entryKind == TypeEntry::Composite) {
bool wasSuppressed = m_suppressRefresh; bool wasSuppressed = m_suppressRefresh;
@@ -1759,34 +1852,34 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
if (spec.isPointer) { if (spec.isPointer) {
// Pointer modifier: e.g. "Material*" → Pointer64 + refId // Pointer modifier: e.g. "Material*" → Pointer64 + refId
if (node.kind != NodeKind::Pointer64) if (nodeKind != NodeKind::Pointer64)
changeNodeKind(nodeIdx, NodeKind::Pointer64); changeNodeKind(nodeIdx, NodeKind::Pointer64);
int idx = m_doc->tree.indexOfId(node.id); int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0 && m_doc->tree.nodes[idx].refId != entry.structId) if (idx >= 0 && m_doc->tree.nodes[idx].refId != entry.structId)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{node.id, m_doc->tree.nodes[idx].refId, entry.structId})); cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId}));
} else if (spec.arrayCount > 0) { } else if (spec.arrayCount > 0) {
// Array modifier: e.g. "Material[10]" → Array + Struct element // Array modifier: e.g. "Material[10]" → Array + Struct element
if (node.kind != NodeKind::Array) if (nodeKind != NodeKind::Array)
changeNodeKind(nodeIdx, NodeKind::Array); changeNodeKind(nodeIdx, NodeKind::Array);
int idx = m_doc->tree.indexOfId(node.id); int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) { if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx]; auto& n = m_doc->tree.nodes[idx];
if (n.elementKind != NodeKind::Struct || n.arrayLen != spec.arrayCount) if (n.elementKind != NodeKind::Struct || n.arrayLen != spec.arrayCount)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{node.id, n.elementKind, NodeKind::Struct, cmd::ChangeArrayMeta{nodeId, n.elementKind, NodeKind::Struct,
n.arrayLen, spec.arrayCount})); n.arrayLen, spec.arrayCount}));
if (n.refId != entry.structId) if (n.refId != entry.structId)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{node.id, n.refId, entry.structId})); cmd::ChangePointerRef{nodeId, n.refId, entry.structId}));
} }
} else { } else {
// Plain struct: e.g. "Material" → Struct + structTypeName + refId + collapsed // Plain struct: e.g. "Material" → Struct + structTypeName + refId + collapsed
if (node.kind != NodeKind::Struct) if (nodeKind != NodeKind::Struct)
changeNodeKind(nodeIdx, NodeKind::Struct); changeNodeKind(nodeIdx, NodeKind::Struct);
int idx = m_doc->tree.indexOfId(node.id); int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) { if (idx >= 0) {
int refIdx = m_doc->tree.indexOfId(entry.structId); int refIdx = m_doc->tree.indexOfId(entry.structId);
QString targetName; QString targetName;
@@ -1797,11 +1890,11 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
QString oldTypeName = m_doc->tree.nodes[idx].structTypeName; QString oldTypeName = m_doc->tree.nodes[idx].structTypeName;
if (oldTypeName != targetName) if (oldTypeName != targetName)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeStructTypeName{node.id, oldTypeName, targetName})); cmd::ChangeStructTypeName{nodeId, oldTypeName, targetName}));
// Set refId so compose can expand the referenced struct's children // Set refId so compose can expand the referenced struct's children
if (m_doc->tree.nodes[idx].refId != entry.structId) if (m_doc->tree.nodes[idx].refId != entry.structId)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{node.id, m_doc->tree.nodes[idx].refId, entry.structId})); cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId}));
// ChangePointerRef auto-sets collapsed=true when refId != 0 // ChangePointerRef auto-sets collapsed=true when refId != 0
} }
} }
@@ -1812,33 +1905,32 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
} }
} else if (mode == TypePopupMode::ArrayElement) { } else if (mode == TypePopupMode::ArrayElement) {
if (entry.entryKind == TypeEntry::Primitive) { if (entry.entryKind == TypeEntry::Primitive) {
if (entry.primitiveKind != node.elementKind) { if (entry.primitiveKind != elemKind) {
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{node.id, cmd::ChangeArrayMeta{nodeId,
node.elementKind, entry.primitiveKind, elemKind, entry.primitiveKind,
node.arrayLen, node.arrayLen})); arrLen, arrLen}));
} }
} else if (entry.entryKind == TypeEntry::Composite) { } else if (entry.entryKind == TypeEntry::Composite) {
if (node.elementKind != NodeKind::Struct || node.refId != entry.structId) { if (elemKind != NodeKind::Struct || nodeRefId != entry.structId) {
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{node.id, cmd::ChangeArrayMeta{nodeId,
node.elementKind, NodeKind::Struct, elemKind, NodeKind::Struct,
node.arrayLen, node.arrayLen})); arrLen, arrLen}));
if (node.refId != entry.structId) { if (nodeRefId != entry.structId) {
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{node.id, node.refId, entry.structId})); cmd::ChangePointerRef{nodeId, nodeRefId, entry.structId}));
} }
} }
} }
} else if (mode == TypePopupMode::PointerTarget) { } else if (mode == TypePopupMode::PointerTarget) {
// "void" entry → refId 0; composite entry → real structId // "void" entry → refId 0; composite entry → real structId
uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0; uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0;
if (realRefId != node.refId) { if (realRefId != nodeRefId) {
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{node.id, node.refId, realRefId})); cmd::ChangePointerRef{nodeId, nodeRefId, realRefId}));
} }
} }
refresh();
} }
void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) { void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) {
@@ -1921,11 +2013,11 @@ void RcxController::setupAutoRefresh() {
void RcxController::collectPointerRanges( void RcxController::collectPointerRanges(
uint64_t structId, uint64_t memBase, uint64_t structId, uint64_t memBase,
int depth, int maxDepth, int depth, int maxDepth,
QSet<uint64_t>& visited, QSet<QPair<uint64_t,uint64_t>>& visited,
QVector<QPair<uint64_t,int>>& ranges) const QVector<QPair<uint64_t,int>>& ranges) const
{ {
if (depth >= maxDepth) return; if (depth >= maxDepth) return;
uint64_t key = memBase ^ (structId * 0x9E3779B97F4A7C15ULL); QPair<uint64_t,uint64_t> key{structId, memBase};
if (visited.contains(key)) return; if (visited.contains(key)) return;
visited.insert(key); visited.insert(key);
@@ -1982,7 +2074,7 @@ void RcxController::onRefreshTick() {
ranges.append({0, extent}); ranges.append({0, extent});
if (m_snapshotProv) { if (m_snapshotProv) {
QSet<uint64_t> visited; QSet<QPair<uint64_t,uint64_t>> visited;
uint64_t rootId = m_viewRootId; uint64_t rootId = m_viewRootId;
if (rootId == 0 && !m_doc->tree.nodes.isEmpty()) if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
rootId = m_doc->tree.nodes[0].id; rootId = m_doc->tree.nodes[0].id;

View File

@@ -7,6 +7,7 @@
#include <QUndoCommand> #include <QUndoCommand>
#include <QTimer> #include <QTimer>
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QPointer>
#include <memory> #include <memory>
namespace rcx { namespace rcx {
@@ -138,7 +139,7 @@ private:
int m_activeSourceIdx = -1; int m_activeSourceIdx = -1;
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ── // ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
TypeSelectorPopup* m_cachedPopup = nullptr; QPointer<TypeSelectorPopup> m_cachedPopup;
// ── Auto-refresh state ── // ── Auto-refresh state ──
using PageMap = QHash<uint64_t, QByteArray>; using PageMap = QHash<uint64_t, QByteArray>;
@@ -169,7 +170,7 @@ private:
void resetSnapshot(); void resetSnapshot();
void collectPointerRanges(uint64_t structId, uint64_t memBase, void collectPointerRanges(uint64_t structId, uint64_t memBase,
int depth, int maxDepth, int depth, int maxDepth,
QSet<uint64_t>& visited, QSet<QPair<uint64_t,uint64_t>>& visited,
QVector<QPair<uint64_t,int>>& ranges) const; QVector<QPair<uint64_t,int>>& ranges) const;
}; };

View File

@@ -27,7 +27,6 @@ enum class NodeKind : uint8_t {
Pointer32, Pointer64, Pointer32, Pointer64,
Vec2, Vec3, Vec4, Mat4x4, Vec2, Vec3, Vec4, Mat4x4,
UTF8, UTF16, UTF8, UTF16,
Padding,
Struct, Array Struct, Array
}; };
@@ -37,11 +36,11 @@ inline uint qHash(rcx::NodeKind key, uint seed = 0) { return ::qHash(static_cast
#endif #endif
namespace rcx { // reopen namespace rcx { // reopen
// ── Kind flags (replaces repeated Hex/Padding switches) ── // ── Kind flags (replaces repeated Hex switches) ──
enum KindFlags : uint32_t { enum KindFlags : uint32_t {
KF_None = 0, KF_None = 0,
KF_HexPreview = 1 << 0, // Hex8..Hex64 + Padding (ASCII+hex layout) KF_HexPreview = 1 << 0, // Hex8..Hex64 (ASCII+hex layout)
KF_Container = 1 << 1, // Struct/Array KF_Container = 1 << 1, // Struct/Array
KF_String = 1 << 2, // UTF8/UTF16 KF_String = 1 << 2, // UTF8/UTF16
KF_Vector = 1 << 3, // Vec2/3/4 KF_Vector = 1 << 3, // Vec2/3/4
@@ -84,7 +83,6 @@ inline constexpr KindMeta kKindMeta[] = {
{NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None}, {NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String}, {NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String}, {NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String},
{NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview},
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container}, {NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container}, {NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
}; };
@@ -155,7 +153,6 @@ inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
enum Marker : int { enum Marker : int {
M_CONT = 0, M_CONT = 0,
M_PAD = 1,
M_PTR0 = 2, M_PTR0 = 2,
M_CYCLE = 3, M_CYCLE = 3,
M_ERR = 4, M_ERR = 4,
@@ -187,9 +184,12 @@ struct Node {
int byteSize() const { int byteSize() const {
switch (kind) { switch (kind) {
case NodeKind::UTF8: return strLen; case NodeKind::UTF8: return strLen;
case NodeKind::UTF16: return strLen * 2; case NodeKind::UTF16: return qMin(strLen, INT_MAX / 2) * 2;
case NodeKind::Padding: return qMax(1, arrayLen); case NodeKind::Array: {
case NodeKind::Array: return arrayLen * sizeForKind(elementKind); int elemSz = sizeForKind(elementKind);
if (elemSz <= 0) return 0;
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
}
default: return sizeForKind(kind); default: return sizeForKind(kind);
} }
} }
@@ -221,8 +221,8 @@ struct Node {
n.classKeyword = o["classKeyword"].toString(); n.classKeyword = o["classKeyword"].toString();
n.parentId = o["parentId"].toString("0").toULongLong(); n.parentId = o["parentId"].toString("0").toULongLong();
n.offset = o["offset"].toInt(0); n.offset = o["offset"].toInt(0);
n.arrayLen = o["arrayLen"].toInt(1); n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
n.strLen = o["strLen"].toInt(64); n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
n.collapsed = o["collapsed"].toBool(false); n.collapsed = o["collapsed"].toBool(false);
n.refId = o["refId"].toString("0").toULongLong(); n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8")); n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
@@ -535,7 +535,7 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
int ind = kFoldCol + lm.depth * 3; int ind = kFoldCol + lm.depth * 3;
int start = ind + typeW + kSepWidth; int start = ind + typeW + kSepWidth;
// Hex/Padding: ASCII preview occupies the name column (padded to nameW) // Hex: ASCII preview occupies the name column (padded to nameW)
if (isHexPreview(lm.nodeKind)) if (isHexPreview(lm.nodeKind))
return {start, start + nameW, true}; return {start, start + nameW, true};
@@ -547,9 +547,9 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
lm.lineKind == LineKind::ArrayElementSeparator) return {}; lm.lineKind == LineKind::ArrayElementSeparator) return {};
int ind = kFoldCol + lm.depth * 3; int ind = kFoldCol + lm.depth * 3;
// Hex/Padding uses nameW for ASCII column (same as regular name column) // Hex uses nameW for ASCII column (same as regular name column)
bool isHexPad = isHexPreview(lm.nodeKind); bool isHex = isHexPreview(lm.nodeKind);
int valWidth = isHexPad ? 23 : kColValue; int valWidth = isHex ? 23 : kColValue;
int prefixW = typeW + nameW + 2 * kSepWidth; int prefixW = typeW + nameW + 2 * kSepWidth;
@@ -567,8 +567,8 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
int ind = kFoldCol + lm.depth * 3; int ind = kFoldCol + lm.depth * 3;
bool isHexPad = isHexPreview(lm.nodeKind); bool isHex = isHexPreview(lm.nodeKind);
int valWidth = isHexPad ? 23 : kColValue; int valWidth = isHex ? 23 : kColValue;
int prefixW = typeW + nameW + 2 * kSepWidth; int prefixW = typeW + nameW + 2 * kSepWidth;
int start; int start;