diff --git a/include/Mortis/Detail/CompileSignature.hpp b/include/Mortis/Detail/CompileSignature.hpp new file mode 100644 index 0000000..e36c30c --- /dev/null +++ b/include/Mortis/Detail/CompileSignature.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace Mortis { + +template +struct FixedString { + char value[N + 1]{}; + + constexpr FixedString(const char (&str)[N + 1]) { // NOLINT(google-explicit-constructor) + std::copy_n(str, N + 1, value); + } + + [[nodiscard]] constexpr auto data() const noexcept -> const char* { return value; } + [[nodiscard]] constexpr auto size() const noexcept -> std::size_t { return N; } + [[nodiscard]] constexpr auto view() const noexcept -> std::string_view { return {value, N}; } +}; + +template +FixedString(const char (&)[N]) -> FixedString; + +template +using FixedSignature = std::array; + + +namespace detail { + +consteval auto HexDigit(const char c) noexcept -> std::uint8_t { + if (c >= '0' && c <= '9') return static_cast(c - '0'); + if (c >= 'A' && c <= 'F') return static_cast(c - 'A' + 10); + if (c >= 'a' && c <= 'f') return static_cast(c - 'a' + 10); + return 0xFF; // invalid +} + +consteval auto CountTokens(const std::string_view str) noexcept -> std::size_t { + std::size_t count = 0; + bool inToken = false; + for (const char c : str) { + if (c == ' ') { + inToken = false; + } else if (!inToken) { + inToken = true; + ++count; + } + } + return count; +} + +template +consteval auto ParseSignatureCompileTime(const std::string_view str) + -> std::pair, std::size_t> { + std::array result{}; + std::size_t count = 0; + + std::size_t i = 0; + while (i < str.size()) { + // Skip spaces. + if (str[i] == ' ') { + ++i; + continue; + } + + std::size_t tokenStart = i; + while (i < str.size() && str[i] != ' ') ++i; + const std::size_t tokenLen = i - tokenStart; + + if (tokenLen == 1) { + if (str[tokenStart] != '?') { + throw "Invalid single-character token in signature (expected '?')"; + } + result[count++] = SignatureElement::Wildcard(); + } else if (tokenLen == 2) { + const char hi = str[tokenStart]; + + if (const char lo = str[tokenStart + 1]; hi == '?' && lo == '?') { + // "??" → wildcard. + result[count++] = SignatureElement::Wildcard(); + } else if (hi == '?') { + // "?B" → low nibble known. + const auto loVal = HexDigit(lo); + if (loVal == 0xFF) throw "Invalid hex digit in signature"; + result[count++] = {std::byte{loVal}, std::byte{0x0F}}; + } else if (lo == '?') { + // "A?" → high nibble known. + const auto hiVal = HexDigit(hi); + if (hiVal == 0xFF) throw "Invalid hex digit in signature"; + result[count++] = {static_cast(hiVal << 4), std::byte{0xF0}}; + } else { + // "AB" → exact byte. + const auto hiVal = HexDigit(hi); + const auto loVal = HexDigit(lo); + if (hiVal == 0xFF || loVal == 0xFF) throw "Invalid hex digit in signature"; + result[count++] = SignatureElement::Byte(static_cast((hiVal << 4) | loVal)); + } + } else { + throw "Invalid token length in signature (expected 1 or 2 characters per token)"; + } + } + + if (count == 0) throw "Empty signature"; + bool foundConcrete = false; + for (std::size_t k = 0; k < count; ++k) { + if (result[k].mask != std::byte{0x00}) { // not a wildcard + if (result[k].mask != std::byte{0xFF}) { + throw "First non-wildcard byte must be fully specified (no partial mask), " + "e.g. use 'AA 4? ...' instead of '4? ...'"; + } + foundConcrete = true; + break; + } + } + if (!foundConcrete) throw "Signature must contain at least one concrete (non-wildcard) byte"; + + return {result, count}; +} + +} // namespace detail + +template +consteval auto CompileSignature() { + constexpr auto maxN = detail::CountTokens(Str.view()); + constexpr auto pair = detail::ParseSignatureCompileTime(Str.view()); + constexpr auto size = pair.second; + + FixedSignature out{}; + for (std::size_t i = 0; i < size; ++i) { + out[i] = pair.first[i]; + } + return out; +} + +inline namespace literals { +inline namespace signature_literals { + +/// @brief Compile-time signature literal. +/// Usage: `auto sig = "48 8B ? CC"_sig;` +/// Returns a `std::array`. +template +consteval auto operator""_sig() noexcept { + return CompileSignature(); +} + +/// @brief Compile-time signature literal that returns a SignatureView. +/// The underlying storage has static storage duration. +/// Usage: `SignatureView sv = "48 8B ? CC"_sigv;` +template +constexpr auto operator""_sigv() noexcept { + static constexpr auto sig = CompileSignature(); + return SignatureView{sig}; +} + +} // namespace signature_literals +} // namespace literals + +} // namespace Mortis diff --git a/include/Mortis/Mortis.hpp b/include/Mortis/Mortis.hpp index eb223f3..87f7806 100644 --- a/include/Mortis/Mortis.hpp +++ b/include/Mortis/Mortis.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/tests/TestMemoryScanner.cpp b/tests/TestMemoryScanner.cpp index 6181d85..f68000f 100644 --- a/tests/TestMemoryScanner.cpp +++ b/tests/TestMemoryScanner.cpp @@ -1,5 +1,7 @@ #include "TestHelpers.hpp" #include +#include + #include #include @@ -355,3 +357,117 @@ TEST(MemoryScanner, ScanBufferWildcard) { ASSERT_TRUE(result.hasResult()); EXPECT_EQ(result.getRaw(), &buf[0]); } + +// CompileSignature / _sig literal tests +using namespace Mortis::literals::signature_literals; + +TEST(CompileSignature, BasicParse) { + constexpr auto sig = Mortis::CompileSignature<"48 8B CC">(); + static_assert(sig.size() == 3); + EXPECT_TRUE(sig[0].isConcrete()); + EXPECT_TRUE(sig[0].matches(std::byte{0x48})); + EXPECT_TRUE(sig[1].isConcrete()); + EXPECT_TRUE(sig[1].matches(std::byte{0x8B})); + EXPECT_TRUE(sig[2].isConcrete()); + EXPECT_TRUE(sig[2].matches(std::byte{0xCC})); +} + +TEST(CompileSignature, WildcardSingle) { + constexpr auto sig = Mortis::CompileSignature<"AA ? BB">(); + static_assert(sig.size() == 3); + EXPECT_TRUE(sig[0].isConcrete()); + EXPECT_TRUE(sig[1].isWildcard()); + EXPECT_TRUE(sig[2].isConcrete()); +} + +TEST(CompileSignature, WildcardDouble) { + constexpr auto sig = Mortis::CompileSignature<"AA ?? BB">(); + static_assert(sig.size() == 3); + EXPECT_TRUE(sig[1].isWildcard()); +} + +TEST(CompileSignature, PartialMaskHighNibble) { + // "4?" means high nibble is 0x4, low nibble is wildcard → mask 0xF0. + // The first non-wildcard byte must be fully specified, so place the + // partial mask after a concrete byte. + constexpr auto sig = Mortis::CompileSignature<"8B 4?">(); + static_assert(sig.size() == 2); + EXPECT_TRUE(sig[0].isConcrete()); // first byte is concrete + EXPECT_FALSE(sig[1].isWildcard()); + EXPECT_FALSE(sig[1].isConcrete()); + EXPECT_TRUE(sig[1].matches(std::byte{0x48})); + EXPECT_TRUE(sig[1].matches(std::byte{0x4F})); + EXPECT_FALSE(sig[1].matches(std::byte{0x58})); +} + +TEST(CompileSignature, PartialMaskLowNibble) { + // "?B" means low nibble is 0xB, high nibble is wildcard → mask 0x0F. + // Partial mask must come after a fully-specified first byte. + constexpr auto sig = Mortis::CompileSignature<"CC ?B">(); + static_assert(sig.size() == 2); + EXPECT_TRUE(sig[0].isConcrete()); // first byte is concrete + EXPECT_FALSE(sig[1].isWildcard()); + EXPECT_FALSE(sig[1].isConcrete()); + EXPECT_TRUE(sig[1].matches(std::byte{0x0B})); + EXPECT_TRUE(sig[1].matches(std::byte{0xAB})); + EXPECT_FALSE(sig[1].matches(std::byte{0xAC})); +} + +TEST(CompileSignature, SigLiteral) { + constexpr auto sig = "48 8B ? CC"_sig; + static_assert(sig.size() == 4); + EXPECT_TRUE(sig[0].isConcrete()); + EXPECT_TRUE(sig[0].matches(std::byte{0x48})); + EXPECT_TRUE(sig[1].isConcrete()); + EXPECT_TRUE(sig[2].isWildcard()); + EXPECT_TRUE(sig[3].isConcrete()); +} + +TEST(CompileSignature, SigvLiteral) { + auto view = "AA BB CC"_sigv; + EXPECT_EQ(view.size(), 3u); + EXPECT_TRUE(view[0].matches(std::byte{0xAA})); + EXPECT_TRUE(view[1].matches(std::byte{0xBB})); + EXPECT_TRUE(view[2].matches(std::byte{0xCC})); +} + +TEST(CompileSignature, LowercaseHex) { + constexpr auto sig = "de ad be ef"_sig; + static_assert(sig.size() == 4); + EXPECT_TRUE(sig[0].matches(std::byte{0xDE})); + EXPECT_TRUE(sig[1].matches(std::byte{0xAD})); + EXPECT_TRUE(sig[2].matches(std::byte{0xBE})); + EXPECT_TRUE(sig[3].matches(std::byte{0xEF})); +} + +TEST(CompileSignature, UsableWithScanBuffer) { + const std::array buf = { + std::byte{0x48}, std::byte{0x8B}, std::byte{0x01}, std::byte{0xCC}, + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, + }; + constexpr auto sig = "48 8B ? CC"_sig; + auto result = MemoryScanner::ScanBuffer(buf, sig); + ASSERT_TRUE(result.hasResult()); + EXPECT_EQ(result.getRaw(), &buf[0]); +} + +TEST(CompileSignature, ConsistencyWithRuntimeParse) { + // Compile-time and runtime parsing should produce identical results. + constexpr auto compiled = "48 8B ? CC"_sig; + auto runtimeSig = MemoryScanner::ParseSignature("48 8B ? CC"); + ASSERT_TRUE(runtimeSig.has_value()); + ASSERT_EQ(compiled.size(), runtimeSig->size()); + for (std::size_t i = 0; i < compiled.size(); ++i) { + EXPECT_EQ(compiled[i], (*runtimeSig)[i]) << "Mismatch at index " << i; + } +} + +TEST(CompileSignature, MultipleSpaces) { + // Extra spaces between tokens should be ignored. + constexpr auto sig = "AA BB CC"_sig; + static_assert(sig.size() == 3); + EXPECT_TRUE(sig[0].matches(std::byte{0xAA})); + EXPECT_TRUE(sig[1].matches(std::byte{0xBB})); + EXPECT_TRUE(sig[2].matches(std::byte{0xCC})); +} +