0
\$\begingroup\$

https://github.com/speedrun-program/load-extender

This is the previous version: lengthening the time it takes to access files REVISION #2

To compile this, you need to download and include robin_hood.h, and on Windows you also need to install EasyHook.

I made this because it helps us save non-load time in a speedrun.

In this version, it uses unique_ptr int arrays instead of a class, it has a way to include leading and trailing spaces in file names, and it has a way to deal with if there’s already a file named files_and_delays.txt. I also made it so the txt files are in utf-16 LE on Windows because that’s how file paths are encoded on that OS.

load_extender.cpp

#include <chrono>
#include <thread>
#include <fstream>
#include <string>
#include <vector>
#include <mutex>
#include <filesystem>
#include <memory>
#include <climits>

#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
#define THIS_IS_NOT_THE_TEST_PROGRAM
#endif

#ifdef _WIN32
using wstring_or_string = std::wstring;
#include <Windows.h>
// easyhook.h installed with NuGet
// https://easyhook.github.io/documentation.html
#include <easyhook.h>
#else
using wstring_or_string = std::string;
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <dlfcn.h>
#endif

#include "robin_hood.h"
#include "other_functions.h"
// thank you for making this hash map, martinus.
// here's his github: https://github.com/martinus/robin-hood-hashing

static robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>> rh_map;
static std::mutex delay_array_mutex = read_delays_file(rh_map);

#ifdef _WIN32
static NTSTATUS WINAPI NtCreateFileHook(
    PHANDLE            FileHandle,
    ACCESS_MASK        DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PIO_STATUS_BLOCK   IoStatusBlock,
    PLARGE_INTEGER     AllocationSize,
    ULONG              FileAttributes,
    ULONG              ShareAccess,
    ULONG              CreateDisposition,
    ULONG              CreateOptions,
    PVOID              EaBuffer,
    ULONG              EaLength)
{
    const std::wstring& file_path = ObjectAttributes->ObjectName->Buffer;
    // +1 so '\' isn't included. If '\' isn't found, the whole wstring is checked because npos + 1 is 0
    const std::wstring& file_name = file_path.substr(file_path.rfind(L"\\") + 1);
    delay_file(rh_map, file_name, delay_array_mutex);
    return NtCreateFile(
        FileHandle,
        DesiredAccess,
        ObjectAttributes,
        IoStatusBlock,
        AllocationSize,
        FileAttributes,
        ShareAccess,
        CreateDisposition,
        CreateOptions,
        EaBuffer,
        EaLength
    );
}

extern "C" void __declspec(dllexport) __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO * inRemoteInfo);

void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* inRemoteInfo)
{
    HOOK_TRACE_INFO hHook1 = {NULL};
    LhInstallHook(
        GetProcAddress(GetModuleHandle(TEXT("ntdll")), "NtCreateFile"),
        NtCreateFileHook,
        NULL,
        &hHook1
    );
    ULONG ACLEntries[1] = {0};
    LhSetExclusiveACL(ACLEntries, 1, &hHook1);
}
#else
static auto original_fopen = reinterpret_cast<FILE * (*)(const char* path, const char* mode)>(dlsym(RTLD_NEXT, "fopen"));

FILE* fopen(const char* path, const char* mode)
{
    // finding part of path which shows file name
    int file_name_index = -1; // increases to 0 and checks entire path if no slash is found
    for (int i = 0; path[i] != '\0'; i++)
    {
        if (path[i] == '/')
        {
            file_name_index = i;
        }
    }
    const std::string file_name(&path[file_name_index + 1]);
    delay_file(rh_map, file_name, delay_array_mutex);
    return original_fopen(path, mode);
}
#endif

load_extender_test.cpp

#include <mutex>
#include <memory>
#include <fstream>
#include <string>
#include <vector>
#include <filesystem>
#include <climits>
#include "robin_hood.h"
// thank you for making this hash map, martinus.
// here's his github: https://github.com/martinus/robin-hood-hashing

#ifdef _WIN32
using wstring_or_string = std::wstring; // file paths are UTF-16LE on Windows
#else
using wstring_or_string = std::string;
#endif

#include "other_functions.h"

static void print_rh_map(robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>& rh_map)
{
    printf("\n---------- hash map current state ----------\n");
    for (auto& it : rh_map)
    {
#ifdef _WIN32
        printf("%ls /", it.first.c_str());
#else
        printf("%s /", it.first.c_str());
#endif
        int i = 1; // used after loop to check if -1 is at the end
        for (; it.second[i] >= 0; i++)
        {
            printf(" %d", it.second[i]);
        }
        if (it.second[i] == -1)
        {
            if (i == 1)
            {
                printf(" RESET ALL");
            }
            else
            {
                printf(" REPEAT");
            }
        }
        printf(": INDEX %d\n", it.second[0]);
    }
    printf("---------------------------------------------\n\n");
}

static void test_all_inputs()
{
#ifdef _WIN32
    wchar_t path_separator = L'\\';
#else
    char path_separator = '/';
#endif
    wstring_or_string test_path;
    std::ifstream input_test_file;
    input_test_file.open("test_input.txt", std::ios::binary);
    if (input_test_file.fail())
    {
        printf("couldn't open test_input.txt\n");
        return;
    }
    std::string file_content(
        (std::istreambuf_iterator<char>(input_test_file)),
        (std::istreambuf_iterator<char>())
    );
    if (file_content.length() > 0 && file_content[file_content.length() - 1] == '\n')
    {
        file_content.erase(file_content.length() - 1);
    }
    input_test_file.close();
#ifdef _WIN32
    if (file_content.length() < 2)
    {
        printf("test_input.txt byte order mark is missing\nsave test_input.txt as UTF-16 LE\n");
        return;
    }
    size_t start_index = 2; // first two bytes are BOM bytes
#else
    size_t start_index = 0;
#endif
    size_t stop_index = 0;
    robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>> rh_map;
    std::mutex delay_array_mutex = read_delays_file(rh_map);
    print_rh_map(rh_map);
    do // go through each line
    {
#ifdef _WIN32
        stop_index = file_content.find("\n\0", start_index);
        if (stop_index != std::string::npos)
        {
            // - 1 because of carriage return character
            test_path = string_to_wstring(file_content.substr(start_index, stop_index - start_index - 1));
        }
        else
        {
            test_path = string_to_wstring(file_content.substr(start_index));
        }
        start_index = stop_index + 2; // + 2 to go past newline character and null character
        printf("testing path: %ls\n", test_path.c_str());
#else
        stop_index = file_content.find("\n", start_index);
        if (stop_index != std::string::npos)
        {
            test_path = file_content.substr(start_index, stop_index - start_index);
        }
        else
        {
            test_path = file_content.substr(start_index);
        }
        start_index = stop_index + 1; // + 1 to go past newline character
        printf("testing path: %s\n", test_path.c_str());
#endif
        // +1 so separator isn't included
        // If separator isn't found, the whole wstring is checked because npos + 1 is 0
        const wstring_or_string& test_file = &test_path[test_path.rfind(path_separator) + 1];
        delay_file(rh_map, test_file, delay_array_mutex);
        print_rh_map(rh_map);
        test_path.clear();
    }
    while (stop_index != std::string::npos);
}

int main(int argc, char* argv[])
{
    printf("\ntest start\n");
    test_all_inputs();
    printf("test finished, press Enter to exit\n");
    std::string x;
    std::getline(std::cin, x);
    return 0;
}

other_functions.h

#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
#include <iostream>
#endif

#ifdef _WIN32
static std::wstring string_to_wstring(std::string line)
{
    std::wstring ws_line;
    for (size_t i = 1; i < line.length(); i += 2)
    {
        ws_line.push_back((line[i - 1]) + (line[i] << 8));
    }
    return ws_line;
}
#endif

static void remove_start_and_end_whitespace(wstring_or_string& file_name)
{
    wstring_or_string white_space = {9, 11, 12, 13, 32}; // whitespace character values
    size_t first_not_whitespace = file_name.find_first_not_of(white_space);
    size_t last_not_whitespace = file_name.find_last_not_of(white_space);
    if (first_not_whitespace == std::string::npos)
    {
        file_name.erase(0);
    }
    else
    {
        file_name = file_name.substr(first_not_whitespace, last_not_whitespace - first_not_whitespace + 1);
    }
}

static void set_key_and_value(robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>& rh_map, wstring_or_string& line)
{
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
    static bool too_big_delay_seen = false; // this is used so the message will only be printed once
#endif
    // 47 is forward slash, 45 is dash character, 10 is newline
    // numbers used so it will work with both string and wstring
    int current_delay = 0;
    size_t current_index = (line.length() > 0 && line[0] == 47); // start at 1 if first character is forward slash
    wstring_or_string file_name;
    std::vector<int> delay_sequence;
    delay_sequence.push_back(1); // first index keeps track of next index to get delay time from
    // get file name
    for (; current_index < line.length() && line[current_index] != 47 && line[current_index] != 10; current_index++)
    {
        file_name.push_back(line[current_index]);
    }
    if (line[0] != 47) // remove leading and trailing whitespace if first character wasn't forward slash
    {
        remove_start_and_end_whitespace(file_name);
    }
    // get delay sequence
    for (current_index++; current_index < line.length() && line[current_index] != 10; current_index++)
    {
        if (line[current_index] >= 48 && line[current_index] <= 57) // character is a number
        {
            int current_digit = line[current_index] - 48;
            // avoid going over INT_MAX
            if (((INT_MAX / 10) > current_delay) || ((INT_MAX / 10) == current_delay && (INT_MAX % 10) >= current_digit))
            {
                current_delay = ((current_delay * 10) + current_digit);
            }
            else
            {
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
                if (!too_big_delay_seen)
                {
                    too_big_delay_seen = true;
                    printf("delay time can only be %d milliseconds\n", INT_MAX);
                }
#endif
                current_delay = INT_MAX;
            }
        }
        else if (line[current_index] == 47)
        {
            delay_sequence.push_back(current_delay);
            current_delay = 0;
        }
        else if (line[current_index] == 45)
        {
            delay_sequence.push_back(-1); // sequence starts over after reaching the end
            current_delay = 0;
            break;
        }
    }
    if (current_delay != 0)
    {
        delay_sequence.push_back(current_delay);
    }
    if (delay_sequence[delay_sequence.size() - 1] != -1)
    {
        delay_sequence.push_back(-2); // sequence ends without starting over
    }
    if (delay_sequence[1] != -2) // no delay sequence written on this line
    {
        std::unique_ptr<int[]> delay_sequence_unique_ptr = std::make_unique<int[]>(delay_sequence.size());
        // copy delays into std::unique_ptr<int[]>
        for (size_t i = 0; i < delay_sequence.size() && i < INT_MAX; i++)
        {
            delay_sequence_unique_ptr[i] = delay_sequence[i];
        }
        // delay sequence can't be longer that INT_MAX because first index keeps track of next index to get delay time from
        if ((delay_sequence.size() > INT_MAX) || ((delay_sequence.size() == INT_MAX) && (delay_sequence[INT_MAX] >= 0)))
        {
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
            printf("only %d delays can be stored in a delay sequence\n", INT_MAX - 2);
#endif
            delay_sequence_unique_ptr[INT_MAX] = -2;
        }
        rh_map[wstring_or_string(file_name)] = std::move(delay_sequence_unique_ptr);
    }
}

static std::mutex read_delays_file(robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>& rh_map)
{
    std::ifstream delays_file;
    std::string txt_file_name = "files_and_delays.txt";
    // find files_and_delays.txt file with highest number at the end
    // this is used in case the directory already has a file named files_and_delays.txt
    for (int file_number = 0; std::filesystem::exists("files_and_delays" + std::to_string(file_number) + ".txt"); file_number++)
    {
        txt_file_name = "files_and_delays" + std::to_string(file_number) + ".txt";
    }
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
    printf("opening %s\n", txt_file_name.c_str());
#endif
    delays_file.open(txt_file_name, std::ios::binary);
    if (delays_file.fail())
    {
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
        printf("couldn't open %s\n", txt_file_name.c_str());
#endif
        return std::mutex();
    }
    std::string file_content(
        (std::istreambuf_iterator<char>(delays_file)),
        (std::istreambuf_iterator<char>())
    );
    delays_file.close();
#ifdef _WIN32
    if (file_content.length() < 2)
    {
        printf("files_and_delays.txt byte order mark is missing\nsave files_and_delays.txt as UTF-16 LE\n");
        return std::mutex();
    }
    size_t start_index = 2; // first two bytes are BOM bytes
#else
    size_t start_index = 0;
#endif
    size_t stop_index = 0;
    do // go through each line
    {
#ifdef _WIN32
        stop_index = file_content.find("\n\0", start_index);
        std::wstring line = L"";
        if (stop_index != std::string::npos)
        {
            // - 1 because of carriage return character
            line = string_to_wstring(file_content.substr(start_index, stop_index - start_index - 1));
        }
        else
        {
            line = string_to_wstring(file_content.substr(start_index));
        }
        start_index = stop_index + 2;
#else
        stop_index = file_content.find("\n", start_index);
        std::string line = "";
        if (stop_index != std::string::npos)
        {
            line = file_content.substr(start_index, stop_index - start_index);
        }
        else
        {
            line = file_content.substr(start_index);
        }
        start_index = stop_index + 1;
#endif
        set_key_and_value(rh_map, line);
    }
    while (stop_index != std::string::npos);
    return std::mutex();
}

void delay_file(robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>& rh_map, const wstring_or_string& file_name, std::mutex& delay_array_mutex)
{
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
#ifdef _WIN32
    char print_format[] = "%ls";
#else
    char print_format[] = "%s";
#endif
#endif
    robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>::iterator rh_map_iter = rh_map.find(file_name);
    if (rh_map_iter != rh_map.end())
    {
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
        printf(print_format, file_name.c_str());
        printf(" found in hash map\n");
#endif
        int delay = 0;
        {
            std::lock_guard<std::mutex> delay_array_mutex_lock(delay_array_mutex);
            std::unique_ptr<int[]>& delay_sequence = rh_map_iter->second;
            if (delay_sequence[1] == -1)
            {
                for (auto& it : rh_map)
                {
                    it.second[0] = 1;
                }
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
                printf("all delay sequences reset\n");
#endif
            }
            else if (delay_sequence[delay_sequence[0]] == -1)
            {
                delay_sequence[0] = 1;
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
                printf(print_format, file_name.c_str());
                printf(" delay sequence reset\n");
#endif
            }
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
            else if (delay_sequence[delay_sequence[0]] == -2)
            {
                printf(print_format, file_name.c_str());
                printf(" delay sequence already finished\n");
            }
#endif
            delay = delay_sequence[delay_sequence[0]];
            delay_sequence[0] += (delay_sequence[delay_sequence[0]] >= 0); // if it's -1 or -2, it's at the end already
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
            delay *= (delay >= 0); // it might be set to -1 or -2
            printf(print_format, file_name.c_str());
            printf(" access delayed for %d second(s) and %d millisecond(s)\n", delay / 1000, delay % 1000);
#endif
        }
#ifdef THIS_IS_NOT_THE_TEST_PROGRAM
        if (delay > 0) // it might be set to -1 or -2
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(delay));
        }
#endif
    }
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
    else
    {
        printf(print_format, file_name.c_str());
        printf(" not found in hash map\n");
    }
#endif
}

load_extender_injector.cpp (only used on Windows because LD_PRELOAD and DYLD_INSERT_LIBRARIES can be used on Linux and Mac OS)

#include <tchar.h>
#include <iostream>
#include <string>
#include <Windows.h>
// easyhook.h installed with NuGet
// https://easyhook.github.io/documentation.html
#include <easyhook.h>

void get_exit_input()
{
    std::wcout << "Press Enter to exit";
    std::wstring input;
    std::getline(std::wcin, input);
    std::getline(std::wcin, input);
}

int _tmain(int argc, _TCHAR* argv[])
{
    WCHAR* dllToInject32 = NULL;
    WCHAR* dllToInject64 = NULL;
    LPCWSTR lpApplicationName = argv[0];
    DWORD lpBinaryType;
    if (GetBinaryType(lpApplicationName, &lpBinaryType) == 0 || (lpBinaryType != 0 && lpBinaryType != 6))
    {
        std::wcout << "ERROR: This exe wasn't identified as 32-bit or as 64-bit";
        get_exit_input();
        return 1;
    }
    else if (lpBinaryType == 0)
    {
        dllToInject32 = (WCHAR*)L"load_extender_32.dll";
    }
    else
    {
        dllToInject64 = (WCHAR*)L"load_extender_64.dll";
    }
    DWORD processId;
    std::wcout << "Enter the target process Id: ";
    std::cin >> processId;
    wprintf(L"Attempting to inject dll\n\n");
    // Inject dllToInject into the target process Id, passing 
    // freqOffset as the pass through data.
    NTSTATUS nt = RhInjectLibrary(
        processId,   // The process to inject into
        0,           // ThreadId to wake up upon injection
        EASYHOOK_INJECT_DEFAULT,
        dllToInject32, // 32-bit
        dllToInject64, // 64-bit
        NULL, // data to send to injected DLL entry point
        0 // size of data to send
    );
    if (nt != 0)
    {
        printf("RhInjectLibrary failed with error code = %d\n", nt);
        PWCHAR err = RtlGetLastErrorString();
        std::wcout << err << "\n";
        get_exit_input();
        return 1;
    }
    else
    {
        std::wcout << L"Library injected successfully.\n";
    }
    get_exit_input();
    return 0;
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$
// finding part of path which shows file name
    int file_name_index = -1; // increases to 0 and checks entire path if no slash is found
    for (int i = 0; path[i] != '\0'; i++)
    {
        if (path[i] == '/')
        {
            file_name_index = i;
        }
    }

You mean string::rfind? Don't re-write functions that are already present in the collections or algorithms!

update: I see that path is a const char*. If you can't change the function to take a string_view just pop it into a string_view as the first line:

FILE* fopen(const char* path_raw, const char* mode)
{
    std::string_view path(path_raw);
    const auto file_name_index = path.rfind('/');
    const std::string file_name(path,1,path.length()-1);
    ...

static void remove_start_and_end_whitespace(wstring_or_string& file_name)
{
    wstring_or_string white_space = {9, 11, 12, 13, 32}; // whitespace character values

This would be easier and more efficient if you used string_view, which has a way to create a sub-view of the original, or a substring of a string, without recopying all the data when you trim from the front. I'd suggesting writing it to take a string_view that is constant, and return another string_view. You can still call it with an actual string.

The white_space should be constexpr and you didn't even make it static const, so your code is re-creating a fresh one every time it is called.

But you should not need to do this; use isspace to test whether a character is whitespace. There are forms of std::find that takes a functor, and it can search backwards by using reverse iterators.


    // 47 is forward slash, 45 is dash character, 10 is newline
    // numbers used so it will work with both string and wstring

You should define the character type, not the whole string. Then you can use that with basic_string, basic_string_view, and single characters!

Your "numbers" would become

constexpr auto slash = CharT(47);

but really you are widening the ASCII chars so you don't need the numbers:

constexpr auto slash = CharT('/');

If CharT is just char the cast does nothing; if it's wchar_t it is widened.

\$\endgroup\$
3
  • \$\begingroup\$ No because it’s a char array. I considered using strrchr there, but that ended up needing more to be written to deal with if NULL is returned. \$\endgroup\$ Commented Jun 1, 2021 at 19:15
  • \$\begingroup\$ Use string_view to wrap the raw string, and get all the useful members. Or use the standard algorithms instead: std::find on reverse iterators. \$\endgroup\$
    – JDługosz
    Commented Jun 1, 2021 at 19:21
  • 1
    \$\begingroup\$ I updated my answer. \$\endgroup\$
    – JDługosz
    Commented Jun 1, 2021 at 19:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.