#include #include #include #include #include #include "scanner.h" #include "providers/provider.h" #include "providers/buffer_provider.h" #include "providers/null_provider.h" using namespace rcx; // ── Test provider that exposes custom memory regions ── class RegionProvider : public BufferProvider { QVector m_regions; public: RegionProvider(QByteArray data, QVector regions) : BufferProvider(std::move(data), "test") , m_regions(std::move(regions)) {} QVector enumerateRegions() const override { return m_regions; } }; class TestScanner : public QObject { Q_OBJECT private slots: // ═══════════════════════════════════════════════════════════════════ // Pattern Parsing — Signature mode // ═══════════════════════════════════════════════════════════════════ void parse_emptyPattern() { QByteArray pat, mask; QString err; QVERIFY(!parseSignature("", pat, mask, &err)); QVERIFY(err.contains("Empty")); } void parse_spacesOnly() { QByteArray pat, mask; QString err; QVERIFY(!parseSignature(" ", pat, mask, &err)); QVERIFY(err.contains("Empty")); } void parse_singleByte() { QByteArray pat, mask; QVERIFY(parseSignature("AB", pat, mask)); QCOMPARE(pat.size(), 1); QCOMPARE((uint8_t)pat[0], (uint8_t)0xAB); QCOMPARE((uint8_t)mask[0], (uint8_t)0xFF); } void parse_spaceSeparated() { QByteArray pat, mask; QVERIFY(parseSignature("48 8B 05", pat, mask)); QCOMPARE(pat.size(), 3); QCOMPARE((uint8_t)pat[0], (uint8_t)0x48); QCOMPARE((uint8_t)pat[1], (uint8_t)0x8B); QCOMPARE((uint8_t)pat[2], (uint8_t)0x05); QCOMPARE((uint8_t)mask[0], (uint8_t)0xFF); QCOMPARE((uint8_t)mask[1], (uint8_t)0xFF); QCOMPARE((uint8_t)mask[2], (uint8_t)0xFF); } void parse_withWildcards() { QByteArray pat, mask; QVERIFY(parseSignature("48 ?? 05 ?? ??", pat, mask)); QCOMPARE(pat.size(), 5); QCOMPARE((uint8_t)pat[0], (uint8_t)0x48); QCOMPARE((uint8_t)pat[2], (uint8_t)0x05); QCOMPARE((uint8_t)mask[0], (uint8_t)0xFF); QCOMPARE((uint8_t)mask[1], (uint8_t)0x00); // wildcard QCOMPARE((uint8_t)mask[2], (uint8_t)0xFF); QCOMPARE((uint8_t)mask[3], (uint8_t)0x00); QCOMPARE((uint8_t)mask[4], (uint8_t)0x00); } void parse_singleQuestionMark() { QByteArray pat, mask; QVERIFY(parseSignature("48 ? 05", pat, mask)); QCOMPARE((uint8_t)mask[1], (uint8_t)0x00); } void parse_packedNoSpaces() { QByteArray pat, mask; QVERIFY(parseSignature("488B??05CC", pat, mask)); QCOMPARE(pat.size(), 5); QCOMPARE((uint8_t)pat[0], (uint8_t)0x48); QCOMPARE((uint8_t)pat[1], (uint8_t)0x8B); QCOMPARE((uint8_t)mask[2], (uint8_t)0x00); QCOMPARE((uint8_t)pat[3], (uint8_t)0x05); QCOMPARE((uint8_t)pat[4], (uint8_t)0xCC); } void parse_cStyle() { QByteArray pat, mask; QVERIFY(parseSignature("\\x48\\x8B\\x05", pat, mask)); QCOMPARE(pat.size(), 3); QCOMPARE((uint8_t)pat[0], (uint8_t)0x48); QCOMPARE((uint8_t)pat[1], (uint8_t)0x8B); QCOMPARE((uint8_t)pat[2], (uint8_t)0x05); } void parse_lowercaseHex() { QByteArray pat, mask; QVERIFY(parseSignature("ab cd ef", pat, mask)); QCOMPARE((uint8_t)pat[0], (uint8_t)0xAB); QCOMPARE((uint8_t)pat[1], (uint8_t)0xCD); QCOMPARE((uint8_t)pat[2], (uint8_t)0xEF); } void parse_mixedCase() { QByteArray pat, mask; QVERIFY(parseSignature("aB Cd eF", pat, mask)); QCOMPARE((uint8_t)pat[0], (uint8_t)0xAB); QCOMPARE((uint8_t)pat[1], (uint8_t)0xCD); QCOMPARE((uint8_t)pat[2], (uint8_t)0xEF); } void parse_invalidHex() { QByteArray pat, mask; QString err; QVERIFY(!parseSignature("GG", pat, mask, &err)); QVERIFY(!err.isEmpty()); } void parse_oddCharsNoSpaces() { QByteArray pat, mask; QString err; QVERIFY(!parseSignature("ABC", pat, mask, &err)); QVERIFY(err.contains("Odd")); } void parse_invalidTokenWidth() { QByteArray pat, mask; QString err; QVERIFY(!parseSignature("48 ABC 05", pat, mask, &err)); QVERIFY(!err.isEmpty()); } void parse_leadingTrailingSpaces() { QByteArray pat, mask; QVERIFY(parseSignature(" 48 8B ", pat, mask)); QCOMPARE(pat.size(), 2); } void parse_allWildcards() { QByteArray pat, mask; QVERIFY(parseSignature("?? ?? ??", pat, mask)); QCOMPARE(pat.size(), 3); for (int i = 0; i < 3; i++) QCOMPARE((uint8_t)mask[i], (uint8_t)0x00); } // ═══════════════════════════════════════════════════════════════════ // Value Serialization // ═══════════════════════════════════════════════════════════════════ void serialize_int8() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int8, "-42", pat, mask)); QCOMPARE(pat.size(), 1); QCOMPARE((int8_t)pat[0], (int8_t)-42); QCOMPARE((uint8_t)mask[0], (uint8_t)0xFF); } void serialize_int8_overflow() { QByteArray pat, mask; QString err; QVERIFY(!serializeValue(ValueType::Int8, "200", pat, mask, &err)); } void serialize_int16() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int16, "-1000", pat, mask)); QCOMPARE(pat.size(), 2); int16_t v; std::memcpy(&v, pat.constData(), 2); QCOMPARE(v, (int16_t)-1000); } void serialize_int32() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int32, "12345", pat, mask)); QCOMPARE(pat.size(), 4); int32_t v; std::memcpy(&v, pat.constData(), 4); QCOMPARE(v, 12345); } void serialize_int32_negative() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int32, "-1", pat, mask)); int32_t v; std::memcpy(&v, pat.constData(), 4); QCOMPARE(v, -1); } void serialize_int64() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int64, "9999999999", pat, mask)); QCOMPARE(pat.size(), 8); int64_t v; std::memcpy(&v, pat.constData(), 8); QCOMPARE(v, (int64_t)9999999999LL); } void serialize_uint8() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UInt8, "255", pat, mask)); QCOMPARE(pat.size(), 1); QCOMPARE((uint8_t)pat[0], (uint8_t)255); } void serialize_uint8_hex() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UInt8, "0xFF", pat, mask)); QCOMPARE((uint8_t)pat[0], (uint8_t)0xFF); } void serialize_uint16() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UInt16, "1234", pat, mask)); QCOMPARE(pat.size(), 2); uint16_t v; std::memcpy(&v, pat.constData(), 2); QCOMPARE(v, (uint16_t)1234); } void serialize_uint32() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UInt32, "0xDEADBEEF", pat, mask)); QCOMPARE(pat.size(), 4); uint32_t v; std::memcpy(&v, pat.constData(), 4); QCOMPARE(v, (uint32_t)0xDEADBEEF); } void serialize_uint64() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UInt64, "0xCAFEBABEDEADBEEF", pat, mask)); QCOMPARE(pat.size(), 8); uint64_t v; std::memcpy(&v, pat.constData(), 8); QCOMPARE(v, (uint64_t)0xCAFEBABEDEADBEEFULL); } void serialize_float() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Float, "3.14", pat, mask)); QCOMPARE(pat.size(), 4); float v; std::memcpy(&v, pat.constData(), 4); QCOMPARE(v, 3.14f); } void serialize_double() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Double, "2.71828", pat, mask)); QCOMPARE(pat.size(), 8); double v; std::memcpy(&v, pat.constData(), 8); QCOMPARE(v, 2.71828); } void serialize_vec2() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Vec2, "1.0 2.0", pat, mask)); QCOMPARE(pat.size(), 8); float v[2]; std::memcpy(v, pat.constData(), 8); QCOMPARE(v[0], 1.0f); QCOMPARE(v[1], 2.0f); } void serialize_vec3() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Vec3, "1.0 0.0 0.0", pat, mask)); QCOMPARE(pat.size(), 12); float v[3]; std::memcpy(v, pat.constData(), 12); QCOMPARE(v[0], 1.0f); QCOMPARE(v[1], 0.0f); QCOMPARE(v[2], 0.0f); } void serialize_vec3_wrongCount() { QByteArray pat, mask; QString err; QVERIFY(!serializeValue(ValueType::Vec3, "1.0 2.0", pat, mask, &err)); QVERIFY(err.contains("3")); } void serialize_vec4() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Vec4, "1.0 2.0 3.0 4.0", pat, mask)); QCOMPARE(pat.size(), 16); float v[4]; std::memcpy(v, pat.constData(), 16); QCOMPARE(v[0], 1.0f); QCOMPARE(v[1], 2.0f); QCOMPARE(v[2], 3.0f); QCOMPARE(v[3], 4.0f); } void serialize_utf8() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UTF8, "Hello", pat, mask)); QCOMPARE(pat, QByteArray("Hello")); } void serialize_utf16() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UTF16, "Hi", pat, mask)); QCOMPARE(pat.size(), 4); // 2 chars * 2 bytes uint16_t v[2]; std::memcpy(v, pat.constData(), 4); QCOMPARE(v[0], (uint16_t)'H'); QCOMPARE(v[1], (uint16_t)'i'); } void serialize_hexBytes() { QByteArray pat, mask; QVERIFY(serializeValue(ValueType::HexBytes, "DE AD BE EF", pat, mask)); QCOMPARE(pat.size(), 4); QCOMPARE((uint8_t)pat[0], (uint8_t)0xDE); QCOMPARE((uint8_t)pat[1], (uint8_t)0xAD); QCOMPARE((uint8_t)pat[2], (uint8_t)0xBE); QCOMPARE((uint8_t)pat[3], (uint8_t)0xEF); } void serialize_emptyValue() { QByteArray pat, mask; QString err; QVERIFY(!serializeValue(ValueType::Int32, "", pat, mask, &err)); QVERIFY(err.contains("Empty")); } void serialize_invalidInt() { QByteArray pat, mask; QString err; QVERIFY(!serializeValue(ValueType::Int32, "notanumber", pat, mask, &err)); } void serialize_invalidFloat() { QByteArray pat, mask; QString err; QVERIFY(!serializeValue(ValueType::Float, "abc", pat, mask, &err)); } // ═══════════════════════════════════════════════════════════════════ // Natural Alignment // ═══════════════════════════════════════════════════════════════════ void alignment_int8() { QCOMPARE(naturalAlignment(ValueType::Int8), 1); } void alignment_int16() { QCOMPARE(naturalAlignment(ValueType::Int16), 2); } void alignment_int32() { QCOMPARE(naturalAlignment(ValueType::Int32), 4); } void alignment_int64() { QCOMPARE(naturalAlignment(ValueType::Int64), 8); } void alignment_float() { QCOMPARE(naturalAlignment(ValueType::Float), 4); } void alignment_double(){ QCOMPARE(naturalAlignment(ValueType::Double),8); } void alignment_vec3() { QCOMPARE(naturalAlignment(ValueType::Vec3), 4); } void alignment_utf8() { QCOMPARE(naturalAlignment(ValueType::UTF8), 1); } void alignment_utf16() { QCOMPARE(naturalAlignment(ValueType::UTF16), 2); } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — Basic functionality // ═══════════════════════════════════════════════════════════════════ void scan_exactMatch() { // Buffer: 00 11 22 33 44 55 66 77 QByteArray data(8, '\0'); data[2] = 0x22; data[3] = 0x33; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\x22\x33", 2); req.mask = QByteArray("\xFF\xFF", 2); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)2); } void scan_wildcardMatch() { // Buffer with known pattern QByteArray data(16, '\0'); data[0] = 0x48; data[1] = 0x8B; data[2] = 0xAA; data[3] = 0x05; data[8] = 0x48; data[9] = 0x8B; data[10] = 0xBB; data[11] = 0x05; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; // Pattern: 48 8B ?? 05 req.pattern = QByteArray("\x48\x8B\x00\x05", 4); req.mask = QByteArray("\xFF\xFF\x00\xFF", 4); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)0); QCOMPARE(results[1].address, (uint64_t)8); } void scan_noMatch() { QByteArray data(32, '\0'); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xFF\xFF", 2); req.mask = QByteArray("\xFF\xFF", 2); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } void scan_alignment4() { // Put pattern at offset 2 (not 4-aligned) and offset 4 (4-aligned) QByteArray data(16, '\0'); data[2] = 0xAA; data[3] = 0xBB; data[4] = 0xAA; data[5] = 0xBB; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA\xBB", 2); req.mask = QByteArray("\xFF\xFF", 2); req.alignment = 4; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)4); // only 4-aligned match } void scan_maxResults() { // Fill buffer with pattern every byte QByteArray data(1000, '\xAA'); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA", 1); req.mask = QByteArray("\xFF", 1); req.maxResults = 10; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 10); // capped at maxResults } void scan_emptyProvider() { auto prov = std::make_shared(); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } void scan_emptyPattern() { auto prov = std::make_shared(QByteArray(16, '\0')); ScanEngine engine; QSignalSpy errSpy(&engine, &ScanEngine::error); ScanRequest req; // Empty pattern — error emitted synchronously engine.start(prov, req); QCOMPARE(errSpy.size(), 1); } void scan_chunkBoundaryOverlap() { // Create a buffer where the pattern straddles a chunk boundary // Use a small-ish buffer and simulate by creating a pattern // that sits at a position that would be at the overlap zone const int kChunkSize = 256 * 1024; QByteArray data(kChunkSize + 16, '\0'); // Place pattern right at chunk boundary int pos = kChunkSize - 2; // pattern starts 2 bytes before boundary data[pos] = 0xDE; data[pos + 1] = 0xAD; data[pos + 2] = 0xBE; data[pos + 3] = 0xEF; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xDE\xAD\xBE\xEF", 4); req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4); engine.start(prov, req); QVERIFY(finSpy.wait(10000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)pos); } void scan_multipleMatches() { QByteArray data(64, '\0'); // Place pattern at offsets 0, 16, 32, 48 for (int i = 0; i < 4; i++) { data[i * 16] = 0xCA; data[i * 16 + 1] = 0xFE; } auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xCA\xFE", 2); req.mask = QByteArray("\xFF\xFF", 2); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 4); for (int i = 0; i < 4; i++) QCOMPARE(results[i].address, (uint64_t)(i * 16)); } void scan_singleBytePattern() { QByteArray data(8, '\0'); data[5] = 0x42; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\x42", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)5); } void scan_patternLargerThanData() { QByteArray data(4, '\xAA'); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(8, '\xAA'); req.mask = QByteArray(8, '\xFF'); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } void scan_patternExactSize() { QByteArray data("\xDE\xAD\xBE\xEF", 4); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xDE\xAD\xBE\xEF", 4); req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)0); } void scan_atEndOfBuffer() { QByteArray data(32, '\0'); data[30] = 0xAB; data[31] = 0xCD; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAB\xCD", 2); req.mask = QByteArray("\xFF\xFF", 2); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)30); } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — Region filtering // ═══════════════════════════════════════════════════════════════════ void scan_filterExecutable() { QByteArray data(32, '\0'); data[0] = 0xAA; // in region 0 (not executable) data[16] = 0xAA; // in region 1 (executable) QVector regions; regions.append({0, 16, true, true, false, "heap"}); regions.append({16, 16, true, false, true, "code"}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA", 1); req.mask = QByteArray("\xFF", 1); req.filterExecutable = true; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)16); QCOMPARE(results[0].regionModule, QStringLiteral("code")); } void scan_filterWritable() { QByteArray data(32, '\0'); data[0] = 0xBB; // region 0 (writable) data[16] = 0xBB; // region 1 (not writable) QVector regions; regions.append({0, 16, true, true, false, "data"}); regions.append({16, 16, true, false, true, "code"}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xBB", 1); req.mask = QByteArray("\xFF", 1); req.filterWritable = true; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)0); } void scan_bothFilters() { QByteArray data(48, '\0'); data[0] = 0xCC; // region 0: +w -x data[16] = 0xCC; // region 1: -w +x data[32] = 0xCC; // region 2: +w +x QVector regions; regions.append({0, 16, true, true, false, "data"}); regions.append({16, 16, true, false, true, "code"}); regions.append({32, 16, true, true, true, "rwx"}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xCC", 1); req.mask = QByteArray("\xFF", 1); req.filterExecutable = true; req.filterWritable = true; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); // Both filters: region must be BOTH executable AND writable QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)32); } void scan_regionModuleName() { QByteArray data(16, '\0'); data[0] = 0xDD; QVector regions; regions.append({0, 16, true, true, true, "Game.exe"}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xDD", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].regionModule, QStringLiteral("Game.exe")); } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — Abort // ═══════════════════════════════════════════════════════════════════ void scan_abort() { // Large buffer to ensure scan takes measurable time QByteArray data(1024 * 1024, '\0'); // 1MB auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xFF", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); QVERIFY(engine.isRunning()); engine.abort(); QVERIFY(finSpy.wait(5000)); // Should complete (possibly with 0 results since buffer is all zeros anyway) QCOMPARE(finSpy.size(), 1); } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — Progress signal // ═══════════════════════════════════════════════════════════════════ void scan_progressEmitted() { QByteArray data(512 * 1024, '\0'); // 512KB auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); QSignalSpy progSpy(&engine, &ScanEngine::progress); ScanRequest req; req.pattern = QByteArray("\xFF", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); // Should have at least one progress signal QVERIFY(progSpy.size() > 0); // Last progress should be near 100 int lastPct = progSpy.last().first().toInt(); QVERIFY(lastPct >= 50); // at least past halfway } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — isRunning // ═══════════════════════════════════════════════════════════════════ void scan_isRunning() { ScanEngine engine; QVERIFY(!engine.isRunning()); QByteArray data(256 * 1024, '\0'); auto prov = std::make_shared(data); QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xFF", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); // May or may not be running depending on thread scheduling // Just verify it completes QVERIFY(finSpy.wait(5000)); QVERIFY(!engine.isRunning()); } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — Value scan integration // ═══════════════════════════════════════════════════════════════════ void scan_findInt32Value() { QByteArray data(64, '\0'); int32_t target = 12345; std::memcpy(data.data() + 20, &target, 4); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int32, "12345", pat, mask)); ScanRequest req; req.pattern = pat; req.mask = mask; req.alignment = 4; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)20); } void scan_findFloatValue() { QByteArray data(64, '\0'); float target = 3.14f; std::memcpy(data.data() + 8, &target, 4); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Float, "3.14", pat, mask)); ScanRequest req; req.pattern = pat; req.mask = mask; req.alignment = 4; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)8); } void scan_findUtf16String() { QByteArray data(128, '\0'); // Write "Hi" in UTF-16LE at offset 32 uint16_t chars[] = { 'H', 'i' }; std::memcpy(data.data() + 32, chars, 4); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UTF16, "Hi", pat, mask)); ScanRequest req; req.pattern = pat; req.mask = mask; req.alignment = 2; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)32); } void scan_findVec3() { QByteArray data(64, '\0'); float v[] = { 1.0f, 0.0f, 0.0f }; std::memcpy(data.data() + 12, v, 12); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Vec3, "1.0 0.0 0.0", pat, mask)); ScanRequest req; req.pattern = pat; req.mask = mask; req.alignment = 4; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)12); } // ═══════════════════════════════════════════════════════════════════ // Provider — enumerateRegions default // ═══════════════════════════════════════════════════════════════════ void provider_defaultRegionsEmpty() { BufferProvider p(QByteArray(16, '\0')); auto regions = p.enumerateRegions(); QVERIFY(regions.isEmpty()); } void provider_nullProviderRegionsEmpty() { NullProvider p; auto regions = p.enumerateRegions(); QVERIFY(regions.isEmpty()); } void provider_customRegions() { QVector regs; regs.append({0x1000, 0x2000, true, true, false, "heap"}); regs.append({0x3000, 0x1000, true, false, true, "code"}); RegionProvider p(QByteArray(0x4000, '\0'), regs); auto result = p.enumerateRegions(); QCOMPARE(result.size(), 2); QCOMPARE(result[0].base, (uint64_t)0x1000); QCOMPARE(result[0].moduleName, QStringLiteral("heap")); QCOMPARE(result[1].executable, true); } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — Mask/pattern size mismatch // ═══════════════════════════════════════════════════════════════════ void scan_maskSizeMismatch() { auto prov = std::make_shared(QByteArray(16, '\0')); ScanEngine engine; QSignalSpy errSpy(&engine, &ScanEngine::error); ScanRequest req; req.pattern = QByteArray(4, '\x00'); req.mask = QByteArray(2, '\xFF'); // mismatch! engine.start(prov, req); QCOMPARE(errSpy.size(), 1); } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — Multiple regions, pattern found in each // ═══════════════════════════════════════════════════════════════════ void scan_multipleRegions() { QByteArray data(48, '\0'); data[4] = 0xEE; // region 0 data[20] = 0xEE; // region 1 data[36] = 0xEE; // region 2 QVector regions; regions.append({0, 16, true, true, false, "region0"}); regions.append({16, 16, true, true, false, "region1"}); regions.append({32, 16, true, true, false, "region2"}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xEE", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 3); QCOMPARE(results[0].regionModule, QStringLiteral("region0")); QCOMPARE(results[1].regionModule, QStringLiteral("region1")); QCOMPARE(results[2].regionModule, QStringLiteral("region2")); } // ═══════════════════════════════════════════════════════════════════ // Scan Engine — Overlapping matches // ═══════════════════════════════════════════════════════════════════ void scan_overlappingMatches() { // Pattern AA AA — in buffer AA AA AA should find matches at 0 and 1 QByteArray data(4, '\0'); data[0] = 0xAA; data[1] = 0xAA; data[2] = 0xAA; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA\xAA", 2); req.mask = QByteArray("\xFF\xFF", 2); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)0); QCOMPARE(results[1].address, (uint64_t)1); } // ═══════════════════════════════════════════════════════════════════ // Edge case: scan 1-byte buffer // ═══════════════════════════════════════════════════════════════════ void scan_oneByteBuffer() { QByteArray data(1, '\xAB'); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAB", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)0); } // ═══════════════════════════════════════════════════════════════════ // Edge case: all-wildcard pattern matches everywhere // ═══════════════════════════════════════════════════════════════════ void scan_allWildcardPattern() { QByteArray data(8, '\x42'); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(2, '\x00'); req.mask = QByteArray(2, '\x00'); // all wildcards engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 7); // 8 - 2 + 1 = 7 positions } // ═══════════════════════════════════════════════════════════════════ // Address range filtering — "Current Struct" support // ═══════════════════════════════════════════════════════════════════ void scan_addressRangeNoLimit() { // startAddress=0, endAddress=0 → scan all (default behavior unchanged) QByteArray data(32, '\x00'); data[8] = '\xAA'; data[16] = '\xAA'; data[24] = '\xAA'; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA", 1); req.mask = QByteArray("\xFF", 1); engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 3); // all 3 found } void scan_addressRangeClipsResults() { // Only scan addresses [8, 20) — should find match at offset 8 and 16 but not 24 QByteArray data(32, '\x00'); data[8] = '\xAA'; data[16] = '\xAA'; data[24] = '\xAA'; auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA", 1); req.mask = QByteArray("\xFF", 1); req.startAddress = 8; req.endAddress = 20; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)8); QCOMPARE(results[1].address, (uint64_t)16); } void scan_addressRangeOutsideData() { // Range entirely outside data → no results QByteArray data(16, '\xAA'); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA", 1); req.mask = QByteArray("\xFF", 1); req.startAddress = 100; req.endAddress = 200; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } void scan_addressRangeWithRegions() { // Two regions: [1000, 1016) and [2000, 2016). Range [1000, 1020) clips to first region only. QByteArray data(4096, '\x00'); // Place \xBB at offset 1000 and 2000 data[1000] = '\xBB'; data[2000] = '\xBB'; QVector regions; { MemoryRegion r; r.base = 1000; r.size = 16; r.readable = true; r.writable = true; regions.append(r); } { MemoryRegion r; r.base = 2000; r.size = 16; r.readable = true; r.writable = true; regions.append(r); } auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xBB", 1); req.mask = QByteArray("\xFF", 1); req.startAddress = 1000; req.endAddress = 1020; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)1000); } void scan_unknownWithAddressRange() { // Unknown scan with address range should only capture within range QByteArray data(32, '\x42'); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.condition = ScanCondition::UnknownValue; req.valueSize = 4; req.alignment = 4; req.startAddress = 8; req.endAddress = 24; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); // Range [8, 24) = 16 bytes, alignment 4, valueSize 4 → offsets 8, 12, 16, 20 = 4 results QCOMPARE(results.size(), 4); QCOMPARE(results[0].address, (uint64_t)8); QCOMPARE(results[3].address, (uint64_t)20); } // -- constrainRegions (multi-range intersection) -- void scan_constrainRegions_multipleRanges() { QByteArray data(32, 0); data[4] = char(0xBB); data[12] = char(0xBB); data[20] = char(0xBB); data[28] = char(0xBB); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xBB", 1); req.mask = QByteArray("\xFF", 1); req.constrainRegions = {{0, 8}, {16, 24}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)4); QCOMPARE(results[1].address, (uint64_t)20); } void scan_constrainRegions_intersectsProviderRegions() { QByteArray data(256, 0); data[160] = char(0xCC); data[210] = char(0xCC); QVector regions; regions.append({100, 100, true, false, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xCC", 1); req.mask = QByteArray("\xFF", 1); req.constrainRegions = {{150, 250}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)160); } void scan_constrainRegions_noOverlap() { QByteArray data(32, char(0xEE)); QVector regions; regions.append({0, 16, true, false, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xEE", 1); req.mask = QByteArray("\xFF", 1); req.constrainRegions = {{100, 200}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } // -- constrainRegions edge cases -- void scan_constrainRegions_gapBetweenRegions() { // Provider has two regions with a gap: [0,16) and [32,48). // Constraint spans the gap: [8, 40). Should find matches in both. QByteArray data(64, 0); data[10] = char(0xDD); data[35] = char(0xDD); QVector regions; regions.append({0, 16, true, true, false, {}}); regions.append({32, 16, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xDD)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{8, 40}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)10); QCOMPARE(results[1].address, (uint64_t)35); } void scan_constrainRegions_partialRegionOverlap() { // Provider region [100, 200). Constraint [150, 250) clips to [150, 200). QByteArray data(256, 0); data[120] = char(0xAB); data[160] = char(0xAB); QVector regions; regions.append({100, 100, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xAB)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{150, 250}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)160); } void scan_constrainRegions_mixedModuleAndAnonymous() { // Module region + anonymous heap region. Constraint covers both. QByteArray data(0x10000, 0); data[0x1500] = char(0xCC); data[0x5500] = char(0xCC); QVector regions; regions.append({0x1000, 0x1000, true, false, true, QString("game.exe")}); regions.append({0x5000, 0x1000, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xCC)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{0x0, 0x10000}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)0x1500); QCOMPARE(results[1].address, (uint64_t)0x5500); } void scan_constrainRegions_fallbackProvider() { // BufferProvider returns no regions -> fallback [0, size). // constrainRegions should still work against the fallback. QByteArray data(64, 0); data[10] = char(0xAA); data[30] = char(0xAA); data[50] = char(0xAA); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xAA)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{5, 35}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)10); QCOMPARE(results[1].address, (uint64_t)30); } void scan_constrainRegions_adjacentRegions() { // Two adjacent regions [0,16) and [16,32). Constraint [8,24) spans both. QByteArray data(32, 0); data[12] = char(0xEF); data[20] = char(0xEF); QVector regions; regions.append({0, 16, true, true, false, {}}); regions.append({16, 16, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xEF)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{8, 24}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)12); QCOMPARE(results[1].address, (uint64_t)20); } void scan_constrainRegions_writableFilterPreserved() { // filterWritable=true should still exclude non-writable clipped regions. QByteArray data(0x4000, 0); data[0x1100] = char(0xBB); data[0x2100] = char(0xBB); QVector regions; regions.append({0x1000, 0x1000, true, false, true, {}}); regions.append({0x2000, 0x1000, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xBB)); req.mask = QByteArray(1, char(0xFF)); req.filterWritable = true; req.constrainRegions = {{0x1000, 0x3000}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)0x2100); } void scan_constrainRegions_constraintExtendsBeforeAndAfter() { // Region [10, 20). Constraint [0, 30) extends before and after. // Should only scan [10, 20) — the intersection. QByteArray data(32, 0); data[5] = char(0xAA); // outside region, should NOT be found data[15] = char(0xAA); // inside region, should be found data[25] = char(0xAA); // outside region, should NOT be found QVector regions; regions.append({10, 10, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xAA)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{0, 30}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)15); } void scan_constrainRegions_emptyConstraintScansAll() { // Empty constrainRegions should scan everything (no restriction). QByteArray data(32, 0); data[5] = char(0xBB); data[15] = char(0xBB); QVector regions; regions.append({0, 32, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xBB)); req.mask = QByteArray(1, char(0xFF)); // constrainRegions left empty engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); } void scan_constrainRegions_singleAddressRange() { // Equivalent to startAddress/endAddress: single constraint range. QByteArray data(32, 0); data[8] = char(0xAA); data[16] = char(0xAA); data[24] = char(0xAA); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xAA)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{8, 20}}; // same as startAddress=8, endAddress=20 engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)8); QCOMPARE(results[1].address, (uint64_t)16); } void scan_constrainRegions_withStartEndAddress() { // Both constrainRegions and startAddress/endAddress set. // constrainRegions: [0, 16) and [24, 32). startAddress/endAddress: [8, 28). // Effective scan should be intersection of both: [8, 16) and [24, 28). // Match at 4 (outside both), 12 (in both), 20 (in startEnd but not constrain), // 26 (in both), 30 (in constrain but not startEnd). QByteArray data(32, 0); data[4] = char(0xDD); data[12] = char(0xDD); data[20] = char(0xDD); data[26] = char(0xDD); data[30] = char(0xDD); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xDD)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{0, 16}, {24, 32}}; req.startAddress = 8; req.endAddress = 28; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); // only 12 and 26 QCOMPARE(results[0].address, (uint64_t)12); QCOMPARE(results[1].address, (uint64_t)26); } void scan_constrainRegions_unknownValueScan() { // Unknown value scan with constrainRegions should only capture within ranges. QByteArray data(32, char(0x42)); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.condition = ScanCondition::UnknownValue; req.valueSize = 4; req.alignment = 4; req.constrainRegions = {{8, 24}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); // Range [8, 24) = 16 bytes, alignment 4, valueSize 4 -> offsets 8, 12, 16, 20 = 4 results QCOMPARE(results.size(), 4); QCOMPARE(results[0].address, (uint64_t)8); QCOMPARE(results[3].address, (uint64_t)20); } void scan_constrainRegions_nonZeroBase() { // Region with non-zero base; constraint matches exactly. QByteArray data(0x10000, 0); data[0x8100] = char(0xFF); QVector regions; regions.append({0x8000, 0x1000, true, true, false, {}}); auto prov = std::make_shared(data2, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xFF)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{0x8000, 0x9000}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)0x8100); } void scan_constrainRegions_zeroSizeConstraint() { // Degenerate: constraint with start == end (zero size). Should scan nothing. QByteArray data(32, char(0xAA)); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xAA)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{10, 10}}; // zero-size engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } void scan_constrainRegions_invertedRange() { // Degenerate: constraint with start > end. Should be treated as empty/invalid. QByteArray data(32, char(0xAA)); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xAA)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{20, 10}}; // inverted engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } void scan_constrainRegions_overlappingConstraints() { // Two overlapping constraints: [4, 20) and [12, 28). // Should NOT double-count matches in the overlap [12, 20). QByteArray data(32, 0); data[8] = char(0xCC); data[16] = char(0xCC); data[24] = char(0xCC); auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xCC)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{4, 20}, {12, 28}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); // After merge: [4, 28). All three matches are in range, no duplicates. QCOMPARE(results.size(), 3); } void scan_constrainRegions_patternAtFirstByte() { // Pattern at the very first byte of a clipped sub-region. // Region [0, 64). Constraint [20, 40). Match at offset 20. QByteArray data(64, 0); data[20] = char(0xFE); QVector regions; regions.append({0, 64, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0xFE)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{20, 40}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)20); } void scan_constrainRegions_patternAtLastByte() { // Pattern at the very last valid position of a clipped sub-region. // Region [0, 64). Constraint [20, 40). 4-byte pattern at offset 36 (last valid: 40-4=36). QByteArray data(64, 0); data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF); QVector regions; regions.append({0, 64, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xDE\xAD\xBE\xEF", 4); req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4); req.constrainRegions = {{20, 40}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)36); } void scan_constrainRegions_patternOneByteAfterEnd() { // Pattern starts 1 byte before constraint end — only 3 of 4 bytes are in range. // Should NOT match because the full pattern doesn't fit. // Region [0, 64). Constraint [20, 39). 4-byte pattern at offset 36 (needs 36..39, but 39 is excluded). QByteArray data(64, 0); data[36] = char(0xDE); data[37] = char(0xAD); data[38] = char(0xBE); data[39] = char(0xEF); QVector regions; regions.append({0, 64, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xDE\xAD\xBE\xEF", 4); req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4); req.constrainRegions = {{20, 39}}; // ends at 39, pattern needs 36..39 inclusive engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); // pattern doesn't fit } void scan_constrainRegions_regionSmallerThanPattern() { // Clipped sub-region is smaller than the pattern. Should scan nothing, not crash. // Region [0, 64). Constraint [30, 32). 4-byte pattern can't fit in 2 bytes. QByteArray data(64, char(0xAA)); QVector regions; regions.append({0, 64, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA\xAA\xAA\xAA", 4); req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4); req.constrainRegions = {{30, 32}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } void scan_constrainRegions_patternExactlyFitsRegion() { // Clipped sub-region is exactly pattern size. Should find match if bytes match. // Region [0, 64). Constraint [30, 34). 4-byte pattern, 4-byte region. QByteArray data(64, 0); data[30] = char(0x11); data[31] = char(0x22); data[32] = char(0x33); data[33] = char(0x44); QVector regions; regions.append({0, 64, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\x11\x22\x33\x44", 4); req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4); req.constrainRegions = {{30, 34}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 1); QCOMPARE(results[0].address, (uint64_t)30); } void scan_constrainRegions_matchAtRegionBoundaries() { // Two adjacent clipped sub-regions. Matches at the last byte of the first // and first byte of the second. Both should be found. // Regions: [0, 16) and [16, 32). Constraint [0, 32) (full coverage). QByteArray data(32, 0); data[15] = char(0x77); // last byte of first region data[16] = char(0x77); // first byte of second region QVector regions; regions.append({0, 16, true, true, false, {}}); regions.append({16, 16, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray(1, char(0x77)); req.mask = QByteArray(1, char(0xFF)); req.constrainRegions = {{0, 32}}; engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 2); QCOMPARE(results[0].address, (uint64_t)15); QCOMPARE(results[1].address, (uint64_t)16); } void scan_constrainRegions_multibyteAtClipBoundary() { // 4-byte pattern that straddles the constraint boundary — should NOT be found // because the clipped region doesn't contain the full pattern. // Region [0, 64). Constraint [10, 13). Pattern at offset 10 is 4 bytes (10..13), // but constraint end is 13 (exclusive), so only 3 bytes [10,13) are in range. QByteArray data(64, 0); data[10] = char(0xAA); data[11] = char(0xBB); data[12] = char(0xCC); data[13] = char(0xDD); QVector regions; regions.append({0, 64, true, true, false, {}}); auto prov = std::make_shared(data, regions); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = QByteArray("\xAA\xBB\xCC\xDD", 4); req.mask = QByteArray("\xFF\xFF\xFF\xFF", 4); req.constrainRegions = {{10, 13}}; // only 3 bytes, pattern needs 4 engine.start(prov, req); QVERIFY(finSpy.wait(5000)); auto results = finSpy.first().first().value>(); QCOMPARE(results.size(), 0); } // ── Value type + pattern scans at every position in a constrained region ── // Helper: run a scan with the given pattern/mask/alignment in a constrained region, // return the result addresses. QVector scanConstrained(const QByteArray& data, const QByteArray& pat, const QByteArray& mask, int alignment, uint64_t cStart, uint64_t cEnd) { auto prov = std::make_shared(data); ScanEngine engine; QSignalSpy finSpy(&engine, &ScanEngine::finished); ScanRequest req; req.pattern = pat; req.mask = mask; req.alignment = alignment; req.constrainRegions = {{cStart, cEnd}}; engine.start(prov, req); if (!finSpy.wait(5000)) return {}; auto results = finSpy.first().first().value>(); QVector addrs; for (const auto& r : results) addrs.append(r.address); return addrs; } void scan_int32_atRegionStart() { QByteArray data(128, 0); int32_t v = 0x12345678; std::memcpy(data.data() + 32, &v, 4); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int32, "305419896", pat, mask)); // 0x12345678 auto addrs = scanConstrained(data, pat, mask, 4, 32, 96); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)32); } void scan_int32_atRegionEnd() { QByteArray data(128, 0); int32_t v = 0x12345678; // Last aligned 4-byte position in [32, 96) is 92 std::memcpy(data.data() + 92, &v, 4); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int32, "305419896", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 4, 32, 96); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)92); } void scan_float_atRegionStart() { QByteArray data(128, 0); float v = 3.14f; std::memcpy(data.data() + 16, &v, 4); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Float, "3.14", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 4, 16, 80); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)16); } void scan_float_atRegionEnd() { QByteArray data(128, 0); float v = 3.14f; // Last aligned 4-byte position in [16, 80) is 76 std::memcpy(data.data() + 76, &v, 4); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Float, "3.14", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 4, 16, 80); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)76); } void scan_double_atRegionEnd() { QByteArray data(128, 0); double v = 2.71828; // Last aligned 8-byte position in [0, 128) is 120 std::memcpy(data.data() + 120, &v, 8); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Double, "2.71828", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 8, 0, 128); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)120); } void scan_int64_atRegionEnd() { QByteArray data(128, 0); int64_t v = 0x0BADC0DEDEADBEEFLL; // Last aligned 8-byte position in [8, 72) is 64 std::memcpy(data.data() + 64, &v, 8); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int64, "841540768839352047", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 8, 8, 72); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)64); } void scan_utf16_atRegionEnd() { QByteArray data(128, 0); // "AB" in UTF-16LE = 4 bytes uint16_t chars[] = { 'A', 'B' }; // Last aligned 2-byte position where 4 bytes fit in [0, 128) is 124 std::memcpy(data.data() + 124, chars, 4); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UTF16, "AB", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 2, 0, 128); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)124); } void scan_vec3_atRegionEnd() { QByteArray data(128, 0); float v[] = { 1.0f, 2.0f, 3.0f }; // 12 bytes // Last aligned 4-byte position where 12 bytes fit in [0, 128) is 116 std::memcpy(data.data() + 116, v, 12); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Vec3, "1.0 2.0 3.0", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 4, 0, 128); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)116); } void scan_pattern_atRegionStart() { QByteArray data(128, 0); data[20] = char(0x48); data[21] = char(0x8B); data[22] = char(0x05); QByteArray pat, mask; QVERIFY(parseSignature("48 8B 05", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 1, 20, 100); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)20); } void scan_pattern_atRegionEnd() { QByteArray data(128, 0); // 3-byte pattern, last position in [20, 100) is 97 data[97] = char(0x48); data[98] = char(0x8B); data[99] = char(0x05); QByteArray pat, mask; QVERIFY(parseSignature("48 8B 05", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 1, 20, 100); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)97); } void scan_pattern_withWildcard_atRegionEnd() { QByteArray data(128, 0); // "48 ?? 05" at last position 97 in [20, 100) data[97] = char(0x48); data[98] = char(0xFF); data[99] = char(0x05); QByteArray pat, mask; QVERIFY(parseSignature("48 ?? 05", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 1, 20, 100); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)97); } void scan_int32_multiplePositions_inConstrainedRegion() { // Place int32 at first, middle, and last aligned positions in [32, 96). // Aligned positions: 32, 36, 40, ..., 88, 92. First=32, last=92, mid=60. QByteArray data(128, 0); int32_t v = 0xCAFEBABE; std::memcpy(data.data() + 32, &v, 4); std::memcpy(data.data() + 60, &v, 4); std::memcpy(data.data() + 92, &v, 4); // Also place one outside the constraint to verify it's excluded std::memcpy(data.data() + 8, &v, 4); std::memcpy(data.data() + 100, &v, 4); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UInt32, "0xCAFEBABE", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 4, 32, 96); QCOMPARE(addrs.size(), 3); QCOMPARE(addrs[0], (uint64_t)32); QCOMPARE(addrs[1], (uint64_t)60); QCOMPARE(addrs[2], (uint64_t)92); } void scan_pattern_multiplePositions_inConstrainedRegion() { // IDA-style pattern at first, last, and middle of [16, 80). // Pattern "AA BB" (2 bytes), alignment 1. First=16, last=78, mid=50. QByteArray data(128, 0); data[16] = char(0xAA); data[17] = char(0xBB); data[50] = char(0xAA); data[51] = char(0xBB); data[78] = char(0xAA); data[79] = char(0xBB); // Outside constraint data[10] = char(0xAA); data[11] = char(0xBB); data[90] = char(0xAA); data[91] = char(0xBB); QByteArray pat, mask; QVERIFY(parseSignature("AA BB", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 1, 16, 80); QCOMPARE(addrs.size(), 3); QCOMPARE(addrs[0], (uint64_t)16); QCOMPARE(addrs[1], (uint64_t)50); QCOMPARE(addrs[2], (uint64_t)78); } void scan_int8_alignment1_atRegionEnd() { // 1-byte value at last byte of constrained region [10, 50). QByteArray data(64, 0); data[49] = char(0x7F); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Int8, "127", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 1, 10, 50); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)49); } void scan_uint16_alignment2_atRegionEnd() { // 2-byte value at last aligned-2 position in [10, 50) = offset 48. QByteArray data(64, 0); uint16_t v = 0xBEEF; std::memcpy(data.data() + 48, &v, 2); QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UInt16, "0xBEEF", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 2, 10, 50); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)48); } void scan_alignment4_skipsUnaligned() { // int32 placed at unaligned offset 18 inside [16, 48). Alignment 4. // Aligned positions from 16: 16, 20, 24, 28, 32, 36, 40, 44. // Offset 18 is not aligned to 4 from the region start, so should be skipped. QByteArray data(64, 0); int32_t v = 0xDEADBEEF; std::memcpy(data.data() + 18, &v, 4); // unaligned QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UInt32, "0xDEADBEEF", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 4, 16, 48); QCOMPARE(addrs.size(), 0); } void scan_alignment8_skipsUnaligned() { // double placed at offset 12 inside [0, 64). Alignment 8. // Aligned positions: 0, 8, 16, 24, 32, 40, 48, 56. // Offset 12 is not 8-aligned, so should be skipped. QByteArray data(64, 0); double v = 99.99; std::memcpy(data.data() + 12, &v, 8); // unaligned QByteArray pat, mask; QVERIFY(serializeValue(ValueType::Double, "99.99", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 8, 0, 64); QCOMPARE(addrs.size(), 0); } void scan_alignment2_findsAligned_skipsUnaligned() { // utf16 "Hi" (4 bytes) at aligned offset 20 and unaligned offset 33. // Constraint [16, 48), alignment 2. Should find only offset 20. QByteArray data(64, 0); uint16_t chars[] = { 'H', 'i' }; std::memcpy(data.data() + 20, chars, 4); // aligned to 2 std::memcpy(data.data() + 33, chars, 4); // unaligned to 2 QByteArray pat, mask; QVERIFY(serializeValue(ValueType::UTF16, "Hi", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 2, 16, 48); QCOMPARE(addrs.size(), 1); QCOMPARE(addrs[0], (uint64_t)20); } void scan_alignment1_overlappingWrites() { // Pattern "AA BB" written at 20, then overwritten at 21, plus 25. // Second write clobbers offset 20's pattern; only 21 and 25 match. QByteArray data(48, 0); data[20] = char(0xAA); data[21] = char(0xBB); data[21] = char(0xAA); data[22] = char(0xBB); // overlapping at 21 data[25] = char(0xAA); data[26] = char(0xBB); QByteArray pat, mask; QVERIFY(parseSignature("AA BB", pat, mask)); auto addrs = scanConstrained(data, pat, mask, 1, 16, 32); QCOMPARE(addrs.size(), 2); // 21 and 25 (20 was overwritten) QCOMPARE(addrs[0], (uint64_t)21); QCOMPARE(addrs[1], (uint64_t)25); } }; QTEST_MAIN(TestScanner) #include "test_scanner.moc"