c++ – lengthening the time it takes to access files REVISION #3

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) != ''; 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 %dn", it.second(0));
    }
    printf("---------------------------------------------nn");
}

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.txtn");
        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 missingnsave test_input.txt as UTF-16 LEn");
        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", 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: %lsn", 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: %sn", 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 startn");
    test_all_inputs();
    printf("test finished, press Enter to exitn");
    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 millisecondsn", 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 sequencen", 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 %sn", 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 %sn", 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 missingnsave files_and_delays.txt as UTF-16 LEn");
        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", 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 mapn");
#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 resetn");
#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 resetn");
#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 finishedn");
            }
#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 mapn");
    }
#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 dllnn");
    // 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 = %dn", 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;
}