mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add AddressParser + tests, remove symbol from commandrow
This commit is contained in:
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
300
src/addressparser.cpp
Normal 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
27
src/addressparser.h
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
13
src/core.h
13
src/core.h
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
219
tests/test_addressparser.cpp
Normal file
219
tests/test_addressparser.cpp
Normal 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"
|
||||
Reference in New Issue
Block a user