fix: narrow inline editor selection for pointer values, resolve correct write address

resolvedSpanFor() now applies narrowPtrValueSpan() to trim the "// Module+offset"
symbol comment from the editable span, matching hitTestTarget(). Previously the
full value column text was selected, making the parser fail on commit (toULongLong
rejected the non-hex suffix), so pointer value saves were silently no-ops.

With the parse now succeeding, a second bug was exposed: setNodeValue() computed
write addresses via computeOffset() which sums tree offsets without dereferencing
pointers. For nodes inside expanded pointer targets (e.g. VTable entries), this
wrote to struct_base+child_offset instead of *ptr_value+child_offset, causing an
access violation crash. The fix passes lm->offsetAddr (the compose-resolved
absolute address) through the inlineEditCommitted signal so setNodeValue() uses
the correct dereferenced address.
This commit is contained in:
IChooseYou
2026-02-19 13:05:25 -07:00
parent d989e2a947
commit 2a44d2ac57
4 changed files with 33 additions and 13 deletions

View File

@@ -230,7 +230,8 @@ void RcxController::connectEditor(RcxEditor* editor) {
// Inline editing signals // Inline editing signals
connect(editor, &RcxEditor::inlineEditCommitted, connect(editor, &RcxEditor::inlineEditCommitted,
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) { this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
uint64_t resolvedAddr) {
// CommandRow BaseAddress/Source/RootClass edit has nodeIdx=-1 // CommandRow BaseAddress/Source/RootClass edit has nodeIdx=-1
if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source
&& target != EditTarget::RootClassType && target != EditTarget::RootClassName) { refresh(); return; } && target != EditTarget::RootClassType && target != EditTarget::RootClassName) { refresh(); return; }
@@ -241,7 +242,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
const Node& node = m_doc->tree.nodes[nodeIdx]; const Node& node = m_doc->tree.nodes[nodeIdx];
// ASCII edit on Hex 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, resolvedAddr);
} else { } else {
renameNode(nodeIdx, text); renameNode(nodeIdx, text);
} }
@@ -311,7 +312,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
break; break;
} }
case EditTarget::Value: case EditTarget::Value:
setNodeValue(nodeIdx, subLine, text); setNodeValue(nodeIdx, subLine, text, /*isAscii=*/false, resolvedAddr);
break; break;
case EditTarget::BaseAddress: { case EditTarget::BaseAddress: {
QString s = text.trimmed(); QString s = text.trimmed();
@@ -1023,14 +1024,22 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
} }
void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text, void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
bool isAscii) { bool isAscii, uint64_t resolvedAddr) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
if (!m_doc->provider->isWritable()) return; if (!m_doc->provider->isWritable()) return;
const Node& node = m_doc->tree.nodes[nodeIdx]; const Node& node = m_doc->tree.nodes[nodeIdx];
int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx);
if (signedAddr < 0) return; // malformed tree: negative offset // Use the compose-resolved address when available (correct for pointer children).
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedAddr); // Fall back to tree.baseAddress + computeOffset for callers that don't supply it.
uint64_t addr;
if (resolvedAddr != 0) {
addr = resolvedAddr;
} else {
int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx);
if (signedAddr < 0) return; // malformed tree: negative offset
addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedAddr);
}
// For vector components, redirect to float parsing at sub-offset // For vector components, redirect to float parsing at sub-offset
NodeKind editKind = node.kind; NodeKind editKind = node.kind;

View File

@@ -92,7 +92,8 @@ public:
void removeNode(int nodeIdx); void removeNode(int nodeIdx);
void toggleCollapse(int nodeIdx); void toggleCollapse(int nodeIdx);
void materializeRefChildren(int nodeIdx); void materializeRefChildren(int nodeIdx);
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false); void setNodeValue(int nodeIdx, int subLine, const QString& text,
bool isAscii = false, uint64_t resolvedAddr = 0);
void duplicateNode(int nodeIdx); void duplicateNode(int nodeIdx);
void convertToTypedPointer(uint64_t nodeId); void convertToTypedPointer(uint64_t nodeId);
void splitHexNode(uint64_t nodeId); void splitHexNode(uint64_t nodeId);

View File

@@ -488,8 +488,10 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
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::ArrayElementType
|| m_editState.target == EditTarget::PointerTarget)) { || m_editState.target == EditTarget::PointerTarget)) {
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit(); auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
} }
}); });
@@ -1515,7 +1517,8 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
switch (t) { switch (t) {
case EditTarget::Type: s = typeSpan(*lm, typeW); break; case EditTarget::Type: s = typeSpan(*lm, typeW); break;
case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break; case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break;
case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break; case EditTarget::Value: s = narrowPtrValueSpan(*lm,
valueSpan(*lm, textLen, typeW, nameW), lineText); break;
case EditTarget::BaseAddress: break; // No longer on header lines case EditTarget::BaseAddress: break; // No longer on header lines
case EditTarget::ArrayIndex: case EditTarget::ArrayIndex:
case EditTarget::ArrayCount: case EditTarget::ArrayCount:
@@ -2368,8 +2371,12 @@ void RcxEditor::commitInlineEdit() {
if (m_editState.target == EditTarget::Type && editedText.isEmpty()) if (m_editState.target == EditTarget::Type && editedText.isEmpty())
editedText = m_editState.original; editedText = m_editState.original;
// Grab resolved address from LineMeta before endInlineEdit clears state
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit(); auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText, addr);
} }
// ── Cancel inline edit ── // ── Cancel inline edit ──
@@ -2469,13 +2476,15 @@ void RcxEditor::showSourcePicker() {
QAction* sel = menu.exec(pos); QAction* sel = menu.exec(pos);
if (sel) { if (sel) {
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit(); auto info = endInlineEdit();
QString text = sel->text(); QString text = sel->text();
if (sel->data().toString() == QStringLiteral("#clear")) if (sel->data().toString() == QStringLiteral("#clear"))
text = QStringLiteral("#clear"); text = QStringLiteral("#clear");
else if (sel->data().isValid()) else if (sel->data().isValid())
text = QStringLiteral("#saved:") + QString::number(sel->data().toInt()); text = QStringLiteral("#saved:") + QString::number(sel->data().toInt());
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
} else { } else {
cancelInlineEdit(); cancelInlineEdit();
} }

View File

@@ -69,7 +69,8 @@ signals:
void keywordConvertRequested(const QString& newKeyword); void keywordConvertRequested(const QString& newKeyword);
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods); void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine, void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text); EditTarget target, const QString& text,
uint64_t resolvedAddr = 0);
void inlineEditCancelled(); void inlineEditCancelled();
void typeSelectorRequested(); void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos); void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);