Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions include/Mortis/Detail/CompileSignature.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#pragma once

#include <Mortis/MemoryScanner.hpp>

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string_view>

namespace Mortis {

template <std::size_t N>
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 <std::size_t N>
FixedString(const char (&)[N]) -> FixedString<N - 1>;

template <std::size_t N>
using FixedSignature = std::array<SignatureElement, N>;


namespace detail {

consteval auto HexDigit(const char c) noexcept -> std::uint8_t {
if (c >= '0' && c <= '9') return static_cast<std::uint8_t>(c - '0');
if (c >= 'A' && c <= 'F') return static_cast<std::uint8_t>(c - 'A' + 10);
if (c >= 'a' && c <= 'f') return static_cast<std::uint8_t>(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 <std::size_t MaxN>
consteval auto ParseSignatureCompileTime(const std::string_view str)
-> std::pair<std::array<SignatureElement, MaxN>, std::size_t> {
std::array<SignatureElement, MaxN> 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<std::byte>(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<std::byte>((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 <FixedString Str>
consteval auto CompileSignature() {
constexpr auto maxN = detail::CountTokens(Str.view());
constexpr auto pair = detail::ParseSignatureCompileTime<maxN>(Str.view());
constexpr auto size = pair.second;

FixedSignature<size> 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<SignatureElement, N>`.
template <FixedString Str>
consteval auto operator""_sig() noexcept {
return CompileSignature<Str>();
}

/// @brief Compile-time signature literal that returns a SignatureView.
/// The underlying storage has static storage duration.
/// Usage: `SignatureView sv = "48 8B ? CC"_sigv;`
template <FixedString Str>
constexpr auto operator""_sigv() noexcept {
static constexpr auto sig = CompileSignature<Str>();
return SignatureView{sig};
}

} // namespace signature_literals
} // namespace literals

} // namespace Mortis
1 change: 1 addition & 0 deletions include/Mortis/Mortis.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <Mortis/Config.hpp>
#include <Mortis/Detail/CompileSignature.hpp>
#include <Mortis/ImportHook.hpp>
#include <Mortis/InlineHook.hpp>
#include <Mortis/MemoryScanner.hpp>
Expand Down
116 changes: 116 additions & 0 deletions tests/TestMemoryScanner.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "TestHelpers.hpp"
#include <Mortis/Process.hpp>
#include <Mortis/Detail/CompileSignature.hpp>

#include <gtest/gtest.h>

#include <array>
Expand Down Expand Up @@ -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}));
}