Add testing framework and some regression tests, fix string casing regression in devmode, allow data_mods folder to be changed

This commit is contained in:
Will Toohey 2025-04-04 00:03:16 +10:00
parent 8bfa283a73
commit 4c9d6619f4
27 changed files with 468 additions and 188 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
*.zip
*.dll
build32/
build64/
dist/
subprojects/googletest*
subprojects/packagecache

View File

@ -118,6 +118,9 @@ folder.
Folders cannot have "," in their name if using allow/blocklist
--layered-logfile=filename.log
Use a custom, separate logfile instead of the game's log.
--layered-data-mods-folder=./some_folder
Use a custom mods folder instead of the default ./data_mods
MUST start with "./" to avoid path weirdness.
```
# Logs

BIN
avs2-core.dll Normal file

Binary file not shown.

View File

@ -9,3 +9,6 @@ system = 'windows'
cpu_family = 'x86'
cpu = 'i686'
endian = 'little'
[properties]
needs_exe_wrapper = false

View File

@ -9,3 +9,6 @@ system = 'windows'
cpu_family = 'x86_64'
cpu = 'x86_64'
endian = 'little'
[properties]
needs_exe_wrapper = false

89
ensure_xp_compatible.py Normal file
View File

@ -0,0 +1,89 @@
import argparse
import os
from glob import glob
from typing import TypeAlias
import pefile
parser = argparse.ArgumentParser()
parser.add_argument("dll_path")
args = parser.parse_args()
Imports: TypeAlias = dict[bytes, list[tuple[bytes | None, int | None]]]
Exports: TypeAlias = list[tuple[bytes | None, int | None]]
class MissingDLL(Exception):
pass
class MissingFunction(Exception):
pass
def load_imports(path: str) -> Imports:
print(f"Loading imports from {path}")
pe = pefile.PE(path)
imports: Imports = {}
total = 0
for entry in pe.DIRECTORY_ENTRY_IMPORT:
these = []
for imp in entry.imports:
these.append((imp.name, imp.ordinal))
imports[entry.dll.lower()] = these
total += len(these)
print(f" ...{total} imports from {len(imports)} DLLs")
return imports
def load_exports(path: str) -> Exports:
print(f"Loading exports from {path}...")
pe = pefile.PE(path)
exports: Exports = []
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
exports.append((exp.name, exp.ordinal))
print(f" ...{len(exports)} exports")
return exports
available_functions: dict[bytes, Exports] = {}
for dll in glob("./xp_dlls/*.dll"):
available_functions[os.path.basename(dll).lower().encode()] = load_exports(dll)
needed_functions = load_imports(args.dll_path)
# https://www.geoffchappell.com/studies/windows/win32/kernel32/api/index.htm
# my test .dll is 32-bit and these are only in 64
ignore_functions = [
b"RtlLookupFunctionEntry",
b"RtlUnwindEx",
b"RtlVirtualUnwind",
b"__C_specific_handler",
]
for dll_name, imports in needed_functions.items():
if (dll := available_functions.get(dll_name)) is None:
raise MissingDLL(f"Need {dll_name} but it's not in the XP DLLs folder")
for name, ordinal in imports:
if name is not None:
if name in ignore_functions:
continue
if next((fn for fn in dll if fn[0] == name), None) is None:
raise MissingFunction(f'Function "{name}" not present in XP {dll_name}')
elif ordinal is not None:
if next((fn for fn in dll if fn[1] == ordinal), None) is None:
raise MissingFunction(
f"Function with ordinal {ordinal} not present in XP {dll_name}"
)
else:
raise RuntimeError("Import with no name or ordinal???")
print("All functions present!")

View File

@ -1,6 +1,6 @@
project('layeredfs', 'c', 'cpp', version: '3.4',
default_options: [
'cpp_std=c++17',
'cpp_std=c++20',
'buildtype=release',
'strip=true',
'werror=true',
@ -66,10 +66,14 @@ layeredfs_cfg_dep = declare_dependency(
]
)
avs_standalone_lib = static_library('avs_standalone',
sources: 'src/avs_standalone.cpp'
)
executable('playpen',
sources: 'src/playpen.cpp',
build_by_default: false,
link_with: [layeredfs_lib, texbin_verbose_lib],
link_with: [layeredfs_lib, texbin_verbose_lib, avs_standalone_lib],
dependencies: layeredfs_cfg_dep,
)
@ -80,17 +84,10 @@ executable('texbin_debug',
dependencies: layeredfs_cfg_dep,
)
# "normal" hook
shared_library('ifs_hook',
link_with: [layeredfs_lib, texbin_lib],
dependencies: layeredfs_cfg_dep,
name_prefix: '',
install_dir: '/',
install: true,
)
# pre-configured DLLs if you don't know how to (or can't) add cmdline args
special_cfgs = [
# "normal" hook
['', []],
['always_verbose', ['-DCFG_VERBOSE']],
['always_logs_to_file', ['-DCFG_LOGFILE']],
['always_verbose_and_logs_to_file', ['-DCFG_VERBOSE','-DCFG_LOGFILE']],
@ -113,6 +110,8 @@ if host_machine.cpu_family() == 'x86'
)
endif
python = import('python').find_installation()
foreach cfg : special_cfgs
folder_name = cfg[0]
defines = cfg[1]
@ -121,14 +120,32 @@ foreach cfg : special_cfgs
continue
endif
shared_library('ifs_hook_' + folder_name,
lib_name = 'ifs_hook_' + folder_name
special_defines = []
install_dir = '/special_builds' / folder_name
if folder_name == ''
lib_name = 'ifs_hook'
install_dir = '/'
else
special_defines = [f'-DSPECIAL_VER="@folder_name@"']
endif
ifs_dll = shared_library(lib_name,
link_with: [layeredfs_lib, texbin_lib],
dependencies: layeredfs_cfg_dep,
cpp_args: [defines, f'-DSPECIAL_VER="@folder_name@"'],
cpp_args: [defines, special_defines],
name_prefix: '',
install_dir: '/special_builds' / folder_name,
install: true,
)
test(lib_name + '_is_xp',
python,
args: [
files('ensure_xp_compatible.py'),
ifs_dll,
],
workdir: meson.current_source_dir(),
)
endforeach
injector_cfgs = [
@ -165,3 +182,16 @@ foreach cfg : injector_cfgs
install: true,
)
endforeach
gtest_proj = subproject('gtest', required: false, default_options: {'default_library': 'static'})
gtest_main_dep = gtest_proj.get_variable('gtest_main_dep')
gmock_dep = gtest_proj.get_variable('gmock_dep')
test('tests', executable('tests_bin',
sources: 'src/tests.cpp',
link_with: [layeredfs_lib, texbin_lib, avs_standalone_lib],
dependencies: [layeredfs_cfg_dep, gtest_main_dep, gmock_dep],
build_by_default: false,
),
workdir: meson.current_source_dir(),
)

163
src/avs_standalone.cpp Normal file
View File

@ -0,0 +1,163 @@
#include "avs_standalone.hpp"
#include <windows.h>
#include "avs.h"
#include "hook.h"
#include "log.hpp"
namespace avs_standalone
{
typedef void (*avs_log_writer_t)(const char *chars, uint32_t nchars, void *ctx);
static LONG WINAPI exc_handler(_EXCEPTION_POINTERS *ExceptionInfo);
static size_t read_str(int32_t context, void *dst_buf, size_t count);
static void log_writer(const char *chars, uint32_t nchars, void *ctx);
#define FOREACH_EXTRA_FUNC(X) \
X("XCgsqzn0000129", void, avs_boot, node_t config, void *com_heap, size_t sz_com_heap, void *reserved, avs_log_writer_t log_writer, void *log_context) \
X("XCgsqzn000012a", void, avs_shutdown) \
X("XCgsqzn00000a1", node_t, property_search, property_t prop, node_t node, const char *path) \
X("XCgsqzn0000048", int, avs_fs_addfs, void *filesys) \
X("XCgsqzn0000159", void *, avs_filesys_ramfs)
#define AVS_FUNC_PTR(obfus_name, ret_type, name, ...) ret_type (*name)(__VA_ARGS__);
FOREACH_EXTRA_FUNC(AVS_FUNC_PTR)
static bool g_print_logs = true;
static size_t g_boot_cfg_offset;
#define DEFAULT_HEAP_SIZE 16777216
bool boot(bool _print_logs) {
AddVectoredExceptionHandler(1, exc_handler);
log_to_stdout();
if(!load_dll()) {
log_fatal("DLL load failed");
return false;
}
init_avs(); // fails because of Minhook not being initialised, don't care
auto avs_heap = malloc(DEFAULT_HEAP_SIZE);
g_boot_cfg_offset = 0;
int prop_len = property_read_query_memsize(read_str, 0, 0, 0);
if (prop_len <= 0) {
log_fatal("error reading config (size <= 0)");
return false;
}
auto buffer = malloc(prop_len);
auto avs_config = property_create(PROP_READ | PROP_WRITE | PROP_CREATE | PROP_APPEND, buffer, prop_len);
if (!avs_config) {
log_fatal("cannot create property");
return false;
}
g_boot_cfg_offset = 0;
if (!property_insert_read(avs_config, 0, read_str, 0)) {
log_fatal("avs-core", "cannot read property");
return false;
}
auto avs_config_root = property_search(avs_config, 0, "/config");
if(!avs_config_root) {
log_fatal("no root config node");
return false;
}
bool was_print = g_print_logs;
g_print_logs = _print_logs;
avs_boot(avs_config_root, avs_heap, DEFAULT_HEAP_SIZE, NULL, log_writer, NULL);
g_print_logs = was_print;
return true;
}
void shutdown(void) {
avs_shutdown();
}
#define LOAD_FUNC(obfus_name, ret_type, name, ...) \
if (!(name = (decltype(name))GetProcAddress(avs, obfus_name))) \
{ \
log_fatal("avs_standalone: couldn't get " #name); \
return false; \
}
bool load_dll(void)
{
auto avs = LoadLibraryA("avs2-core.dll");
if (!avs)
{
log_fatal("Playpen: Couldn't load avs dll");
return false;
}
FOREACH_EXTRA_FUNC(LOAD_FUNC);
return true;
}
void log_writer(const char *chars, uint32_t nchars, void *ctx)
{
// don't print noisy shutdown logs
auto prefix = "[----/--/-- --:--:--] ";
auto len = strlen(prefix);
if (strncmp(chars, prefix, len) == 0)
{
return;
}
if (g_print_logs)
{
fprintf(stderr, "%.*s", nchars, chars);
}
}
static const char *boot_cfg = R"(<?xml version="1.0" encoding="SHIFT_JIS"?>
<config>
<fs>
<nr_filesys __type="u16">16</nr_filesys>
<nr_mountpoint __type="u16">1024</nr_mountpoint>
<nr_mounttable __type="u16">32</nr_mounttable>
<nr_filedesc __type="u16">4096</nr_filedesc>
<link_limit __type="u16">8</link_limit>
<root>
<device>.</device>
<!--<option>posix=1</option>-->
</root>
<mounttable>
<vfs name="boot" fstype="fs" src="dev/raw" dst="/dev/raw" opt="vf=1,posix=1"/>
<vfs name="boot" fstype="fs" src="dev/nvram" dst="/dev/nvram" opt="vf=0,posix=1"/>
</mounttable>
</fs>
<log><level>misc</level></log>
<sntp>
<ea_on __type="bool">0</ea_on>
<servers></servers>
</sntp>
</config>
)";
static size_t read_str(int32_t context, void *dst_buf, size_t count)
{
memcpy(dst_buf, &boot_cfg[g_boot_cfg_offset], count);
g_boot_cfg_offset += count;
return count;
}
LONG WINAPI exc_handler(_EXCEPTION_POINTERS *ExceptionInfo) {
switch(ExceptionInfo->ExceptionRecord->ExceptionCode) {
case DBG_PRINTEXCEPTION_C:
break;
default:
fprintf(stderr, "Unhandled exception %lX\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
break;
}
return EXCEPTION_CONTINUE_SEARCH;
}
}

7
src/avs_standalone.hpp Normal file
View File

@ -0,0 +1,7 @@
namespace avs_standalone
{
bool boot(bool print_logs);
void shutdown(void);
bool load_dll(void);
}

View File

@ -6,12 +6,13 @@
#include "log.hpp"
#include "utils.hpp"
#define VERBOSE_FLAG "--layered-verbose"
#define DEVMODE_FLAG "--layered-devmode"
#define DISABLE_FLAG "--layered-disable"
#define ALLOWLIST_FLAG "--layered-allowlist"
#define BLOCKLIST_FLAG "--layered-blocklist"
#define LOGFILE_FLAG "--layered-logfile"
#define VERBOSE_FLAG "--layered-verbose"
#define DEVMODE_FLAG "--layered-devmode"
#define DISABLE_FLAG "--layered-disable"
#define ALLOWLIST_FLAG "--layered-allowlist"
#define BLOCKLIST_FLAG "--layered-blocklist"
#define LOGFILE_FLAG "--layered-logfile"
#define MOD_FOLDER_FLAG "--layered-data-mods-folder"
config_t config;
@ -46,6 +47,7 @@ void load_config(void) {
config.disable = false;
config.allowlist.clear();
config.blocklist.clear();
config.mod_folder = DEFAULT_MOD_FOLDER;
#ifdef CFG_VERBOSE
config.verbose_logs = true;
@ -91,16 +93,24 @@ void load_config(void) {
config.logfile = &path[1];
}
}
else if (strncmp(__argv[i], MOD_FOLDER_FLAG, strlen(MOD_FOLDER_FLAG)) == 0) {
std::string_view path = &__argv[i][strlen(MOD_FOLDER_FLAG)];
// correct format: --layered-data-mods-folder=./my_mods
if(path.starts_with("=./")) {
config.mod_folder = path.substr(1);
}
}
}
}
void print_config(void) {
log_info("Options: %s=%d %s=%d %s=%d %s=%s %s=%s %s=%s",
log_info("Options: %s=%d %s=%d %s=%d %s=%s %s=%s %s=%s %s=%s",
VERBOSE_FLAG, config.verbose_logs,
DEVMODE_FLAG, config.developer_mode,
DISABLE_FLAG, config.disable,
LOGFILE_FLAG, config.logfile,
ALLOWLIST_FLAG, allowlist,
BLOCKLIST_FLAG, blocklist
BLOCKLIST_FLAG, blocklist,
MOD_FOLDER_FLAG, config.mod_folder.c_str()
);
}

View File

@ -12,9 +12,13 @@ typedef struct {
const char *logfile;
std::set<std::string, CaseInsensitiveCompare> allowlist;
std::set<std::string, CaseInsensitiveCompare> blocklist;
std::string mod_folder;
} config_t;
#define DEFAULT_LOGFILE "ifs_hook.log"
#define DEFAULT_MOD_FOLDER "./data_mods"
#define CACHE_FOLDER (config.mod_folder + "/_cache")
extern config_t config;

View File

@ -225,7 +225,7 @@ void handle_texbin(HookFile &file) {
}
auto starting = file.get_path_to_open();
string out = CACHE_FOLDER "/" + file.norm_path;
string out = CACHE_FOLDER + "/" + file.norm_path;
auto out_hashed = out + ".hashed";
auto cache_hasher = CacheHasher(out_hashed);
@ -292,15 +292,15 @@ uint32_t handle_file_open(HookFile &file) {
file.mod_path = find_first_modfile(norm_copy);
}
if(string_ends_with(file.path, ".xml")) {
if(file.path.ends_with(".xml")) {
merge_xmls(file);
}
if(string_ends_with(file.path, ".bin")) {
if(file.path.ends_with(".bin")) {
handle_texbin(file);
}
if (string_ends_with(file.path, "texturelist.xml")) {
if (file.path.ends_with("texturelist.xml")) {
parse_texturelist(file);
}
else {

View File

@ -39,7 +39,7 @@ typedef struct image {
string ifs_mod_path;
int width;
int height;
const string cache_folder() { return CACHE_FOLDER "/" + ifs_mod_path; }
const string cache_folder() { return CACHE_FOLDER + "/" + ifs_mod_path; }
const string cache_file() { return cache_folder() + "/" + name_md5; };
} image_t;
@ -296,7 +296,7 @@ void parse_texturelist(HookFile &file) {
}
if (prop_was_rewritten) {
string outfolder = CACHE_FOLDER "/" + ifs_mod_path;
string outfolder = CACHE_FOLDER + "/" + ifs_mod_path;
if (!mkdir_p(outfolder)) {
log_warning("Couldn't create cache folder");
}
@ -459,7 +459,7 @@ void merge_xmls(HookFile &file) {
return;
auto starting = file.get_path_to_open();
out = CACHE_FOLDER "/" + file.norm_path;
out = CACHE_FOLDER + "/" + file.norm_path;
auto out_hashed = out + ".hashed";
auto cache_hasher = CacheHasher(out_hashed);

View File

@ -125,7 +125,7 @@ optional<string> normalise_path(const string &_path) {
vector<string> available_mods() {
vector<string> ret;
string mod_root = MOD_FOLDER "/";
string mod_root = config.mod_folder + "/";
// just pretend we have no mods at all
if (config.disable) {
@ -134,7 +134,7 @@ vector<string> available_mods() {
if (config.developer_mode) {
static bool first_search = true;
for (auto folder : folders_in_folder(MOD_FOLDER)) {
for (auto folder : folders_in_folder(config.mod_folder.c_str())) {
if (!strcasecmp(folder.c_str(), "_cache")) {
continue;
}
@ -218,7 +218,7 @@ optional<string> find_first_modfile(const string &norm_path) {
for (auto &dir : available_mods()) {
auto mod_path = dir + "/" + norm_path;
if (file_exists(mod_path.c_str())) {
return mod_path;
return path_to_actual_case(mod_path);
}
}
}
@ -233,7 +233,7 @@ optional<string> find_first_modfolder(const string &norm_path) {
for (auto &dir : available_mods()) {
auto mod_path = dir + "/" + norm_path;
if (folder_exists(mod_path.c_str())) {
return mod_path;
return path_to_actual_case(mod_path) + "/";
}
}
}

View File

@ -14,9 +14,6 @@ using std::optional;
using std::string;
using std::vector;
#define MOD_FOLDER "./data_mods"
#define CACHE_FOLDER MOD_FOLDER "/_cache"
void init_modpath_handler(void);
void cache_mods(void);
vector<string> available_mods();

View File

@ -9,27 +9,8 @@
#include <fstream>
void boot_avs(void);
bool load_dll(void);
LONG WINAPI exc_handler(_EXCEPTION_POINTERS *ExceptionInfo);
typedef void (*avs_log_writer_t)(const char *chars, uint32_t nchars, void *ctx);
#define FOREACH_EXTRA_FUNC(X) \
X("XCgsqzn0000129", void, avs_boot, node_t config, void *com_heap, size_t sz_com_heap, void *reserved, avs_log_writer_t log_writer, void *log_context) \
X("XCgsqzn000012a", void, avs_shutdown) \
X("XCgsqzn00000a1", node_t, property_search, property_t prop, node_t node, const char *path) \
X("XCgsqzn0000048", int, avs_fs_addfs, void* filesys) \
X("XCgsqzn0000159", void*, avs_filesys_ramfs) \
#define AVS_FUNC_PTR(obfus_name, ret_type, name, ...) ret_type (* name )( __VA_ARGS__ );
FOREACH_EXTRA_FUNC(AVS_FUNC_PTR)
static bool print_logs = true;
#define QUIET_BOOT
#include "texbin.hpp"
#include "avs_standalone.hpp"
#define log_assert(cond) if(!(cond)) {log_fatal("Assertion failed:" #cond);}
@ -219,7 +200,7 @@ FAIL:
if (prop_buffer)
free(prop_buffer);*/
/*auto d = avs_fs_opendir(MOD_FOLDER);
/*auto d = avs_fs_opendir(config.mod_folder.c_str());
if (!d) {
log_warning("couldn't d");
return;
@ -296,125 +277,16 @@ void textypes() {
}
int main(int argc, char** argv) {
// textypes();
AddVectoredExceptionHandler(1, exc_handler);
log_to_stdout();
if(!load_dll()) {
log_fatal("DLL load failed");
if(!avs_standalone::boot(false)) {
log_fatal("avs_standalone boot failed");
return 1;
}
init_avs(); // fails because of Minhook not being initialised, don't care
#ifdef QUIET_BOOT
print_logs = false;
boot_avs();
print_logs = true;
#else
boot_avs();
#endif
init(); // this double-hooks some AVS funcs, don't care
avs_playpen();
avs_shutdown();
avs_standalone::shutdown();
return 0;
}
#define DEFAULT_HEAP_SIZE 16777216
void log_writer(const char *chars, uint32_t nchars, void *ctx) {
// don't print noisy shutdown logs
auto prefix = "[----/--/-- --:--:--] ";
auto len = strlen(prefix);
if(strncmp(chars, prefix, len) == 0) {
return;
}
if(print_logs) {
fprintf(stderr, "%.*s", nchars, chars);
}
}
static const char *boot_cfg = R"(<?xml version="1.0" encoding="SHIFT_JIS"?>
<config>
<fs>
<nr_filesys __type="u16">16</nr_filesys>
<nr_mountpoint __type="u16">1024</nr_mountpoint>
<nr_mounttable __type="u16">32</nr_mounttable>
<nr_filedesc __type="u16">4096</nr_filedesc>
<link_limit __type="u16">8</link_limit>
<root><device>.</device></root>
<mounttable>
<vfs name="boot" fstype="fs" src="dev/raw" dst="/dev/raw" opt="vf=1,posix=1"/>
<vfs name="boot" fstype="fs" src="dev/nvram" dst="/dev/nvram" opt="vf=0,posix=1"/>
</mounttable>
</fs>
<log><level>misc</level></log>
<sntp>
<ea_on __type="bool">0</ea_on>
<servers></servers>
</sntp>
</config>
)";
static size_t off;
static size_t read_str(int32_t context, void *dst_buf, size_t count) {
memcpy(dst_buf, &boot_cfg[off], count);
off += count;
return count;
}
#define LOAD_FUNC(obfus_name, ret_type, name, ...) \
if(!(name = (decltype(name))GetProcAddress(avs, obfus_name))) {\
log_fatal("Playpen: couldn't get " #name); \
return false; \
}
bool load_dll(void) {
auto avs = LoadLibraryA("avs2-core.dll");
if(!avs) {
log_fatal("Playpen: Couldn't load avs dll");
return false;
}
FOREACH_EXTRA_FUNC(LOAD_FUNC);
return true;
}
void boot_avs(void) {
auto avs_heap = malloc(DEFAULT_HEAP_SIZE);
off = 0;
int prop_len = property_read_query_memsize(read_str, 0, 0, 0);
if (prop_len <= 0) {
log_fatal("error reading config (size <= 0)");
return;
}
auto buffer = malloc(prop_len);
auto avs_config = property_create(PROP_READ | PROP_WRITE | PROP_CREATE | PROP_APPEND, buffer, prop_len);
if (!avs_config) {
log_fatal("cannot create property");
return;
}
off = 0;
if (!property_insert_read(avs_config, 0, read_str, 0)) {
log_fatal("avs-core", "cannot read property");
return;
}
auto avs_config_root = property_search(avs_config, 0, "/config");
if(!avs_config_root) {
log_fatal("no root config node");
return;
}
avs_boot(avs_config_root, avs_heap, DEFAULT_HEAP_SIZE, NULL, log_writer, NULL);
}
LONG WINAPI exc_handler(_EXCEPTION_POINTERS *ExceptionInfo) {
fprintf(stderr, "Unhandled exception %lX\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
return EXCEPTION_CONTINUE_SEARCH;
}

View File

@ -64,7 +64,7 @@ static CriticalSectionLock mangling_mtx;
static void ramfs_demangler_demangle_if_possible_nolock(std::string& raw_path);
void ramfs_demangler_on_fs_open(const std::string& path, AVS_FILE open_result) {
if (open_result < 0 || !string_ends_with(path.c_str(), ".ifs")) {
if (open_result < 0 || !path.ends_with(".ifs")) {
return;
}
@ -162,12 +162,11 @@ void ramfs_demangler_on_fs_mount(const char* mountpoint, const char* fsroot, con
cleanup->second.mounted_path = mountpoint;
}
}
else if(string_ends_with(fsroot, ".ifs")) {
else if(string root = fsroot; root.ends_with(".ifs")) {
// this fixes ifs-inside-ifs by demangling the root location too
string root = (string)fsroot;
ramfs_demangler_demangle_if_possible_nolock(root);
log_verbose("imagefs mount mapped to %s", root.c_str());
mangling_map[mountpoint] = root;
mangling_map[mountpoint] = root;
}
}

60
src/tests.cpp Normal file
View File

@ -0,0 +1,60 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "config.hpp"
#include "hook.h"
#include "avs_standalone.hpp"
#include "modpath_handler.h"
using ::testing::Contains;
using ::testing::Optional;
class Environment : public ::testing::Environment {
public:
~Environment() override {}
// Override this to define how to set up the environment.
void SetUp() override {
ASSERT_TRUE(avs_standalone::boot(false));
ASSERT_EQ(init(), 0);
config.mod_folder = "./testcases_data_mods";
print_config();
cache_mods();
ASSERT_THAT(available_mods(), Contains(config.mod_folder + "/empty"));
}
// Override this to define how to tear down the environment.
void TearDown() override {
avs_standalone::shutdown();
}
};
testing::Environment* const foo_env =
testing::AddGlobalTestEnvironment(new Environment);
class DevModeOnOff : public testing::TestWithParam<bool> {
void SetUp() override {
config.developer_mode = GetParam();
}
void TearDown() override {
config.developer_mode = false;
}
};
INSTANTIATE_TEST_SUITE_P(DevModeOnOffInstance, DevModeOnOff, testing::Bool());
TEST_P(DevModeOnOff, MissingFileNullopt) {
ASSERT_EQ(find_first_modfile("doesn't exist"), std::nullopt);
}
TEST_P(DevModeOnOff, CaseInsensitiveFiles) {
EXPECT_THAT(find_first_modfile("OhNo/oWo"), Optional(config.mod_folder + "/Case_Sensitive/OhNO/oWo"));
EXPECT_THAT(find_first_modfile("ohno/owo"), Optional(config.mod_folder + "/Case_Sensitive/OhNO/oWo"));
}
TEST_P(DevModeOnOff, CaseInsensitiveFolders) {
EXPECT_THAT(find_first_modfolder("OhNO"), Optional(config.mod_folder + "/Case_Sensitive/OhNO/"));
EXPECT_THAT(find_first_modfolder("ohno"), Optional(config.mod_folder + "/Case_Sensitive/OhNO/"));
}

View File

@ -19,19 +19,6 @@ char* snprintf_auto(const char* fmt, ...) {
return s;
}
bool string_ends_with(const char * str, const char * suffix) {
size_t str_len = strlen(str);
size_t suffix_len = strlen(suffix);
return
(str_len >= suffix_len) &&
(0 == _stricmp(str + (str_len - suffix_len), suffix));
}
bool string_ends_with(const std::string &str, const char * suffix) {
return string_ends_with(str.c_str(), suffix);
}
void string_replace(std::string &str, const char* from, const char* to) {
auto to_len = strlen(to);
auto from_len = strlen(from);
@ -119,6 +106,35 @@ bool folder_exists(const char* name) {
return true;
}
std::string path_to_actual_case(std::string path) {
WIN32_FIND_DATAA ffd;
HANDLE hFind;
size_t start = std::string::npos;
while((start = path.rfind('/', start)) != string::npos) {
hFind = FindFirstFileA(path.c_str(), &ffd);
if (hFind == INVALID_HANDLE_VALUE) {
continue;
}
FindClose(hFind);
auto segment = &path[start+1];
size_t segment_len = strlen(segment);
if(strcasecmp(segment, ffd.cFileName) == 0)
memcpy(segment, ffd.cFileName, segment_len);
path[start] = '\0';
}
// restore slashes
for(auto& c: path) {
if(c == '\0')
c = '/';
}
return path;
}
void str_toupper_inline(std::string& str) {
for (size_t i = 0; i < str.length(); i++) {
str[i] = toupper(str[i]);

View File

@ -12,8 +12,6 @@
#define lenof(x) (sizeof(x) / sizeof(*x))
char* snprintf_auto(const char* fmt, ...);
bool string_ends_with(const char * str, const char * suffix);
bool string_ends_with(const std::string &str, const char * suffix);
// case insensitive
void string_replace(std::string &str, const char* from, const char* to);
// // case insensitive
@ -24,6 +22,11 @@ wchar_t *str_widen(const char *src);
void str_toupper_inline(std::string &str);
bool file_exists(const char* name);
bool folder_exists(const char* name);
// the given path:
// - must be known to exist
// - must start with "/" or "./"
// - must not end with "/"
std::string path_to_actual_case(std::string path);
std::vector<std::string> folders_in_folder(const char* root);
uint64_t file_time(const char* path);
LONG time(void);

16
subprojects/gtest.wrap Normal file
View File

@ -0,0 +1,16 @@
[wrap-file]
directory = googletest-1.15.2
source_url = https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz
source_filename = gtest-1.15.2.tar.gz
source_hash = 7b42b4d6ed48810c5362c265a17faebe90dc2373c885e5216439d37927f02926
patch_filename = gtest_1.15.2-4_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.15.2-4/get_patch
patch_hash = a5151324b97e6a98fa7a0e8095523e6d5c4bb3431210d6ac4ad9800c345acf40
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.15.2-4/gtest-1.15.2.tar.gz
wrapdb_version = 1.15.2-4
[provide]
gtest = gtest_dep
gtest_main = gtest_main_dep
gmock = gmock_dep
gmock_main = gmock_main_dep

3
test.sh Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
meson test -C build64 --print-errorlogs "$@"

View File

1
xp_dlls/README.md Normal file
View File

@ -0,0 +1 @@
Taken from my Jubeat cab's XP image

BIN
xp_dlls/kernel32.dll Normal file

Binary file not shown.

BIN
xp_dlls/msvcrt.dll Normal file

Binary file not shown.