Add AddressParser + tests, remove symbol from commandrow

This commit is contained in:
Sen66
2026-02-21 17:03:44 +01:00
parent b089e20d36
commit 8e88d588be
14 changed files with 670 additions and 120 deletions

300
src/addressparser.cpp Normal file
View File

@@ -0,0 +1,300 @@
#include "addressparser.h"
namespace rcx {
// ── Address Expression Parser ──────────────────────────────────────────
//
// Parses expressions like:
// "7FF66CCE0000" → plain hex address
// "0x100 + 0x200" → arithmetic on hex values
// "<Program.exe> + 0xDE" → module base + offset
// "[<Program.exe> + 0xDE] - AB" → dereference pointer, then subtract
// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing)
//
// Grammar (standard operator precedence: *, / bind tighter than +, -):
//
// expr = term (('+' | '-') term)*
// term = unary (('*' | '/') unary)*
// unary = '-' unary | atom
// atom = '[' expr ']' -- read pointer at address (dereference)
// | '<' moduleName '>' -- resolve module base address
// | '(' expr ')' -- grouping
// | hexLiteral -- hex number, optional 0x prefix
//
// All numeric literals are hexadecimal (base 16).
// Module names and pointer reads are resolved via optional callbacks.
// Without callbacks, modules and dereferences evaluate to 0 (syntax-check mode).
class ExpressionParser {
public:
ExpressionParser(const QString& input, const AddressParserCallbacks* callbacks)
: m_input(input), m_callbacks(callbacks) {}
AddressParseResult parse() {
skipSpaces();
if (atEnd())
return error("empty expression");
uint64_t value = 0;
if (!parseExpression(value))
return error(m_error);
skipSpaces();
if (!atEnd())
return error(QStringLiteral("unexpected '%1'").arg(m_input[m_pos]));
return {true, value, {}, -1};
}
private:
const QString& m_input;
const AddressParserCallbacks* m_callbacks;
int m_pos = 0;
QString m_error;
int m_errorPos = 0;
// ── Helpers ──
bool atEnd() const { return m_pos >= m_input.size(); }
QChar peek() const { return atEnd() ? QChar('\0') : m_input[m_pos]; }
void advance() { m_pos++; }
void skipSpaces() {
while (!atEnd() && m_input[m_pos].isSpace())
m_pos++;
}
AddressParseResult error(const QString& msg) const {
return {false, 0, msg, m_errorPos};
}
bool fail(const QString& msg) {
m_error = msg;
m_errorPos = m_pos;
return false;
}
bool expect(QChar ch) {
skipSpaces();
if (peek() != ch)
return fail(QStringLiteral("expected '%1'").arg(ch));
advance();
return true;
}
static bool isHexDigit(QChar ch) {
return (ch >= '0' && ch <= '9')
|| (ch >= 'a' && ch <= 'f')
|| (ch >= 'A' && ch <= 'F');
}
// ── Recursive descent parsing ──
// expr = term (('+' | '-') term)*
bool parseExpression(uint64_t& result) {
if (!parseTerm(result))
return false;
for (;;) {
skipSpaces();
QChar op = peek();
if (op != '+' && op != '-')
break;
advance();
uint64_t rhs = 0;
if (!parseTerm(rhs))
return false;
result = (op == '+') ? result + rhs : result - rhs;
}
return true;
}
// term = unary (('*' | '/') unary)*
bool parseTerm(uint64_t& result) {
if (!parseUnary(result))
return false;
for (;;) {
skipSpaces();
QChar op = peek();
if (op != '*' && op != '/')
break;
advance();
uint64_t rhs = 0;
if (!parseUnary(rhs))
return false;
if (op == '*') {
result *= rhs;
} else {
if (rhs == 0)
return fail("division by zero");
result /= rhs;
}
}
return true;
}
// unary = '-' unary | atom
bool parseUnary(uint64_t& result) {
skipSpaces();
if (peek() == '-') {
advance();
uint64_t inner = 0;
if (!parseUnary(inner))
return false;
result = static_cast<uint64_t>(-static_cast<int64_t>(inner));
return true;
}
return parseAtom(result);
}
// atom = '[' expr ']' | '<' name '>' | '(' expr ')' | hexLiteral
bool parseAtom(uint64_t& result) {
skipSpaces();
if (atEnd())
return fail("unexpected end of expression");
QChar ch = peek();
if (ch == '[') return parseDereference(result);
if (ch == '<') return parseModuleName(result);
if (ch == '(') return parseGrouping(result);
return parseHexNumber(result);
}
// '[' expr ']' — read the pointer value at the computed address
bool parseDereference(uint64_t& result) {
advance(); // skip '['
uint64_t address = 0;
if (!parseExpression(address))
return false;
if (!expect(']'))
return false;
// Without a callback, just return 0 (syntax-check mode)
if (!m_callbacks || !m_callbacks->readPointer) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->readPointer(address, &ok);
if (!ok)
return fail(QStringLiteral("failed to read memory at 0x%1").arg(address, 0, 16));
return true;
}
// '<' moduleName '>' — resolve a module's base address (e.g. <Program.exe>)
bool parseModuleName(uint64_t& result) {
advance(); // skip '<'
int nameStart = m_pos;
while (!atEnd() && peek() != '>')
advance();
if (atEnd())
return fail("expected '>'");
QString name = m_input.mid(nameStart, m_pos - nameStart).trimmed();
advance(); // skip '>'
if (name.isEmpty())
return fail("empty module name");
// Without a callback, just return 0 (syntax-check mode)
if (!m_callbacks || !m_callbacks->resolveModule) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->resolveModule(name, &ok);
if (!ok)
return fail(QStringLiteral("module '%1' not found").arg(name));
return true;
}
// '(' expr ')' — parenthesized sub-expression for grouping
bool parseGrouping(uint64_t& result) {
advance(); // skip '('
if (!parseExpression(result))
return false;
return expect(')');
}
// Hex number with optional "0x" prefix. All literals are base-16.
bool parseHexNumber(uint64_t& result) {
skipSpaces();
if (atEnd())
return fail("unexpected end of expression");
int start = m_pos;
// Skip optional 0x/0X prefix
if (m_pos + 1 < m_input.size()
&& m_input[m_pos] == '0'
&& (m_input[m_pos + 1] == 'x' || m_input[m_pos + 1] == 'X'))
m_pos += 2;
// Consume hex digits
int digitsStart = m_pos;
while (!atEnd() && isHexDigit(peek()))
advance();
if (m_pos == digitsStart) {
m_errorPos = start;
return fail("expected hex number");
}
QString digits = m_input.mid(digitsStart, m_pos - digitsStart);
bool ok = false;
result = digits.toULongLong(&ok, 16);
if (!ok) {
m_errorPos = start;
return fail("invalid hex number");
}
return true;
}
};
// ── Public API ─────────────────────────────────────────────────────────
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
const AddressParserCallbacks* cb)
{
Q_UNUSED(ptrSize);
// WinDbg displays 64-bit addresses with backtick separators for readability,
// e.g. "00007ff6`1a2b3c4d". Strip them so users can paste directly.
// Also remove ' in case user uses it
QString cleaned = formula;
cleaned.remove('`');
cleaned.remove('\'');
ExpressionParser parser(cleaned, cb);
return parser.parse();
}
QString AddressParser::validate(const QString& formula)
{
QString cleaned = formula;
cleaned.remove('`');
cleaned.remove('\'');
cleaned = cleaned.trimmed();
if (cleaned.isEmpty())
return QStringLiteral("empty");
// Parse with no callbacks — modules and dereferences succeed but return 0.
// This checks syntax only.
ExpressionParser parser(cleaned, nullptr);
auto result = parser.parse();
return result.ok ? QString() : result.error;
}
} // namespace rcx

27
src/addressparser.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <QString>
#include <cstdint>
#include <functional>
namespace rcx {
struct AddressParseResult {
bool ok;
uint64_t value;
QString error;
int errorPos;
};
struct AddressParserCallbacks {
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
};
class AddressParser {
public:
static AddressParseResult evaluate(const QString& formula, int ptrSize = 8,
const AddressParserCallbacks* cb = nullptr);
static QString validate(const QString& formula);
};
} // namespace rcx

View File

@@ -1,4 +1,5 @@
#include "controller.h"
#include "addressparser.h"
#include "typeselectorpopup.h"
#include "providerregistry.h"
#include "themes/thememanager.h"
@@ -328,53 +329,29 @@ void RcxController::connectEditor(RcxEditor* editor) {
s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000)
s.remove('\n');
s.remove('\r');
// Support simple equations: 0x10+0x4, 0x100-0x10, etc.
uint64_t newBase = 0;
bool ok = true;
int pos = 0;
bool firstTerm = true;
bool adding = true;
while (pos < s.size() && ok) {
// Skip whitespace
while (pos < s.size() && s[pos].isSpace()) pos++;
if (pos >= s.size()) break;
// Check for +/- operator (except first term)
if (!firstTerm) {
if (s[pos] == '+') { adding = true; pos++; }
else if (s[pos] == '-') { adding = false; pos++; }
else { ok = false; break; }
while (pos < s.size() && s[pos].isSpace()) pos++;
}
// Parse hex number (with or without 0x prefix)
int start = pos;
bool hasPrefix = (pos + 1 < s.size() &&
s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'));
if (hasPrefix) pos += 2;
int numStart = pos;
while (pos < s.size() && (s[pos].isDigit() ||
(s[pos] >= 'a' && s[pos] <= 'f') ||
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
if (pos == numStart) { ok = false; break; }
QString numStr = s.mid(numStart, pos - numStart);
uint64_t val = numStr.toULongLong(&ok, 16);
if (!ok) break;
if (adding) newBase += val;
else newBase -= val;
firstTerm = false;
AddressParserCallbacks cbs;
if (m_doc->provider) {
auto* prov = m_doc->provider.get();
cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t {
uint64_t base = prov->symbolToAddress(name);
*ok = (base != 0);
return base;
};
cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t {
uint64_t val = 0;
*ok = prov->read(addr, &val, 8);
return val;
};
}
if (ok && newBase != m_doc->tree.baseAddress) {
auto result = AddressParser::evaluate(s, 8, &cbs);
if (result.ok && result.value != m_doc->tree.baseAddress) {
uint64_t oldBase = m_doc->tree.baseAddress;
QString oldFormula = m_doc->tree.baseAddressFormula;
// Store formula if input uses module/deref syntax, otherwise clear
QString newFormula = (s.contains('<') || s.contains('[')) ? s : QString();
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeBase{oldBase, newBase}));
cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula}));
}
break;
}
@@ -982,6 +959,7 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
clearHistoryForAdjs(c.offAdjs);
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
tree.baseAddressFormula = isUndo ? c.oldFormula : c.newFormula;
resetSnapshot();
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
@@ -1779,30 +1757,15 @@ void RcxController::updateCommandRow() {
.arg(provName);
}
// -- Symbol for selected node (getSymbol integration) --
QString sym;
if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin();
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 + m_doc->tree.computeOffset(idx);
sym = m_doc->provider->getSymbol(addr);
}
}
QString addr;
if (!m_doc->tree.baseAddressFormula.isEmpty())
addr = m_doc->tree.baseAddressFormula;
else
addr = QStringLiteral("0x") +
QString::number(m_doc->tree.baseAddress, 16).toUpper();
QString addr = QStringLiteral("0x") +
QString::number(m_doc->tree.baseAddress, 16).toUpper();
// Build the row. If we have a symbol, append it after the address.
QString row;
if (sym.isEmpty()) {
row = QStringLiteral("%1 \u00B7 %2")
.arg(elide(src, 40), elide(addr, 24));
} else {
row = QStringLiteral("%1 \u00B7 %2 %3")
.arg(elide(src, 40), elide(addr, 24), elide(sym, 40));
}
QString row = QStringLiteral("%1 \u00B7 %2")
.arg(elide(src, 40), elide(addr, 24));
// Build row 2: root class type + name (uses current view root)
QString row2;
@@ -2341,6 +2304,26 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
m_doc->dataPath.clear();
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
// Re-evaluate stored formula against the new provider
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
AddressParserCallbacks cbs;
auto* prov = m_doc->provider.get();
cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t {
uint64_t base = prov->symbolToAddress(name);
*ok = (base != 0);
return base;
};
cbs.readPointer = [prov](uint64_t addr, bool* ok) -> uint64_t {
uint64_t val = 0;
*ok = prov->read(addr, &val, 8);
return val;
};
auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, 8, &cbs);
if (result.ok)
m_doc->tree.baseAddress = result.value;
}
resetSnapshot();
emit m_doc->documentChanged();
refresh();
@@ -2351,8 +2334,10 @@ void RcxController::switchToSavedSource(int idx) {
if (idx == m_activeSourceIdx) return;
// Save current source's base address before switching
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size()) {
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
m_savedSources[m_activeSourceIdx].baseAddressFormula = m_doc->tree.baseAddressFormula;
}
m_activeSourceIdx = idx;
const auto& entry = m_savedSources[idx];
@@ -2360,9 +2345,12 @@ void RcxController::switchToSavedSource(int idx) {
if (entry.kind == QStringLiteral("File")) {
m_doc->loadData(entry.filePath);
m_doc->tree.baseAddress = entry.baseAddress;
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
refresh();
} else if (!entry.providerTarget.isEmpty()) {
// Plugin-based provider (e.g. "processmemory" with target "pid:name")
// Restore formula before attach so it can be re-evaluated against the new provider
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
attachViaPlugin(entry.kind, entry.providerTarget);
}
}

View File

@@ -70,6 +70,7 @@ struct SavedSourceEntry {
QString filePath; // for File sources
QString providerTarget; // for plugin providers (e.g. "pid:name")
uint64_t baseAddress = 0;
QString baseAddressFormula;
};
// ── Controller ──

View File

@@ -267,6 +267,7 @@ struct Node {
struct NodeTree {
QVector<Node> nodes;
uint64_t baseAddress = 0x00400000;
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
uint64_t m_nextId = 1;
mutable QHash<uint64_t, int> m_idCache;
@@ -400,6 +401,8 @@ struct NodeTree {
QJsonObject toJson() const {
QJsonObject o;
o["baseAddress"] = QString::number(baseAddress, 16);
if (!baseAddressFormula.isEmpty())
o["baseAddressFormula"] = baseAddressFormula;
o["nextId"] = QString::number(m_nextId);
QJsonArray arr;
for (const auto& n : nodes) arr.append(n.toJson());
@@ -410,6 +413,7 @@ struct NodeTree {
static NodeTree fromJson(const QJsonObject& o) {
NodeTree t;
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
t.baseAddressFormula = o["baseAddressFormula"].toString();
t.m_nextId = o["nextId"].toString("1").toULongLong();
QJsonArray arr = o["nodes"].toArray();
for (const auto& v : arr) {
@@ -541,7 +545,7 @@ namespace cmd {
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
struct Remove { uint64_t nodeId; QVector<Node> subtree;
QVector<OffsetAdj> offAdjs; };
struct ChangeBase { uint64_t oldBase, newBase; };
struct ChangeBase { uint64_t oldBase, newBase; QString oldFormula, newFormula; };
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
struct ChangeArrayMeta { uint64_t nodeId;
NodeKind oldElementKind, newElementKind;
@@ -663,8 +667,11 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
if (tag < 0) return {};
int start = tag + 3; // after " · "
int end = start;
while (end < lineText.size() && !lineText[end].isSpace()) end++;
// Scan to next " · " separator (or end of line) to support formulas with spaces
int nextSep = lineText.indexOf(QStringLiteral(" \u00B7"), start);
int end = (nextSep >= 0) ? nextSep : lineText.size();
// Trim trailing whitespace
while (end > start && lineText[end - 1].isSpace()) end--;
if (end <= start) return {};
return {start, end, true};
}

View File

@@ -1,4 +1,5 @@
#include "core.h"
#include "addressparser.h"
#include <cmath>
#include <cstring>
#include <limits>
@@ -664,43 +665,13 @@ QString validateValue(NodeKind kind, const QString& text) {
return QStringLiteral("invalid");
}
// ── Base address validation (supports simple +/- equations) ──
// ── Base address validation (delegates to AddressParser) ──
QString validateBaseAddress(const QString& text) {
QString s = text.trimmed();
if (s.isEmpty()) return QStringLiteral("empty");
int pos = 0;
bool firstTerm = true;
while (pos < s.size()) {
// Skip whitespace
while (pos < s.size() && s[pos].isSpace()) pos++;
if (pos >= s.size()) break;
// Check for +/- operator (except first term)
if (!firstTerm) {
if (s[pos] == '+' || s[pos] == '-') pos++;
else return QStringLiteral("invalid '%1'").arg(s[pos]);
while (pos < s.size() && s[pos].isSpace()) pos++;
}
// Skip 0x prefix if present
if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'))
pos += 2;
// Must have at least one hex digit
int numStart = pos;
while (pos < s.size() && (s[pos].isDigit() ||
(s[pos] >= 'a' && s[pos] <= 'f') ||
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
if (pos == numStart) return QStringLiteral("invalid");
firstTerm = false;
}
return {};
//s.remove('`');
return AddressParser::validate(s);
}
} // namespace rcx::fmt

View File

@@ -47,6 +47,13 @@ public:
return {};
}
// Resolve a module/symbol name to its address (reverse of getSymbol).
// Returns 0 if the name is not found.
virtual uint64_t symbolToAddress(const QString& name) const {
Q_UNUSED(name);
return 0;
}
// --- Derived convenience (non-virtual, never override) ---
bool isValid() const { return size() > 0; }

View File

@@ -67,6 +67,9 @@ public:
QString getSymbol(uint64_t addr) const override {
return m_real ? m_real->getSymbol(addr) : QString();
}
uint64_t symbolToAddress(const QString& n) const override {
return m_real ? m_real->symbolToAddress(n) : 0;
}
bool write(uint64_t addr, const void* buf, int len) override {
if (!m_real) return false;