beginner – C++ program which lets you lengthen the time it takes to access files REVISION #2

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

This is the previous post: C++ programs for Windows and Linux which let you lengthen the time it takes to access files REVISED VERSION

I tried to add everything mentioned in the answer there except using an obj / o file because I read that you have to include something twice if it’s used in both files, so it seemed like it would make the size a lot bigger because the files for robin_hood map and easyhook are big. I also would have had to put some of the functions in their own file because I want them to be compiled without printf calls unless it’s the testing program. And I also couldn’t figure out how to get it to work, the main file couldn’t see the things from the other file. But I’ll try again to get it to work if someone says it would still really be a good idea.

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

I want this to be revised as well as possible because I don’t have Mac OS, so I need to ask someone else to compile it for that OS. So I want it to be written as well as I can get it so I won’t have to ask people to compile it multiple times due to revisions.

struct_and_functions.h:

#include <stdint.h>
#include <iostream>
#include <fstream>
#include <string>
#include <atomic>
#include <vector>
#include <exception>

class delay_sequence
{
public:
    std::atomic<std::size_t> index;
    std::vector<std::chrono::milliseconds> delays;
    bool repeat;
    bool reset_all;

    delay_sequence() { index = 0; repeat = false; reset_all = false; }
    delay_sequence(const delay_sequence& copy_sequence)
    {
        index = copy_sequence.index.load();
        delays = copy_sequence.delays;
        repeat = copy_sequence.repeat;
        reset_all = copy_sequence.reset_all;
    }
    delay_sequence& operator=(delay_sequence copy_sequence)
    {
        index = copy_sequence.index.load();
        delays = copy_sequence.delays;
        repeat = copy_sequence.repeat;
        reset_all = copy_sequence.reset_all;
        return *this;
    }
};

// find out how much space to reserve for delays vector
static std::size_t setup_delay_sequence(delay_sequence& current_sequence, std::wstring& line, std::size_t start_position)
{
    std::size_t delay_sequence_length = 1;
    for (std::size_t line_position = start_position; line_position < line.length(); line_position++)
    {
        if (line(line_position) == L'/')
        {
            ++delay_sequence_length;
        }
        else if (line(line_position) == L'-') // no more delays past reset point
        {
            --delay_sequence_length; // the last slash was before the reset point, so it won't go in the vector
            if (delay_sequence_length == 0)
            {
                current_sequence.reset_all = true;
                break;
            }
            current_sequence.repeat = true;
            break;
        }
    }
    return delay_sequence_length;
}

// this will let the user write something like "1,500"
static void remove_non_digits(std::wstring& delay_substr)
{
    std::size_t position_to_write = 0;
    for (std::size_t i = 0; i < delay_substr.length(); i++)
    {
        if (isdigit(delay_substr(i)))
        {
            delay_substr(position_to_write) = delay_substr(i);
            ++position_to_write;
        }
    }
    delay_substr.erase(position_to_write);
}

static void set_delays(std::vector<std::chrono::milliseconds>& delays, std::wstring& line, std::size_t delay_sequence_length, std::size_t slash_position)
{
    static bool TOO_BIG_DELAY_SEEN = false; // static so function will remember if it already printed the message in a previous call
    delays.reserve(delay_sequence_length);
    unsigned long long max_delay = std::chrono::milliseconds::max().count();
    while (slash_position != std::wstring::npos && delays.size() != delay_sequence_length)
    {
        std::size_t next_slash_position = line.find(L'/', slash_position + 1);
        std::wstring delay_substr = line.substr(slash_position + 1, next_slash_position - slash_position - 1);
        remove_non_digits(delay_substr);
        if (delay_substr.length() == 0) // no numbers were written
        {
            delays.push_back(std::chrono::milliseconds(0));
            slash_position = next_slash_position;
            continue;
        }
        try
        {
            unsigned long long delay_time = std::stoull(delay_substr);
            if (delay_time > max_delay)
            {
                throw std::out_of_range(NULL);
            }
            delays.push_back(std::chrono::milliseconds(delay_time));
        }
        catch (const std::out_of_range& oor)
        {
            if (!TOO_BIG_DELAY_SEEN)
            {
                printf("delay time can only be %llu millisecondsn", max_delay);
                TOO_BIG_DELAY_SEEN = true;
            }
            delays.push_back(std::chrono::milliseconds(max_delay));
        }
        slash_position = next_slash_position;
    }
}

static void set_key_and_value(rh::unordered_flat_map<std::wstring, delay_sequence>& my_rh_map, std::wstring& line)
{
    std::size_t slash_position = line.find(L'/');
    if (slash_position == std::wstring::npos) // no slash so no delay sequence so line isn't valid
    {
        return;
    }
    std::size_t position_before_whitespace = line.find_last_not_of(L" tfvr/", slash_position);
    if (position_before_whitespace == std::wstring::npos) // the entire file name was whitespace
    {
        return;
    }
    std::wstring file_name = line.substr(0, position_before_whitespace + 1);
    delay_sequence current_sequence;
    std::size_t delay_sequence_length = setup_delay_sequence(current_sequence, line, slash_position + 1);
    set_delays(current_sequence.delays, line, delay_sequence_length, slash_position);
    my_rh_map(file_name) = current_sequence;
}

static unsigned char rh_map_setup(rh::unordered_flat_map<std::wstring, delay_sequence>& my_rh_map)
{
    std::size_t line_count = 1;
    std::wstring line;
    std::wifstream my_file;
    my_file.open("files_and_delays.txt");
    if (my_file.fail())
    {
        printf("couldn't open files_and_delays.txtn");
        return 1;
    }
    while (std::getline(my_file, line) && line_count < SIZE_MAX)
    {
        ++line_count;
    }
    if (line_count == SIZE_MAX)
    {
        printf("can't store %zu delaysn", SIZE_MAX);
        return 1;
    }
    my_rh_map.reserve(line_count); // overestimates slightly due to extra newlines and starting at 1
    my_file.clear();
    if (!my_file.seekg(0, std::ios::beg))
    {
        printf("seekg on files_and_delays.txt failedn");
        return 1;
    }
    line.clear();
    while (std::getline(my_file, line))
    {
        set_key_and_value(my_rh_map, line);
        line.clear();
    }
    my_file.close();
    return 0;
}

static void delay_file(rh::unordered_flat_map<std::wstring, delay_sequence>& my_rh_map, std::wstring& file_name)
{
    rh::unordered_flat_map<std::wstring, delay_sequence>::iterator rh_map_iter = my_rh_map.find(file_name);
    if (rh_map_iter != my_rh_map.end()) // key found
    {
        printf("%ls successfully found in hash mapn", file_name.c_str());
        delay_sequence& found_sequence = rh_map_iter->second;
        if (found_sequence.reset_all) // reset all delay sequences
        {
            for (auto& it : my_rh_map)
            {
                it.second.index = 0;
            }
            printf("all delay sequences resetn");
        }
        else
        {
            if (found_sequence.repeat) // reset delay sequence
            {
                if (found_sequence.index.load() > 0 && found_sequence.index.load() % found_sequence.delays.size() == 0)
                {
                    printf("%ls delay sequence resetn", file_name.c_str());
                }
                found_sequence.index = found_sequence.index.load() % found_sequence.delays.size();
            }
            if (found_sequence.index < found_sequence.delays.size())
            {
                // this is defined in the main file so it won't sleep if being used with load_extender_test.exe
                call_sleep_thread(found_sequence.delays(found_sequence.index));
                found_sequence.index += 1;
            }
            else // delay sequence already finished
            {
                printf("%ls delay sequence already finishedn", file_name.c_str());
            }
        }
    }
    else // key not found
    {
        printf("%ls not found in hash mapn", file_name.c_str());
    }
}

load_extender_test.cpp:

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

// this is defined here so delay_file function will use the right version
static void call_sleep_thread(std::chrono::milliseconds duration)
{
    unsigned long long total_milliseconds = duration.count();
    unsigned long long total_seconds = total_milliseconds / 1000;
    short remaining_milliseconds = total_milliseconds % 1000;
    printf("sleep for %llu second(s) and %d millisecond(s)n", total_seconds, remaining_milliseconds);
}

namespace rh = robin_hood;

#include "struct_and_functions.h" // included here so alias can be used

// these are global so the hooked function can see them
static rh::unordered_flat_map<std::wstring, delay_sequence> my_rh_map;
// SETUP_SUCCEEDED only changed here
static unsigned char SETUP_SUCCEEDED = rh_map_setup(my_rh_map);

static void print_rh_map()
{
    printf("n---------- hash map current state ----------n");
    for (auto& it : my_rh_map)
    {
        printf("%ls / ", it.first.c_str());
        delay_sequence& sequence_to_print = it.second;
        for (std::size_t i = 0; i < sequence_to_print.delays.size(); i++)
        {
            // casted to unsigned long long to get rid of warning message
            // visual studio on windows wants %lu, but gcc on linux wants %llu
            printf("%llu ", (unsigned long long)sequence_to_print.delays(i).count());
        }
        if (sequence_to_print.reset_all)
        {
            printf("RESET_ALL ");
        }
        else if (sequence_to_print.repeat)
        {
            printf("REPEAT ");
        }
        printf(": INDEX %zu", sequence_to_print.index.load());
        printf("n");
    }
    printf("---------------------------------------------nn");
}

static void test_all_inputs()
{
    #ifdef _WIN32
        std::wstring path_separator = L"\";
    #else
        std::wstring path_separator = L"/";
    #endif
    std::wstring test_path;
    std::wifstream input_test_file;
    input_test_file.open("test_input.txt");
    if (input_test_file.fail())
    {
        printf("couldn't open test_input.txtn");
        return;
    }
    if (SETUP_SUCCEEDED == 0)
    {
        print_rh_map();
        while (std::getline(input_test_file, test_path))
        {
            printf("testing path: %lsn", test_path.c_str());
            // +1 so separator isn't included
            // If separator isn't found, the whole wstring is checked because npos is -1
            std::wstring test_file = test_path.substr(test_path.rfind(path_separator) + 1);
            delay_file(my_rh_map, test_file);
            print_rh_map();
            test_path.clear();
        }
    }
    else
    {
        printf("setup failedn");
    }
    input_test_file.close();
}

int main(int argc, char* argv())
{
    printf("ntest startn");
    test_all_inputs();
    printf("test finished, press Enter to exitn");
    std::wstring input;
    std::getline(std::wcin, input);
    return 0;
}

load_extender.cpp

// #define NOMINMAX allows std::chrono::milliseconds::max().count();

#include <chrono>
#include <thread>
#ifdef _WIN32
    #define NOMINMAX
    #include <Windows.h>
    #include <easyhook.h>
#else
    #ifndef _GNU_SOURCE
        #define _GNU_SOURCE
    #endif
    #include <stdio.h>
    #include <dlfcn.h>
#endif
#include "robin_hood.h"
// thank you for making this hash map, martinus.
// here's his github: https://github.com/martinus/robin-hood-hashing

#define DISABLE_PRINTF

#ifdef DISABLE_PRINTF
    #define printf(fmt, ...) (0)
#endif

// this is defined here so delay_file function will use the right version
static void call_sleep_thread(std::chrono::milliseconds duration)
{
    std::this_thread::sleep_for(duration);
}

namespace rh = robin_hood;

// included here so alias can be used

#include "struct_and_functions.h"

// these are global so the hooked function can see them
static rh::unordered_flat_map<std::wstring, delay_sequence> my_rh_map;
// SETUP_SUCCEEDED only changed here
static unsigned char SETUP_SUCCEEDED = rh_map_setup(my_rh_map);

#ifdef _WIN32
    static NTSTATUS WINAPI NtOpenFileHook(
        PHANDLE           FileHandle,
        ACCESS_MASK        DesiredAccess,
        POBJECT_ATTRIBUTES ObjectAttributes,
        PIO_STATUS_BLOCK  IoStatusBlock,
        ULONG              ShareAccess,
        ULONG              OpenOptions)
    {
        if (SETUP_SUCCEEDED == 0)
        {
            std::wstring file_path = ObjectAttributes->ObjectName->Buffer;
            // +1 so '' isn't included. If '' isn't found, the whole wstring is checked because npos is -1
            std::wstring file_name = file_path.substr(file_path.rfind(L"\") + 1);
            delay_file(my_rh_map, file_name);
        }
        return NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, ShareAccess, OpenOptions);
    }

    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")), "NtOpenFile"),
            NtOpenFileHook,
            NULL,
            &hHook1);

        ULONG ACLEntries(1) = { 0 };
        LhSetExclusiveACL(ACLEntries, 1, &hHook1);
        return;
    }
#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)
    {
        if (SETUP_SUCCEEDED == 0)
        {
            // finding part of path which shows file name
            std::size_t file_name_index = SIZE_MAX; // overflows to 0 and checks entire path if no slash is found
            std::size_t end_index = 0;
            for (; path(end_index) != ''; end_index++)
            {
                if (path(end_index) == '/')
                {
                    file_name_index = end_index;
                }
            }
            std::wstring file_name(&path(file_name_index + 1), &path(end_index));
            delay_file(my_rh_map, file_name);
        }
        return original_fopen(path, mode);
    }
#endif

load_extender_injector.cpp (only used on Windows, LD_PRELOAD and DYLD_INSERT_LIBRARIES can be used on Linux and Max OS):

#include <tchar.h>
#include <iostream>
#include <string>
#include <Windows.h>
#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;
}