Add initial project files (excluding ignored content)
This commit is contained in:
31
external/safetyhook/test/allocator.cpp
vendored
Normal file
31
external/safetyhook/test/allocator.cpp
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
#include <boost/ut.hpp>
|
||||
#include <safetyhook.hpp>
|
||||
|
||||
using namespace boost::ut;
|
||||
|
||||
static suite<"allocator"> allocator_tests = [] {
|
||||
"Allocator reuses freed memory"_test = [] {
|
||||
const auto allocator = safetyhook::Allocator::create();
|
||||
auto first_allocation = allocator->allocate(128);
|
||||
|
||||
expect(first_allocation.has_value());
|
||||
|
||||
const auto first_allocation_address = first_allocation->address();
|
||||
const auto second_allocation = allocator->allocate(256);
|
||||
|
||||
expect(second_allocation.has_value());
|
||||
expect(neq(second_allocation->address(), first_allocation_address));
|
||||
|
||||
first_allocation->free();
|
||||
|
||||
const auto third_allocation = allocator->allocate(64);
|
||||
|
||||
expect(third_allocation.has_value());
|
||||
expect(eq(third_allocation->address(), first_allocation_address));
|
||||
|
||||
const auto fourth_allocation = allocator->allocate(64);
|
||||
|
||||
expect(fourth_allocation.has_value());
|
||||
expect(eq(fourth_allocation->address(), third_allocation->address() + 64));
|
||||
};
|
||||
};
|
||||
673
external/safetyhook/test/inline_hook.cpp
vendored
Normal file
673
external/safetyhook/test/inline_hook.cpp
vendored
Normal file
@@ -0,0 +1,673 @@
|
||||
#include <thread>
|
||||
|
||||
#include <boost/ut.hpp>
|
||||
#include <safetyhook.hpp>
|
||||
#include <xbyak/xbyak.h>
|
||||
|
||||
using namespace std::literals;
|
||||
using namespace boost::ut;
|
||||
using namespace Xbyak::util;
|
||||
|
||||
static suite<"inline hook"> inline_hook_tests = [] {
|
||||
"Function hooked multiple times"_test = [] {
|
||||
struct Target {
|
||||
SAFETYHOOK_NOINLINE static std::string fn(std::string name) { return "hello " + name; }
|
||||
};
|
||||
|
||||
expect(eq(Target::fn("world"), "hello world"sv));
|
||||
|
||||
// First hook.
|
||||
static SafetyHookInline hook0;
|
||||
|
||||
struct Hook0 {
|
||||
static std::string fn(std::string name) { return hook0.call<std::string>(name + " and bob"); }
|
||||
};
|
||||
|
||||
auto hook0_result = SafetyHookInline::create(Target::fn, Hook0::fn);
|
||||
|
||||
expect(hook0_result.has_value());
|
||||
|
||||
hook0 = std::move(*hook0_result);
|
||||
|
||||
expect(eq(Target::fn("world"), "hello world and bob"sv));
|
||||
|
||||
// Second hook.
|
||||
static SafetyHookInline hook1;
|
||||
|
||||
struct Hook1 {
|
||||
static std::string fn(std::string name) { return hook1.call<std::string>(name + " and alice"); }
|
||||
};
|
||||
|
||||
auto hook1_result = SafetyHookInline::create(Target::fn, Hook1::fn);
|
||||
|
||||
expect(hook1_result.has_value());
|
||||
|
||||
hook1 = std::move(*hook1_result);
|
||||
|
||||
expect(eq(Target::fn("world"), "hello world and alice and bob"sv));
|
||||
|
||||
// Third hook.
|
||||
static SafetyHookInline hook2;
|
||||
|
||||
struct Hook2 {
|
||||
static std::string fn(std::string name) { return hook2.call<std::string>(name + " and eve"); }
|
||||
};
|
||||
|
||||
auto hook2_result = SafetyHookInline::create(Target::fn, Hook2::fn);
|
||||
|
||||
expect(hook2_result.has_value());
|
||||
|
||||
hook2 = std::move(*hook2_result);
|
||||
|
||||
expect(eq(Target::fn("world"), "hello world and eve and alice and bob"sv));
|
||||
|
||||
// Fourth hook.
|
||||
static SafetyHookInline hook3;
|
||||
|
||||
struct Hook3 {
|
||||
static std::string fn(std::string name) { return hook3.call<std::string>(name + " and carol"); }
|
||||
};
|
||||
|
||||
auto hook3_result = SafetyHookInline::create(Target::fn, Hook3::fn);
|
||||
|
||||
expect(hook3_result.has_value());
|
||||
|
||||
hook3 = std::move(*hook3_result);
|
||||
|
||||
expect(eq(Target::fn("world"), "hello world and carol and eve and alice and bob"sv));
|
||||
|
||||
// Unhook.
|
||||
hook3.reset();
|
||||
hook2.reset();
|
||||
hook1.reset();
|
||||
hook0.reset();
|
||||
};
|
||||
|
||||
"Function with multiple args hooked"_test = [] {
|
||||
struct Target {
|
||||
SAFETYHOOK_NOINLINE static int add(int x, int y) { return x + y; }
|
||||
};
|
||||
|
||||
expect(Target::add(2, 3) == 5_i);
|
||||
|
||||
static SafetyHookInline add_hook;
|
||||
|
||||
struct AddHook {
|
||||
static int add(int x, int y) { return add_hook.call<int>(x * 2, y * 2); }
|
||||
};
|
||||
|
||||
auto add_hook_result = SafetyHookInline::create(Target::add, AddHook::add);
|
||||
|
||||
expect(add_hook_result.has_value());
|
||||
|
||||
add_hook = std::move(*add_hook_result);
|
||||
|
||||
expect(Target::add(3, 4) == 14_i);
|
||||
|
||||
add_hook.reset();
|
||||
|
||||
expect(Target::add(5, 6) == 11_i);
|
||||
};
|
||||
|
||||
#if SAFETYHOOK_OS_WINDOWS
|
||||
"Active function is hooked and unhooked"_test = [] {
|
||||
static int count = 0;
|
||||
static bool is_running = true;
|
||||
|
||||
struct Target {
|
||||
SAFETYHOOK_NOINLINE static std::string say_hello(int times) { return "Hello #" + std::to_string(times); }
|
||||
|
||||
static void say_hello_infinitely() {
|
||||
while (is_running) {
|
||||
say_hello(count++);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::thread t{Target::say_hello_infinitely};
|
||||
|
||||
std::this_thread::sleep_for(1s);
|
||||
|
||||
static SafetyHookInline hook;
|
||||
|
||||
struct Hook {
|
||||
static std::string say_hello(int times [[maybe_unused]]) { return hook.call<std::string>(1337); }
|
||||
};
|
||||
|
||||
auto hook_result = SafetyHookInline::create(Target::say_hello, Hook::say_hello);
|
||||
|
||||
expect(hook_result.has_value());
|
||||
|
||||
hook = std::move(*hook_result);
|
||||
|
||||
expect(eq(Target::say_hello(0), "Hello #1337"sv));
|
||||
|
||||
std::this_thread::sleep_for(1s);
|
||||
hook.reset();
|
||||
|
||||
is_running = false;
|
||||
t.join();
|
||||
|
||||
expect(eq(Target::say_hello(0), "Hello #0"sv));
|
||||
expect(count > 0_i);
|
||||
};
|
||||
#endif
|
||||
|
||||
"Function with short unconditional branch is hooked"_test = [] {
|
||||
static SafetyHookInline hook;
|
||||
|
||||
struct Hook {
|
||||
static int SAFETYHOOK_FASTCALL fn() { return hook.fastcall<int>() + 42; };
|
||||
};
|
||||
|
||||
Xbyak::CodeGenerator cg{};
|
||||
|
||||
cg.jmp("@f");
|
||||
cg.mov(eax, 0);
|
||||
cg.ret();
|
||||
cg.nop(10, false);
|
||||
cg.L("@@");
|
||||
cg.mov(eax, 1);
|
||||
cg.ret();
|
||||
cg.nop(10, false);
|
||||
|
||||
const auto fn = cg.getCode<int(SAFETYHOOK_FASTCALL*)()>();
|
||||
|
||||
expect(fn() == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn() == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn() == 1_i);
|
||||
};
|
||||
|
||||
"Function with short conditional branch is hooked"_test = [] {
|
||||
static SafetyHookInline hook;
|
||||
|
||||
struct Hook {
|
||||
static int SAFETYHOOK_FASTCALL fn(int x) { return hook.fastcall<int>(x) + 42; };
|
||||
};
|
||||
|
||||
Xbyak::CodeGenerator cg{};
|
||||
Xbyak::Label label{};
|
||||
const auto finalize = [&cg, &label] {
|
||||
cg.mov(eax, 0);
|
||||
cg.ret();
|
||||
cg.nop(10, false);
|
||||
cg.L(label);
|
||||
cg.mov(eax, 1);
|
||||
cg.ret();
|
||||
cg.nop(10, false);
|
||||
return cg.getCode<int(SAFETYHOOK_FASTCALL*)(int)>();
|
||||
};
|
||||
|
||||
#if SAFETYHOOK_OS_WINDOWS
|
||||
constexpr auto param = ecx;
|
||||
#elif SAFETYHOOK_OS_LINUX
|
||||
constexpr auto param = edi;
|
||||
#endif
|
||||
|
||||
"JB"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jb(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 43_i);
|
||||
expect(fn(8) == 42_i);
|
||||
expect(fn(9) == 42_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JBE"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jbe(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 43_i);
|
||||
expect(fn(8) == 43_i);
|
||||
expect(fn(9) == 42_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JL"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jl(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 43_i);
|
||||
expect(fn(8) == 42_i);
|
||||
expect(fn(9) == 42_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JLE"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jle(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 43_i);
|
||||
expect(fn(8) == 43_i);
|
||||
expect(fn(9) == 42_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JNB"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jnb(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 42_i);
|
||||
expect(fn(8) == 43_i);
|
||||
expect(fn(9) == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JNBE"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jnbe(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 42_i);
|
||||
expect(fn(8) == 42_i);
|
||||
expect(fn(9) == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JNL"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jnl(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 42_i);
|
||||
expect(fn(8) == 43_i);
|
||||
expect(fn(9) == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JNLE"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jnle(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 42_i);
|
||||
expect(fn(8) == 42_i);
|
||||
expect(fn(9) == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JNO"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jno(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 43_i);
|
||||
expect(fn(8) == 43_i);
|
||||
expect(fn(9) == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JNP"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jnp(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 42_i);
|
||||
expect(fn(8) == 42_i);
|
||||
expect(fn(9) == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JNS"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jns(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 42_i);
|
||||
expect(fn(8) == 43_i);
|
||||
expect(fn(9) == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JNZ"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jnz(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 43_i);
|
||||
expect(fn(8) == 42_i);
|
||||
expect(fn(9) == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 1_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JO"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jo(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 42_i);
|
||||
expect(fn(8) == 42_i);
|
||||
expect(fn(9) == 42_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JP"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jp(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 43_i);
|
||||
expect(fn(8) == 43_i);
|
||||
expect(fn(9) == 42_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JS"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.js(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 43_i);
|
||||
expect(fn(8) == 42_i);
|
||||
expect(fn(9) == 42_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 1_i);
|
||||
expect(fn(8) == 0_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
|
||||
"JZ"_test = [&] {
|
||||
cg.cmp(param, 8);
|
||||
cg.jz(label);
|
||||
const auto fn = finalize();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn(7) == 42_i);
|
||||
expect(fn(8) == 43_i);
|
||||
expect(fn(9) == 42_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(7) == 0_i);
|
||||
expect(fn(8) == 1_i);
|
||||
expect(fn(9) == 0_i);
|
||||
|
||||
cg.reset();
|
||||
};
|
||||
};
|
||||
|
||||
"Function with short jump inside trampoline"_test = [] {
|
||||
Xbyak::CodeGenerator cg{};
|
||||
|
||||
cg.jmp("@f");
|
||||
cg.ret();
|
||||
cg.L("@@");
|
||||
cg.mov(eax, 42);
|
||||
cg.ret();
|
||||
cg.nop(10, false);
|
||||
|
||||
const auto fn = cg.getCode<int (*)()>();
|
||||
|
||||
expect(fn() == 42_i);
|
||||
|
||||
static SafetyHookInline hook;
|
||||
|
||||
struct Hook {
|
||||
static int fn() { return hook.call<int>() + 1; }
|
||||
};
|
||||
|
||||
hook = safetyhook::create_inline(fn, Hook::fn);
|
||||
|
||||
expect(fn() == 43_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn() == 42_i);
|
||||
};
|
||||
|
||||
"Function hook can be enable and disabled"_test = [] {
|
||||
struct Target {
|
||||
SAFETYHOOK_NOINLINE static int fn(int a) {
|
||||
volatile int b = a;
|
||||
return b * 2;
|
||||
}
|
||||
};
|
||||
|
||||
expect(Target::fn(1) == 2_i);
|
||||
expect(Target::fn(2) == 4_i);
|
||||
expect(Target::fn(3) == 6_i);
|
||||
|
||||
static SafetyHookInline hook;
|
||||
|
||||
struct Hook {
|
||||
static int fn(int a) { return hook.call<int>(a + 1); }
|
||||
};
|
||||
|
||||
auto hook0_result = SafetyHookInline::create(Target::fn, Hook::fn, SafetyHookInline::StartDisabled);
|
||||
|
||||
expect(hook0_result.has_value());
|
||||
|
||||
hook = std::move(*hook0_result);
|
||||
|
||||
expect(Target::fn(1) == 2_i);
|
||||
expect(Target::fn(2) == 4_i);
|
||||
expect(Target::fn(3) == 6_i);
|
||||
|
||||
expect(hook.enable().has_value());
|
||||
|
||||
expect(Target::fn(1) == 4_i);
|
||||
expect(Target::fn(2) == 6_i);
|
||||
expect(Target::fn(3) == 8_i);
|
||||
|
||||
expect(hook.disable().has_value());
|
||||
|
||||
expect(Target::fn(1) == 2_i);
|
||||
expect(Target::fn(2) == 4_i);
|
||||
expect(Target::fn(3) == 6_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(Target::fn(1) == 2_i);
|
||||
expect(Target::fn(2) == 4_i);
|
||||
expect(Target::fn(3) == 6_i);
|
||||
};
|
||||
};
|
||||
104
external/safetyhook/test/inline_hook.x86_64.cpp
vendored
Normal file
104
external/safetyhook/test/inline_hook.x86_64.cpp
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
#include <boost/ut.hpp>
|
||||
#include <safetyhook.hpp>
|
||||
#include <xbyak/xbyak.h>
|
||||
|
||||
#if SAFETYHOOK_ARCH_X86_64
|
||||
|
||||
using namespace std::literals;
|
||||
using namespace boost::ut;
|
||||
using namespace Xbyak::util;
|
||||
|
||||
void asciiz(Xbyak::CodeGenerator& cg, const char* str) {
|
||||
while (*str) {
|
||||
cg.db(*str++);
|
||||
}
|
||||
|
||||
cg.db(0);
|
||||
}
|
||||
|
||||
static suite<"inline hook (x64)"> inline_hook_x64_tests = [] {
|
||||
"Function with RIP-relative operand is hooked"_test = [] {
|
||||
Xbyak::CodeGenerator cg{};
|
||||
Xbyak::Label str_label{};
|
||||
|
||||
cg.lea(rax, ptr[rip + str_label]);
|
||||
cg.ret();
|
||||
|
||||
for (auto i = 0; i < 10; ++i) {
|
||||
cg.nop(10, false);
|
||||
}
|
||||
|
||||
cg.L(str_label);
|
||||
asciiz(cg, "Hello");
|
||||
|
||||
const auto fn = cg.getCode<const char* (*)()>();
|
||||
|
||||
expect(eq(fn(), "Hello"sv));
|
||||
|
||||
static SafetyHookInline hook;
|
||||
|
||||
struct Hook {
|
||||
static const char* fn() { return "Hello, world!"; }
|
||||
};
|
||||
|
||||
auto hook_result = SafetyHookInline::create(fn, Hook::fn);
|
||||
|
||||
expect(hook_result.has_value());
|
||||
|
||||
hook = std::move(*hook_result);
|
||||
|
||||
expect(eq(fn(), "Hello, world!"sv));
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(eq(fn(), "Hello"sv));
|
||||
};
|
||||
|
||||
"Function with no nearby memory is hooked"_test = [] {
|
||||
Xbyak::CodeGenerator cg{5'000'000'000}; // 5 GB
|
||||
Xbyak::Label start{};
|
||||
|
||||
#if SAFETYHOOK_OS_WINDOWS
|
||||
constexpr auto param = ecx;
|
||||
#elif SAFETYHOOK_OS_LINUX
|
||||
constexpr auto param = edi;
|
||||
#endif
|
||||
|
||||
cg.nop(2'500'000'000, false); // 2.5 GB
|
||||
cg.L(start);
|
||||
cg.mov(dword[rsp + 8], param);
|
||||
cg.mov(eax, dword[rsp + 8]);
|
||||
cg.imul(eax, dword[rsp + 8]);
|
||||
cg.ret();
|
||||
|
||||
auto fn = reinterpret_cast<int (*)(int)>(const_cast<uint8_t*>(start.getAddress()));
|
||||
|
||||
expect(fn(2) == 4_i);
|
||||
expect(fn(3) == 9_i);
|
||||
expect(fn(4) == 16_i);
|
||||
|
||||
static SafetyHookInline hook;
|
||||
|
||||
struct Hook {
|
||||
static int fn(int a) { return hook.call<int>(a) * a; }
|
||||
};
|
||||
|
||||
auto hook_result = SafetyHookInline::create(fn, Hook::fn);
|
||||
|
||||
expect(hook_result.has_value());
|
||||
|
||||
hook = std::move(*hook_result);
|
||||
|
||||
expect(fn(2) == 8_i);
|
||||
expect(fn(3) == 27_i);
|
||||
expect(fn(4) == 64_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(fn(2) == 4_i);
|
||||
expect(fn(3) == 9_i);
|
||||
expect(fn(4) == 16_i);
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
||||
4
external/safetyhook/test/main.cpp
vendored
Normal file
4
external/safetyhook/test/main.cpp
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
#include <boost/ut.hpp>
|
||||
|
||||
int main() {
|
||||
}
|
||||
135
external/safetyhook/test/mid_hook.cpp
vendored
Normal file
135
external/safetyhook/test/mid_hook.cpp
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
#include <boost/ut.hpp>
|
||||
#include <safetyhook.hpp>
|
||||
|
||||
using namespace boost::ut;
|
||||
|
||||
static suite<"mid hook"> mid_hook_tests = [] {
|
||||
"Mid hook to change a register"_test = [] {
|
||||
struct Target {
|
||||
SAFETYHOOK_NOINLINE static int SAFETYHOOK_FASTCALL add_42(int a) { return a + 42; }
|
||||
};
|
||||
|
||||
expect(Target::add_42(0) == 42_i);
|
||||
|
||||
static SafetyHookMid hook;
|
||||
|
||||
struct Hook {
|
||||
static void add_42(SafetyHookContext& ctx) {
|
||||
#if SAFETYHOOK_OS_WINDOWS
|
||||
#if SAFETYHOOK_ARCH_X86_64
|
||||
ctx.rcx = 1337 - 42;
|
||||
#elif SAFETYHOOK_ARCH_X86_32
|
||||
ctx.ecx = 1337 - 42;
|
||||
#endif
|
||||
#elif SAFETYHOOK_OS_LINUX
|
||||
#if SAFETYHOOK_ARCH_X86_64
|
||||
ctx.rdi = 1337 - 42;
|
||||
#elif SAFETYHOOK_ARCH_X86_32
|
||||
ctx.edi = 1337 - 42;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
auto hook_result = SafetyHookMid::create(Target::add_42, Hook::add_42);
|
||||
|
||||
expect(hook_result.has_value());
|
||||
|
||||
hook = std::move(*hook_result);
|
||||
|
||||
expect(Target::add_42(1) == 1337_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(Target::add_42(2) == 44_i);
|
||||
};
|
||||
|
||||
#if SAFETYHOOK_ARCH_X86_64
|
||||
"Mid hook to change an XMM register"_test = [] {
|
||||
struct Target {
|
||||
SAFETYHOOK_NOINLINE static float SAFETYHOOK_FASTCALL add_42(float a) { return a + 0.42f; }
|
||||
};
|
||||
|
||||
expect(Target::add_42(0.0f) == 0.42_f);
|
||||
|
||||
static SafetyHookMid hook;
|
||||
|
||||
struct Hook {
|
||||
static void add_42(SafetyHookContext& ctx) { ctx.xmm0.f32[0] = 1337.0f - 0.42f; }
|
||||
};
|
||||
|
||||
auto hook_result = SafetyHookMid::create(Target::add_42, Hook::add_42);
|
||||
|
||||
expect(hook_result.has_value());
|
||||
|
||||
hook = std::move(*hook_result);
|
||||
|
||||
expect(Target::add_42(1.0f) == 1337.0_f);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(Target::add_42(2.0f) == 2.42_f);
|
||||
};
|
||||
#endif
|
||||
|
||||
"Mid hook enable and disable"_test = [] {
|
||||
struct Target {
|
||||
SAFETYHOOK_NOINLINE static int SAFETYHOOK_FASTCALL add_42(int a) {
|
||||
volatile int b = a;
|
||||
return b + 42;
|
||||
}
|
||||
};
|
||||
|
||||
expect(Target::add_42(0) == 42_i);
|
||||
expect(Target::add_42(1) == 43_i);
|
||||
expect(Target::add_42(2) == 44_i);
|
||||
|
||||
static SafetyHookMid hook;
|
||||
|
||||
struct Hook {
|
||||
static void add_42(SafetyHookContext& ctx) {
|
||||
#if SAFETYHOOK_OS_WINDOWS
|
||||
#if SAFETYHOOK_ARCH_X86_64
|
||||
ctx.rcx = 1337 - 42;
|
||||
#elif SAFETYHOOK_ARCH_X86_32
|
||||
ctx.ecx = 1337 - 42;
|
||||
#endif
|
||||
#elif SAFETYHOOK_OS_LINUX
|
||||
#if SAFETYHOOK_ARCH_X86_64
|
||||
ctx.rdi = 1337 - 42;
|
||||
#elif SAFETYHOOK_ARCH_X86_32
|
||||
ctx.edi = 1337 - 42;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
auto hook_result = SafetyHookMid::create(Target::add_42, Hook::add_42, SafetyHookMid::StartDisabled);
|
||||
|
||||
expect(hook_result.has_value());
|
||||
|
||||
hook = std::move(*hook_result);
|
||||
|
||||
expect(Target::add_42(0) == 42_i);
|
||||
expect(Target::add_42(1) == 43_i);
|
||||
expect(Target::add_42(2) == 44_i);
|
||||
|
||||
expect(hook.enable().has_value());
|
||||
|
||||
expect(Target::add_42(1) == 1337_i);
|
||||
expect(Target::add_42(2) == 1337_i);
|
||||
expect(Target::add_42(3) == 1337_i);
|
||||
|
||||
expect(hook.disable().has_value());
|
||||
|
||||
expect(Target::add_42(0) == 42_i);
|
||||
expect(Target::add_42(1) == 43_i);
|
||||
expect(Target::add_42(2) == 44_i);
|
||||
|
||||
hook.reset();
|
||||
|
||||
expect(Target::add_42(0) == 42_i);
|
||||
expect(Target::add_42(1) == 43_i);
|
||||
expect(Target::add_42(2) == 44_i);
|
||||
};
|
||||
};
|
||||
341
external/safetyhook/test/vmt_hook.cpp
vendored
Normal file
341
external/safetyhook/test/vmt_hook.cpp
vendored
Normal file
@@ -0,0 +1,341 @@
|
||||
#include <boost/ut.hpp>
|
||||
#include <safetyhook.hpp>
|
||||
|
||||
using namespace boost::ut;
|
||||
|
||||
#if SAFETYHOOK_OS_WINDOWS
|
||||
static constexpr auto VMT_OFFSET = 0;
|
||||
#elif SAFETYHOOK_OS_LINUX
|
||||
static constexpr auto VMT_OFFSET = 1;
|
||||
#endif
|
||||
|
||||
static suite<"vmt hook"> vmt_hook_tests = [] {
|
||||
"VMT hook an object instance"_test = [] {
|
||||
struct Interface {
|
||||
virtual ~Interface() = default;
|
||||
virtual int add_42(int a) = 0;
|
||||
};
|
||||
|
||||
struct Target : Interface {
|
||||
SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Interface> target = std::make_unique<Target>();
|
||||
|
||||
expect(target->add_42(0) == 42_i);
|
||||
|
||||
static SafetyHookVmt target_hook{};
|
||||
static SafetyHookVm add_42_hook{};
|
||||
|
||||
struct Hook : Target {
|
||||
int hooked_add_42(int a) { return add_42_hook.thiscall<int>(this, a) + 1337; }
|
||||
};
|
||||
|
||||
auto vmt_result = SafetyHookVmt::create(target.get());
|
||||
|
||||
expect(vmt_result.has_value());
|
||||
|
||||
target_hook = std::move(*vmt_result);
|
||||
|
||||
auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42);
|
||||
|
||||
expect(vm_result.has_value());
|
||||
|
||||
add_42_hook = std::move(*vm_result);
|
||||
|
||||
expect(target->add_42(1) == 1380_i);
|
||||
|
||||
add_42_hook.reset();
|
||||
|
||||
expect(target->add_42(2) == 44_i);
|
||||
};
|
||||
|
||||
"Resetting the VMT hook removes all VM hooks for that object"_test = [] {
|
||||
struct Interface {
|
||||
virtual ~Interface() = default;
|
||||
virtual int add_42(int a) = 0;
|
||||
virtual int add_43(int a) = 0;
|
||||
};
|
||||
|
||||
struct Target : Interface {
|
||||
SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; }
|
||||
SAFETYHOOK_NOINLINE int add_43(int a) override { return a + 43; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Interface> target = std::make_unique<Target>();
|
||||
|
||||
expect(target->add_42(0) == 42_i);
|
||||
expect(target->add_43(0) == 43_i);
|
||||
|
||||
static SafetyHookVmt target_hook{};
|
||||
static SafetyHookVm add_42_hook{};
|
||||
static SafetyHookVm add_43_hook{};
|
||||
|
||||
struct Hook : Target {
|
||||
int hooked_add_42(int a) { return add_42_hook.thiscall<int>(this, a) + 1337; }
|
||||
int hooked_add_43(int a) { return add_43_hook.thiscall<int>(this, a) + 1337; }
|
||||
};
|
||||
|
||||
auto vmt_result = SafetyHookVmt::create(target.get());
|
||||
|
||||
expect(vmt_result.has_value());
|
||||
|
||||
target_hook = std::move(*vmt_result);
|
||||
|
||||
auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42);
|
||||
|
||||
expect(vm_result.has_value());
|
||||
|
||||
add_42_hook = std::move(*vm_result);
|
||||
|
||||
expect(target->add_42(1) == 1380_i);
|
||||
|
||||
vm_result = target_hook.hook_method(2 + VMT_OFFSET, &Hook::hooked_add_43);
|
||||
|
||||
expect(vm_result.has_value());
|
||||
|
||||
add_43_hook = std::move(*vm_result);
|
||||
|
||||
expect(target->add_43(1) == 1381_i);
|
||||
|
||||
target_hook.reset();
|
||||
|
||||
expect(target->add_42(2) == 44_i);
|
||||
expect(target->add_43(2) == 45_i);
|
||||
};
|
||||
|
||||
"VMT hooking an object maintains correct RTTI"_test = [] {
|
||||
struct Interface {
|
||||
virtual ~Interface() = default;
|
||||
virtual int add_42(int a) = 0;
|
||||
};
|
||||
|
||||
struct Target : Interface {
|
||||
SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; }
|
||||
};
|
||||
|
||||
auto target = std::make_unique<Target>();
|
||||
|
||||
expect(target->add_42(0) == 42_i);
|
||||
expect(neq(dynamic_cast<Interface*>(target.get()), nullptr));
|
||||
|
||||
static SafetyHookVmt target_hook{};
|
||||
static SafetyHookVm add_42_hook{};
|
||||
|
||||
struct Hook : Target {
|
||||
int hooked_add_42(int a) { return add_42_hook.thiscall<int>(this, a) + 1337; }
|
||||
};
|
||||
|
||||
auto vmt_result = SafetyHookVmt::create(target.get());
|
||||
|
||||
expect(vmt_result.has_value());
|
||||
|
||||
target_hook = std::move(*vmt_result);
|
||||
|
||||
auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42);
|
||||
|
||||
expect(vm_result.has_value());
|
||||
|
||||
add_42_hook = std::move(*vm_result);
|
||||
|
||||
expect(target->add_42(1) == 1380_i);
|
||||
expect(neq(dynamic_cast<Interface*>(target.get()), nullptr));
|
||||
};
|
||||
|
||||
"Can safely destroy VmtHook after object is deleted"_test = [] {
|
||||
struct Interface {
|
||||
virtual ~Interface() = default;
|
||||
virtual int add_42(int a) = 0;
|
||||
};
|
||||
|
||||
struct Target : Interface {
|
||||
SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Interface> target = std::make_unique<Target>();
|
||||
|
||||
expect(target->add_42(0) == 42_i);
|
||||
|
||||
static SafetyHookVmt target_hook{};
|
||||
static SafetyHookVm add_42_hook{};
|
||||
|
||||
struct Hook : Target {
|
||||
int hooked_add_42(int a) { return add_42_hook.thiscall<int>(this, a) + 1337; }
|
||||
};
|
||||
|
||||
auto vmt_result = SafetyHookVmt::create(target.get());
|
||||
|
||||
expect(vmt_result.has_value());
|
||||
|
||||
target_hook = std::move(*vmt_result);
|
||||
|
||||
auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42);
|
||||
|
||||
expect(vm_result.has_value());
|
||||
|
||||
add_42_hook = std::move(*vm_result);
|
||||
|
||||
expect(target->add_42(1) == 1380_i);
|
||||
|
||||
target.reset();
|
||||
target_hook.reset();
|
||||
};
|
||||
|
||||
"Can apply an existing VMT hook to more than one object"_test = [] {
|
||||
struct Interface {
|
||||
virtual ~Interface() = default;
|
||||
virtual int add_42(int a) = 0;
|
||||
};
|
||||
|
||||
struct Target : Interface {
|
||||
SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Interface> target = std::make_unique<Target>();
|
||||
std::unique_ptr<Interface> target0 = std::make_unique<Target>();
|
||||
std::unique_ptr<Interface> target1 = std::make_unique<Target>();
|
||||
std::unique_ptr<Interface> target2 = std::make_unique<Target>();
|
||||
|
||||
expect(target->add_42(0) == 42_i);
|
||||
|
||||
static SafetyHookVmt target_hook{};
|
||||
static SafetyHookVm add_42_hook{};
|
||||
|
||||
struct Hook : Target {
|
||||
int hooked_add_42(int a) { return add_42_hook.thiscall<int>(this, a) + 1337; }
|
||||
};
|
||||
|
||||
auto vmt_result = SafetyHookVmt::create(target.get());
|
||||
|
||||
expect(vmt_result.has_value());
|
||||
|
||||
target_hook = std::move(*vmt_result);
|
||||
|
||||
auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42);
|
||||
|
||||
expect(vm_result.has_value());
|
||||
|
||||
add_42_hook = std::move(*vm_result);
|
||||
|
||||
target_hook.apply(target0.get());
|
||||
target_hook.apply(target1.get());
|
||||
target_hook.apply(target2.get());
|
||||
|
||||
expect(target->add_42(1) == 1380_i);
|
||||
expect(target0->add_42(1) == 1380_i);
|
||||
expect(target1->add_42(1) == 1380_i);
|
||||
expect(target2->add_42(1) == 1380_i);
|
||||
|
||||
add_42_hook.reset();
|
||||
|
||||
expect(target->add_42(2) == 44_i);
|
||||
expect(target0->add_42(2) == 44_i);
|
||||
expect(target1->add_42(2) == 44_i);
|
||||
expect(target2->add_42(2) == 44_i);
|
||||
};
|
||||
|
||||
"Can remove an object that was previously VMT hooked"_test = [] {
|
||||
struct Interface {
|
||||
virtual ~Interface() = default;
|
||||
virtual int add_42(int a) = 0;
|
||||
};
|
||||
|
||||
struct Target : Interface {
|
||||
SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Interface> target = std::make_unique<Target>();
|
||||
std::unique_ptr<Interface> target0 = std::make_unique<Target>();
|
||||
std::unique_ptr<Interface> target1 = std::make_unique<Target>();
|
||||
std::unique_ptr<Interface> target2 = std::make_unique<Target>();
|
||||
|
||||
expect(target->add_42(0) == 42_i);
|
||||
|
||||
static SafetyHookVmt target_hook{};
|
||||
static SafetyHookVm add_42_hook{};
|
||||
|
||||
struct Hook : Target {
|
||||
int hooked_add_42(int a) { return add_42_hook.thiscall<int>(this, a) + 1337; }
|
||||
};
|
||||
|
||||
auto vmt_result = SafetyHookVmt::create(target.get());
|
||||
|
||||
expect(vmt_result.has_value());
|
||||
|
||||
target_hook = std::move(*vmt_result);
|
||||
|
||||
auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42);
|
||||
|
||||
expect(vm_result.has_value());
|
||||
|
||||
add_42_hook = std::move(*vm_result);
|
||||
|
||||
target_hook.apply(target0.get());
|
||||
target_hook.apply(target1.get());
|
||||
target_hook.apply(target2.get());
|
||||
|
||||
expect(target->add_42(1) == 1380_i);
|
||||
expect(target0->add_42(1) == 1380_i);
|
||||
expect(target1->add_42(1) == 1380_i);
|
||||
expect(target2->add_42(1) == 1380_i);
|
||||
|
||||
target_hook.remove(target0.get());
|
||||
|
||||
expect(target->add_42(2) == 1381_i);
|
||||
expect(target0->add_42(2) == 44_i);
|
||||
expect(target1->add_42(2) == 1381_i);
|
||||
expect(target2->add_42(2) == 1381_i);
|
||||
|
||||
target_hook.remove(target2.get());
|
||||
|
||||
expect(target->add_42(2) == 1381_i);
|
||||
expect(target0->add_42(2) == 44_i);
|
||||
expect(target1->add_42(2) == 1381_i);
|
||||
expect(target2->add_42(2) == 44_i);
|
||||
|
||||
target_hook.remove(target.get());
|
||||
|
||||
expect(target->add_42(2) == 44_i);
|
||||
expect(target0->add_42(2) == 44_i);
|
||||
expect(target1->add_42(2) == 1381_i);
|
||||
expect(target2->add_42(2) == 44_i);
|
||||
|
||||
target_hook.remove(target1.get());
|
||||
|
||||
expect(target->add_42(2) == 44_i);
|
||||
expect(target0->add_42(2) == 44_i);
|
||||
expect(target1->add_42(2) == 44_i);
|
||||
expect(target2->add_42(2) == 44_i);
|
||||
};
|
||||
|
||||
"VMT hook an object instance with easy API"_test = [] {
|
||||
struct Interface {
|
||||
virtual ~Interface() = default;
|
||||
virtual int add_42(int a) = 0;
|
||||
};
|
||||
|
||||
struct Target : Interface {
|
||||
SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Interface> target = std::make_unique<Target>();
|
||||
|
||||
expect(target->add_42(0) == 42_i);
|
||||
|
||||
static SafetyHookVmt target_hook{};
|
||||
static SafetyHookVm add_42_hook{};
|
||||
|
||||
struct Hook : Target {
|
||||
int hooked_add_42(int a) { return add_42_hook.thiscall<int>(this, a) + 1337; }
|
||||
};
|
||||
|
||||
target_hook = safetyhook::create_vmt(target.get());
|
||||
add_42_hook = safetyhook::create_vm(target_hook, 1 + VMT_OFFSET, &Hook::hooked_add_42);
|
||||
|
||||
expect(target->add_42(1) == 1380_i);
|
||||
|
||||
add_42_hook.reset();
|
||||
|
||||
expect(target->add_42(2) == 44_i);
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user