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

View File

@@ -73,6 +73,8 @@ add_executable(Reclass
src/titlebar.cpp
src/mcp/mcp_bridge.h
src/mcp/mcp_bridge.cpp
src/addressparser.h
src/addressparser.cpp
src/disasm.h
src/disasm.cpp
third_party/fadec/decode.c
@@ -154,17 +156,17 @@ if(BUILD_TESTING)
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp)
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_core PRIVATE src)
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_core COMMAND test_core)
add_executable(test_format tests/test_format.cpp src/format.cpp)
add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_format PRIVATE src)
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_format COMMAND test_format)
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp)
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_compose PRIVATE src)
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_compose COMMAND test_compose)
@@ -180,42 +182,47 @@ if(BUILD_TESTING)
add_test(NAME test_command_row COMMAND test_command_row)
add_executable(test_generator tests/test_generator.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_generator PRIVATE src)
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_generator COMMAND test_generator)
add_executable(test_import_xml tests/test_import_xml.cpp
src/import_reclass_xml.cpp src/format.cpp src/compose.cpp)
src/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_xml PRIVATE src)
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_import_xml COMMAND test_import_xml)
add_executable(test_import_source tests/test_import_source.cpp
src/import_source.cpp src/format.cpp src/compose.cpp)
src/import_source.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_source PRIVATE src)
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_import_source COMMAND test_import_source)
add_executable(test_export_xml tests/test_export_xml.cpp
src/export_reclass_xml.cpp src/import_reclass_xml.cpp src/format.cpp src/compose.cpp)
src/export_reclass_xml.cpp src/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_export_xml PRIVATE src)
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_export_xml COMMAND test_export_xml)
add_executable(test_disasm tests/test_disasm.cpp
src/disasm.cpp src/compose.cpp src/format.cpp
src/disasm.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
third_party/fadec/decode.c third_party/fadec/format.c)
target_include_directories(test_disasm PRIVATE src third_party/fadec)
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_disasm COMMAND test_disasm)
add_executable(test_addressparser tests/test_addressparser.cpp src/addressparser.cpp)
target_include_directories(test_addressparser PRIVATE src)
target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_addressparser COMMAND test_addressparser)
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
if(BUILD_UI_TESTS)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -229,7 +236,7 @@ if(BUILD_TESTING)
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -243,7 +250,7 @@ if(BUILD_TESTING)
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -257,7 +264,7 @@ if(BUILD_TESTING)
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_source_management tests/test_source_management.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -271,7 +278,7 @@ if(BUILD_TESTING)
add_test(NAME test_source_management COMMAND test_source_management)
add_executable(test_editor tests/test_editor.cpp
src/editor.cpp src/compose.cpp src/format.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_editor PRIVATE src third_party/fadec)
@@ -281,7 +288,7 @@ if(BUILD_TESTING)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_rendered_view tests/test_rendered_view.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_rendered_view PRIVATE src)
target_link_libraries(test_rendered_view PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
@@ -289,7 +296,7 @@ if(BUILD_TESTING)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -303,7 +310,7 @@ if(BUILD_TESTING)
add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
@@ -317,7 +324,7 @@ if(BUILD_TESTING)
add_test(NAME test_type_selector COMMAND test_type_selector)
add_executable(test_type_visibility tests/test_type_visibility.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})

View File

@@ -284,6 +284,15 @@ void ProcessMemoryProvider::cacheModules()
#endif // platform
uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
ProcessMemoryProvider::~ProcessMemoryProvider()
{
#ifdef _WIN32

View File

@@ -24,6 +24,7 @@ public:
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("LocalProcess"); }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& name) const override;
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }

View File

@@ -74,6 +74,15 @@ QString RcNetCompatProvider::getSymbol(uint64_t addr) const
return {};
}
uint64_t RcNetCompatProvider::symbolToAddress(const QString& name) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
// -- Module enumeration ---------------------------------------------------
namespace {

View File

@@ -28,6 +28,7 @@ public:
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& name) const override;
struct ModuleInfo {
QString name;

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;

View File

@@ -0,0 +1,219 @@
#include "addressparser.h"
#include <QTest>
using rcx::AddressParser;
using rcx::AddressParserCallbacks;
using rcx::AddressParseResult;
class TestAddressParser : public QObject {
Q_OBJECT
private slots:
// -- Hex literals --
void bareHex() { auto r = AddressParser::evaluate("AB"); QVERIFY(r.ok); QCOMPARE(r.value, 0xABULL); }
void prefixedHex() { auto r = AddressParser::evaluate("0x1F4"); QVERIFY(r.ok); QCOMPARE(r.value, 0x1F4ULL); }
void zeroLiteral() { auto r = AddressParser::evaluate("0"); QVERIFY(r.ok); QCOMPARE(r.value, 0ULL); }
void large64bit() { auto r = AddressParser::evaluate("7FF66CCE0000");QVERIFY(r.ok); QCOMPARE(r.value, 0x7FF66CCE0000ULL); }
// -- Arithmetic --
void addition() {
auto r = AddressParser::evaluate("0x100 + 0x200");
QVERIFY(r.ok); QCOMPARE(r.value, 0x300ULL);
}
void subtraction() {
auto r = AddressParser::evaluate("0x300 - 0x100");
QVERIFY(r.ok); QCOMPARE(r.value, 0x200ULL);
}
void multiplication() {
auto r = AddressParser::evaluate("0x10 * 4");
QVERIFY(r.ok); QCOMPARE(r.value, 0x40ULL);
}
void division() {
auto r = AddressParser::evaluate("0x100 / 2");
QVERIFY(r.ok); QCOMPARE(r.value, 0x80ULL);
}
void precedence() {
// 0x10 + 2*3 = 0x10 + 6 = 0x16
auto r = AddressParser::evaluate("0x10 + 2 * 3");
QVERIFY(r.ok); QCOMPARE(r.value, 0x16ULL);
}
void parentheses() {
// (0x10 + 2) * 3 = 0x12 * 3 = 0x36
auto r = AddressParser::evaluate("(0x10 + 2) * 3");
QVERIFY(r.ok); QCOMPARE(r.value, 0x36ULL);
}
// -- Unary minus --
void unaryMinus() {
auto r = AddressParser::evaluate("-0x10 + 0x20");
QVERIFY(r.ok); QCOMPARE(r.value, 0x10ULL);
}
// -- Module resolution --
void moduleResolve() {
AddressParserCallbacks cbs;
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
*ok = (name == "Program.exe");
return *ok ? 0x140000000ULL : 0;
};
auto r = AddressParser::evaluate("<Program.exe> + 0x123", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0x140000123ULL);
}
void moduleNotFound() {
AddressParserCallbacks cbs;
cbs.resolveModule = [](const QString&, bool* ok) -> uint64_t {
*ok = false;
return 0;
};
auto r = AddressParser::evaluate("<NoSuch.dll>", 8, &cbs);
QVERIFY(!r.ok);
QVERIFY(r.error.contains("not found"));
}
// -- Dereference --
void derefSimple() {
AddressParserCallbacks cbs;
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
*ok = (addr == 0x1000);
return *ok ? 0xDEADBEEFULL : 0;
};
auto r = AddressParser::evaluate("[0x1000]", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0xDEADBEEFULL);
}
void derefNested() {
AddressParserCallbacks cbs;
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
*ok = (name == "mod");
return *ok ? 0x400000ULL : 0;
};
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
*ok = true;
if (addr == 0x400100) return 0x500000;
if (addr == 0x900000) return 0xABCDEF;
return 0;
};
// [<mod> + [<mod> + 0x100]] = [0x400000 + [0x400000+0x100]]
// inner deref: [0x400100] = 0x500000
// outer: [0x400000 + 0x500000] = [0x900000] = 0xABCDEF
auto r = AddressParser::evaluate("[<mod> + [<mod> + 0x100]]", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0xABCDEFULL);
}
void derefReadFailure() {
AddressParserCallbacks cbs;
cbs.readPointer = [](uint64_t, bool* ok) -> uint64_t {
*ok = false;
return 0;
};
auto r = AddressParser::evaluate("[0x1000]", 8, &cbs);
QVERIFY(!r.ok);
QVERIFY(r.error.contains("failed to read"));
}
// -- Complex expression from plan --
void complexExpr() {
AddressParserCallbacks cbs;
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
*ok = (name == "Program.exe");
return *ok ? 0x140000000ULL : 0;
};
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
*ok = true;
if (addr == 0x1400000DEULL) return 0x500000;
return 0;
};
// [<Program.exe> + 0xDE] - AB = [0x1400000DE] - 0xAB = 0x500000 - 0xAB = 0x4FFF55
auto r = AddressParser::evaluate("[<Program.exe> + 0xDE] - AB", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0x4FFF55ULL);
}
// -- Errors --
void emptyInput() {
auto r = AddressParser::evaluate("");
QVERIFY(!r.ok);
}
void unmatchedBracket() {
auto r = AddressParser::evaluate("[0x1000");
QVERIFY(!r.ok);
QVERIFY(r.error.contains("']'"));
}
void unmatchedAngle() {
auto r = AddressParser::evaluate("<Program.exe");
QVERIFY(!r.ok);
QVERIFY(r.error.contains("'>'"));
}
void divisionByZero() {
auto r = AddressParser::evaluate("0x100 / 0");
QVERIFY(!r.ok);
QVERIFY(r.error.contains("division by zero"));
}
void trailingGarbage() {
auto r = AddressParser::evaluate("0x100 xyz");
QVERIFY(!r.ok);
QVERIFY(r.error.contains("unexpected"));
}
void trailingOperator() {
auto r = AddressParser::evaluate("0x100 +");
QVERIFY(!r.ok);
}
// -- Validation --
void validateValid() {
QCOMPARE(AddressParser::validate("0x100 + 0x200"), QString());
QCOMPARE(AddressParser::validate("<Prog.exe> + [0x100]"), QString());
}
void validateInvalid() {
QVERIFY(!AddressParser::validate("").isEmpty());
QVERIFY(!AddressParser::validate("[0x100").isEmpty());
QVERIFY(!AddressParser::validate("0x100 xyz").isEmpty());
}
// -- Backtick stripping --
void backtickStripping() {
auto r = AddressParser::evaluate("7ff6`6cce0000");
QVERIFY(r.ok);
QCOMPARE(r.value, 0x7FF66CCE0000ULL);
}
// -- Whitespace tolerance --
void whitespace() {
auto r = AddressParser::evaluate(" 0x100 + 0x200 ");
QVERIFY(r.ok);
QCOMPARE(r.value, 0x300ULL);
}
// -- Legacy compat: simple hex --
void simpleHexAddress() {
auto r = AddressParser::evaluate("140000000");
QVERIFY(r.ok);
QCOMPARE(r.value, 0x140000000ULL);
}
// -- Multiple additions --
void multipleAdditions() {
auto r = AddressParser::evaluate("0x100 + 0x200 + 0x300");
QVERIFY(r.ok);
QCOMPARE(r.value, 0x600ULL);
}
};
QTEST_GUILESS_MAIN(TestAddressParser)
#include "test_addressparser.moc"