aboutsummaryrefslogtreecommitdiff
path: root/libs/usvfs/src/usvfs_dll
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/usvfs/src/usvfs_dll
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/usvfs/src/usvfs_dll')
-rw-r--r--libs/usvfs/src/usvfs_dll/CMakeLists.txt57
-rw-r--r--libs/usvfs/src/usvfs_dll/hookcallcontext.cpp112
-rw-r--r--libs/usvfs/src/usvfs_dll/hookcallcontext.h87
-rw-r--r--libs/usvfs/src/usvfs_dll/hookcontext.cpp330
-rw-r--r--libs/usvfs/src/usvfs_dll/hookcontext.h224
-rw-r--r--libs/usvfs/src/usvfs_dll/hookmanager.cpp333
-rw-r--r--libs/usvfs/src/usvfs_dll/hookmanager.h81
-rw-r--r--libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h263
-rw-r--r--libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp1860
-rw-r--r--libs/usvfs/src/usvfs_dll/hooks/kernel32.h116
-rw-r--r--libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp1520
-rw-r--r--libs/usvfs/src/usvfs_dll/hooks/ntdll.h57
-rw-r--r--libs/usvfs/src/usvfs_dll/hooks/sharedids.h9
-rw-r--r--libs/usvfs/src/usvfs_dll/maptracker.h691
-rw-r--r--libs/usvfs/src/usvfs_dll/pch.cpp1
-rw-r--r--libs/usvfs/src/usvfs_dll/redirectiontree.cpp27
-rw-r--r--libs/usvfs/src/usvfs_dll/redirectiontree.h104
-rw-r--r--libs/usvfs/src/usvfs_dll/semaphore.cpp57
-rw-r--r--libs/usvfs/src/usvfs_dll/semaphore.h27
-rw-r--r--libs/usvfs/src/usvfs_dll/sharedparameters.cpp264
-rw-r--r--libs/usvfs/src/usvfs_dll/stringcast_boost.h42
-rw-r--r--libs/usvfs/src/usvfs_dll/usvfs.cpp965
-rw-r--r--libs/usvfs/src/usvfs_dll/usvfsparameters.cpp193
-rw-r--r--libs/usvfs/src/usvfs_dll/version.rc40
24 files changed, 7460 insertions, 0 deletions
diff --git a/libs/usvfs/src/usvfs_dll/CMakeLists.txt b/libs/usvfs/src/usvfs_dll/CMakeLists.txt
new file mode 100644
index 0000000..2520b50
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/CMakeLists.txt
@@ -0,0 +1,57 @@
+cmake_minimum_required(VERSION 3.16)
+
+include(GNUInstallDirs)
+
+find_package(Boost CONFIG REQUIRED COMPONENTS thread)
+find_package(spdlog CONFIG REQUIRED)
+
+file(GLOB sources "*.cpp" "*.h")
+source_group("" FILES ${sources})
+
+file(GLOB hooks "hooks/*.cpp" "hooks/*.h")
+source_group("dlls" FILES ${hooks})
+
+add_library(usvfs_dll SHARED)
+target_sources(usvfs_dll
+ PRIVATE
+ ${PROJECT_SOURCE_DIR}/include/usvfs/usvfsparametersprivate.h
+ ${sources}
+ ${hooks}
+ version.rc
+ PUBLIC
+ FILE_SET HEADERS
+ BASE_DIRS ${PROJECT_SOURCE_DIR}/include
+ FILES
+ ${PROJECT_SOURCE_DIR}/include/usvfs/dllimport.h
+ ${PROJECT_SOURCE_DIR}/include/usvfs/logging.h
+ ${PROJECT_SOURCE_DIR}/include/usvfs/sharedparameters.h
+ ${PROJECT_SOURCE_DIR}/include/usvfs/usvfs_version.h
+ ${PROJECT_SOURCE_DIR}/include/usvfs/usvfs.h
+ ${PROJECT_SOURCE_DIR}/include/usvfs/usvfsparameters.h
+)
+target_link_libraries(usvfs_dll
+ PRIVATE shared thooklib usvfs_helper
+ Boost::thread spdlog::spdlog_header_only
+ Shlwapi)
+target_compile_definitions(usvfs_dll PRIVATE BUILDING_USVFS_DLL)
+set_target_properties(usvfs_dll
+ PROPERTIES
+ ARCHIVE_OUTPUT_NAME usvfs${ARCH_POSTFIX}
+ ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR}
+ ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR}
+ LIBRARY_OUTPUT_NAME usvfs${ARCH_POSTFIX}
+ LIBRARY_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR}
+ LIBRARY_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR}
+ PDB_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR}
+ PDB_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR}
+ RUNTIME_OUTPUT_NAME usvfs${ARCH_POSTFIX}
+ RUNTIME_OUTPUT_DIRECTORY_DEBUG ${USVFS_LIBDIR}
+ RUNTIME_OUTPUT_DIRECTORY_RELEASE ${USVFS_LIBDIR})
+
+install(TARGETS usvfs_dll EXPORT usvfs${ARCH_POSTFIX}Targets FILE_SET HEADERS)
+install(FILES $<TARGET_PDB_FILE:usvfs_dll> DESTINATION pdb OPTIONAL)
+install(EXPORT usvfs${ARCH_POSTFIX}Targets
+ FILE usvfs${ARCH_POSTFIX}Targets.cmake
+ NAMESPACE usvfs${ARCH_POSTFIX}::
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/usvfs
+)
diff --git a/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp b/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp
new file mode 100644
index 0000000..32510c9
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hookcallcontext.cpp
@@ -0,0 +1,112 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include "hookcallcontext.h"
+#include "hookcontext.h"
+#include <logging.h>
+
+namespace usvfs
+{
+
+class HookStack
+{
+public:
+ static HookStack& instance()
+ {
+ PreserveGetLastError boostChangesGetLastError;
+ if (s_Instance.get() == nullptr) {
+ s_Instance.reset(new HookStack());
+ }
+ return *s_Instance.get();
+ }
+
+ bool setGroup(MutExHookGroup group)
+ {
+ if (m_ActiveGroups.test(static_cast<size_t>(group)) ||
+ m_ActiveGroups.test(static_cast<size_t>(MutExHookGroup::ALL_GROUPS))) {
+ return false;
+ } else {
+ m_ActiveGroups.set(static_cast<size_t>(group), true);
+ return true;
+ }
+ }
+
+ void unsetGroup(MutExHookGroup group)
+ {
+ m_ActiveGroups.set(static_cast<size_t>(group), false);
+ }
+
+private:
+ HookStack() {}
+
+private:
+ static boost::thread_specific_ptr<HookStack> s_Instance;
+ std::bitset<static_cast<size_t>(MutExHookGroup::LAST)> m_ActiveGroups;
+};
+
+boost::thread_specific_ptr<HookStack> HookStack::s_Instance;
+
+HookCallContext::HookCallContext() : m_Active(true), m_Group(MutExHookGroup::NO_GROUP)
+{
+ updateLastError();
+}
+
+HookCallContext::HookCallContext(MutExHookGroup group)
+ : m_Active(HookStack::instance().setGroup(group)), m_Group(group)
+{
+ updateLastError();
+}
+
+HookCallContext::~HookCallContext()
+{
+ if (m_Active && (m_Group != MutExHookGroup::NO_GROUP)) {
+ HookStack::instance().unsetGroup(m_Group);
+ }
+ SetLastError(m_LastError);
+}
+
+void HookCallContext::restoreLastError()
+{
+ SetLastError(m_LastError);
+}
+
+void HookCallContext::updateLastError(DWORD lastError)
+{
+ m_LastError = lastError;
+}
+
+bool HookCallContext::active() const
+{
+ return m_Active;
+}
+
+FunctionGroupLock::FunctionGroupLock(MutExHookGroup group) : m_Group(group)
+{
+ m_Active = HookStack::instance().setGroup(m_Group);
+}
+
+FunctionGroupLock::~FunctionGroupLock()
+{
+ if (m_Active) {
+ HookStack::instance().unsetGroup(m_Group);
+ }
+}
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/hookcallcontext.h b/libs/usvfs/src/usvfs_dll/hookcallcontext.h
new file mode 100644
index 0000000..ebb3bf9
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hookcallcontext.h
@@ -0,0 +1,87 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#pragma once
+
+namespace usvfs
+{
+
+/**
+ * @brief groups of hooks which may be used to implement each other, so only the first
+ * call should be to one should be manipulated
+ */
+enum class MutExHookGroup : int
+{
+ ALL_GROUPS = 0, // An ALL_GROUPS-hook prevents all other hooks from becoming active
+ // BUT hooks from other groups don't prevent the ALL_GROUPS-hook from
+ // becoming activated
+ OPEN_FILE = 1,
+ CREATE_PROCESS = 2,
+ FILE_ATTRIBUTES = 3,
+ FIND_FILES = 4,
+ LOAD_LIBRARY = 5,
+ FULL_PATHNAME = 6,
+ SHELL_FILEOP = 7,
+ DELETE_FILE = 8,
+ GET_FILE_VERSION = 9,
+ GET_MODULE_HANDLE = 10,
+ SEARCH_FILES = 11,
+
+ NO_GROUP = 12,
+ LAST = NO_GROUP,
+};
+
+class HookCallContext
+{
+
+public:
+ HookCallContext();
+ HookCallContext(MutExHookGroup group);
+ ~HookCallContext();
+
+ HookCallContext(const HookCallContext& reference) = delete;
+ HookCallContext& operator=(const HookCallContext& reference) = delete;
+
+ void restoreLastError();
+
+ void updateLastError(DWORD lastError = GetLastError());
+
+ DWORD lastError() const { return m_LastError; }
+
+ bool active() const;
+
+private:
+ DWORD m_LastError;
+ bool m_Active;
+ MutExHookGroup m_Group;
+};
+
+class FunctionGroupLock
+{
+public:
+ FunctionGroupLock(MutExHookGroup group);
+ ~FunctionGroupLock();
+
+private:
+ MutExHookGroup m_Group;
+ bool m_Active;
+};
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/hookcontext.cpp b/libs/usvfs/src/usvfs_dll/hookcontext.cpp
new file mode 100644
index 0000000..f47e42d
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hookcontext.cpp
@@ -0,0 +1,330 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include "hookcontext.h"
+#include "exceptionex.h"
+#include "hookcallcontext.h"
+#include "loghelpers.h"
+#include "usvfs.h"
+#include <shared_memory.h>
+#include <sharedparameters.h>
+#include <usvfsparameters.h>
+#include <winapi.h>
+
+namespace bi = boost::interprocess;
+using usvfs::shared::SharedMemoryT;
+using usvfs::shared::VoidAllocatorT;
+
+using namespace usvfs;
+namespace ush = usvfs::shared;
+
+HookContext* HookContext::s_Instance = nullptr;
+
+void printBuffer(const char* buffer, size_t size)
+{
+ static const int bufferSize = 16 * 3;
+ char temp[bufferSize + 1];
+ temp[bufferSize] = '\0';
+
+ for (size_t i = 0; i < size; ++i) {
+ size_t offset = i % 16;
+ _snprintf(&temp[offset * 3], 3, "%02x ", (unsigned char)buffer[i]);
+ if (offset == 15) {
+ spdlog::get("hooks")->info("{0:x} - {1}", i - offset, temp);
+ }
+ }
+
+ spdlog::get("hooks")->info(temp);
+}
+
+HookContext::HookContext(const usvfsParameters& params, HMODULE module)
+ : m_ConfigurationSHM(bi::open_or_create, params.instanceName, 64 * 1024),
+ m_Parameters(retrieveParameters(params)),
+ m_Tree(m_Parameters->currentSHMName(),
+ 4 * 1024 * 1024) // 4 MiB empirically covers most small setups without
+ // need to resize
+ ,
+ m_InverseTree(
+ m_Parameters->currentInverseSHMName(),
+ 128 * 1024) // 128 KiB should cover reverse tree for even larger setups
+ ,
+ m_DLLModule(module)
+{
+ if (s_Instance != nullptr) {
+ throw std::runtime_error("singleton duplicate instantiation (HookContext)");
+ }
+
+ const auto userCount = m_Parameters->userConnected();
+
+ spdlog::get("usvfs")->debug("context current shm: {0} (now {1} connections)",
+ m_Parameters->currentSHMName(), userCount);
+
+ s_Instance = this;
+
+ if (m_Tree.get() == nullptr) {
+ USVFS_THROW_EXCEPTION(usage_error()
+ << ex_msg("shm not found") << ex_msg(params.instanceName));
+ }
+}
+
+void HookContext::remove(const char* instanceName)
+{
+ bi::shared_memory_object::remove(instanceName);
+}
+
+HookContext::~HookContext()
+{
+ spdlog::get("usvfs")->info("releasing hook context");
+
+ s_Instance = nullptr;
+ const auto userCount = m_Parameters->userDisconnected();
+
+ if (userCount == 0) {
+ spdlog::get("usvfs")->info("removing tree {}", m_Parameters->instanceName());
+ bi::shared_memory_object::remove(m_Parameters->instanceName().c_str());
+ } else {
+ spdlog::get("usvfs")->info("{} users left", userCount);
+ }
+}
+
+SharedParameters* HookContext::retrieveParameters(const usvfsParameters& params)
+{
+ std::pair<SharedParameters*, SharedMemoryT::size_type> res =
+ m_ConfigurationSHM.find<SharedParameters>("parameters");
+
+ if (res.first == nullptr) {
+ // not configured yet
+ spdlog::get("usvfs")->info("create config in {}", ::GetCurrentProcessId());
+
+ res.first = m_ConfigurationSHM.construct<SharedParameters>("parameters")(
+ params, VoidAllocatorT(m_ConfigurationSHM.get_segment_manager()));
+
+ if (res.first == nullptr) {
+ USVFS_THROW_EXCEPTION(bi::bad_alloc());
+ }
+ } else {
+ spdlog::get("usvfs")->info("access existing config in {}", ::GetCurrentProcessId());
+ }
+
+ spdlog::get("usvfs")->info("{} processes", res.first->registeredProcessCount());
+
+ return res.first;
+}
+
+HookContext::ConstPtr HookContext::readAccess(const char*)
+{
+ BOOST_ASSERT(s_Instance != nullptr);
+
+ // TODO: this should be a shared mutex!
+ s_Instance->m_Mutex.wait(200);
+ return ConstPtr(s_Instance, unlockShared);
+}
+
+HookContext::Ptr HookContext::writeAccess(const char*)
+{
+ BOOST_ASSERT(s_Instance != nullptr);
+
+ s_Instance->m_Mutex.wait(200);
+ return Ptr(s_Instance, unlock);
+}
+
+void HookContext::setDebugParameters(LogLevel level, CrashDumpsType dumpType,
+ const std::string& dumpPath,
+ std::chrono::milliseconds delayProcess)
+{
+ m_Parameters->setDebugParameters(level, dumpType, dumpPath, delayProcess);
+}
+
+void HookContext::updateParameters() const
+{
+ m_Parameters->setSHMNames(m_Tree.shmName(), m_InverseTree.shmName());
+}
+
+usvfsParameters HookContext::callParameters() const
+{
+ updateParameters();
+ return m_Parameters->makeLocal();
+}
+
+std::wstring HookContext::dllPath() const
+{
+ std::wstring path = winapi::wide::getModuleFileName(m_DLLModule);
+ return boost::filesystem::path(path).parent_path().make_preferred().wstring();
+}
+
+void HookContext::registerProcess(DWORD pid)
+{
+ m_Parameters->registerProcess(pid);
+}
+
+void HookContext::unregisterCurrentProcess()
+{
+ m_Parameters->unregisterProcess(::GetCurrentProcessId());
+}
+
+std::vector<DWORD> HookContext::registeredProcesses() const
+{
+ return m_Parameters->registeredProcesses();
+}
+
+void HookContext::blacklistExecutable(const std::wstring& wexe)
+{
+ const auto exe = shared::string_cast<std::string>(wexe, shared::CodePage::UTF8);
+
+ spdlog::get("usvfs")->debug("blacklisting '{}'", exe);
+ m_Parameters->blacklistExecutable(exe);
+}
+
+void HookContext::clearExecutableBlacklist()
+{
+ spdlog::get("usvfs")->debug("clearing blacklist");
+ m_Parameters->clearExecutableBlacklist();
+}
+
+BOOL HookContext::executableBlacklisted(LPCWSTR wapp, LPCWSTR wcmd) const
+{
+ std::string app;
+ if (wapp) {
+ app = ush::string_cast<std::string>(wapp, ush::CodePage::UTF8);
+ }
+
+ std::string cmd;
+ if (wcmd) {
+ cmd = ush::string_cast<std::string>(wcmd, ush::CodePage::UTF8);
+ }
+
+ return m_Parameters->executableBlacklisted(app, cmd);
+}
+
+void usvfs::HookContext::addSkipFileSuffix(const std::wstring& fileSuffix)
+{
+ const auto fsuffix =
+ shared::string_cast<std::string>(fileSuffix, shared::CodePage::UTF8);
+
+ if (fsuffix.empty()) {
+ return;
+ }
+
+ spdlog::get("usvfs")->debug("added skip file suffix '{}'", fsuffix);
+ m_Parameters->addSkipFileSuffix(fsuffix);
+}
+
+void usvfs::HookContext::clearSkipFileSuffixes()
+{
+ spdlog::get("usvfs")->debug("clearing skip file suffixes");
+ m_Parameters->clearSkipFileSuffixes();
+}
+
+std::vector<std::string> usvfs::HookContext::skipFileSuffixes() const
+{
+ return m_Parameters->skipFileSuffixes();
+}
+
+void usvfs::HookContext::addSkipDirectory(const std::wstring& directory)
+{
+ const auto dir = shared::string_cast<std::string>(directory, shared::CodePage::UTF8);
+
+ if (dir.empty()) {
+ return;
+ }
+
+ spdlog::get("usvfs")->debug("added skip directory '{}'", dir);
+ m_Parameters->addSkipDirectory(dir);
+}
+
+void usvfs::HookContext::clearSkipDirectories()
+{
+ spdlog::get("usvfs")->debug("clearing skip directories");
+ m_Parameters->clearSkipDirectories();
+}
+
+std::vector<std::string> usvfs::HookContext::skipDirectories() const
+{
+ return m_Parameters->skipDirectories();
+}
+
+void HookContext::forceLoadLibrary(const std::wstring& wprocess,
+ const std::wstring& wpath)
+{
+ const auto process =
+ shared::string_cast<std::string>(wprocess, shared::CodePage::UTF8);
+
+ const auto path = shared::string_cast<std::string>(wpath, shared::CodePage::UTF8);
+
+ spdlog::get("usvfs")->debug("adding forced library '{}' for process '{}'", path,
+ process);
+
+ m_Parameters->addForcedLibrary(process, path);
+}
+
+void HookContext::clearLibraryForceLoads()
+{
+ spdlog::get("usvfs")->debug("clearing forced libraries");
+ m_Parameters->clearForcedLibraries();
+}
+
+std::vector<std::wstring>
+HookContext::librariesToForceLoad(const std::wstring& processName)
+{
+ const auto v = m_Parameters->forcedLibraries(
+ shared::string_cast<std::string>(processName, shared::CodePage::UTF8));
+
+ std::vector<std::wstring> wv;
+ for (const auto& s : v) {
+ wv.push_back(shared::string_cast<std::wstring>(s, shared::CodePage::UTF8));
+ }
+
+ return wv;
+}
+
+void HookContext::registerDelayed(std::future<int> delayed)
+{
+ m_Futures.push_back(std::move(delayed));
+}
+
+std::vector<std::future<int>>& HookContext::delayed()
+{
+ return m_Futures;
+}
+
+void HookContext::unlock(HookContext* instance)
+{
+ instance->m_Mutex.signal();
+}
+
+void HookContext::unlockShared(const HookContext* instance)
+{
+ instance->m_Mutex.signal();
+}
+
+// deprecated
+//
+extern "C" DLLEXPORT HookContext* __cdecl CreateHookContext(
+ const USVFSParameters& oldParams, HMODULE module)
+{
+ const usvfsParameters p(oldParams);
+ return usvfsCreateHookContext(p, module);
+}
+
+extern "C" DLLEXPORT usvfs::HookContext* WINAPI
+usvfsCreateHookContext(const usvfsParameters& params, HMODULE module)
+{
+ return new HookContext(params, module);
+}
diff --git a/libs/usvfs/src/usvfs_dll/hookcontext.h b/libs/usvfs/src/usvfs_dll/hookcontext.h
new file mode 100644
index 0000000..c9f6eb1
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hookcontext.h
@@ -0,0 +1,224 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#pragma once
+
+#include "dllimport.h"
+#include "redirectiontree.h"
+#include "semaphore.h"
+#include "tree_container.h"
+#include <directory_tree.h>
+#include <exceptionex.h>
+#include <usvfsparameters.h>
+#include <usvfsparametersprivate.h>
+#include <winapi.h>
+
+namespace usvfs
+{
+
+class DLLEXPORT SharedParameters;
+
+/**
+ * @brief context available to hooks. This is protected by a many-reader
+ * single-writer mutex
+ */
+class HookContext
+{
+
+public:
+ typedef std::unique_ptr<const HookContext, void (*)(const HookContext*)> ConstPtr;
+ typedef std::unique_ptr<HookContext, void (*)(HookContext*)> Ptr;
+ typedef unsigned int DataIDT;
+
+public:
+ HookContext(const usvfsParameters& params, HMODULE module);
+
+ HookContext(const HookContext& reference) = delete;
+
+ DLLEXPORT ~HookContext();
+
+ HookContext& operator=(const HookContext& reference) = delete;
+
+ static void remove(const char* instance);
+
+ /**
+ * @brief get read access to the context.
+ * @return smart ptr to the context. mutex will automatically be released when
+ * this leaves scope
+ */
+ static ConstPtr readAccess(const char* source);
+
+ /**
+ * @brief get write access to the context.
+ * @return smart ptr to the context. mutex will automatically be released when
+ * this leaves scope
+ */
+ static Ptr writeAccess(const char* source);
+
+ /**
+ * @return table containing file redirection information
+ */
+ RedirectionTreeContainer& redirectionTable() { return m_Tree; }
+
+ /**
+ * @return table containing file redirection information
+ */
+ const RedirectionTreeContainer& redirectionTable() const { return m_Tree; }
+
+ RedirectionTreeContainer& inverseTable() { return m_InverseTree; }
+
+ const RedirectionTreeContainer& inverseTable() const { return m_InverseTree; }
+
+ /**
+ * @return the parameters passed in on dll initialisation
+ */
+ usvfsParameters callParameters() const;
+
+ /**
+ * @return path to the calling library itself
+ */
+ std::wstring dllPath() const;
+
+ /**
+ * @brief get access to custom data
+ * @note the caller gains write access to the data, independent on the lock on
+ * the context
+ * as a whole. The caller himself has to ensure thread safety
+ */
+ template <typename T>
+ T& customData(DataIDT id) const
+ {
+ auto iter = m_CustomData.find(id);
+ if (iter == m_CustomData.end()) {
+ iter = m_CustomData.insert(std::make_pair(id, T())).first;
+ }
+ // std::map is supposed to not invalidate any iterators when elements are
+ // added
+ // so it should be safe to return a pointer here
+ T* res = boost::any_cast<T>(&iter->second);
+ return *res;
+ }
+
+ void registerProcess(DWORD pid);
+ void unregisterCurrentProcess();
+ std::vector<DWORD> registeredProcesses() const;
+
+ void blacklistExecutable(const std::wstring& executableName);
+ void clearExecutableBlacklist();
+ BOOL executableBlacklisted(LPCWSTR lpApplicationName, LPCWSTR lpCommandLine) const;
+
+ void addSkipFileSuffix(const std::wstring& fileSuffix);
+ void clearSkipFileSuffixes();
+ std::vector<std::string> skipFileSuffixes() const;
+
+ void addSkipDirectory(const std::wstring& directory);
+ void clearSkipDirectories();
+ std::vector<std::string> skipDirectories() const;
+
+ void forceLoadLibrary(const std::wstring& processName,
+ const std::wstring& libraryPath);
+ void clearLibraryForceLoads();
+ std::vector<std::wstring> librariesToForceLoad(const std::wstring& processName);
+
+ void setDebugParameters(LogLevel level, CrashDumpsType dumpType,
+ const std::string& dumpPath,
+ std::chrono::milliseconds delayProcess);
+
+ void updateParameters() const;
+
+ void registerDelayed(std::future<int> delayed);
+
+ std::vector<std::future<int>>& delayed();
+
+private:
+ static void unlock(HookContext* instance);
+ static void unlockShared(const HookContext* instance);
+
+ SharedParameters* retrieveParameters(const usvfsParameters& params);
+
+private:
+ static HookContext* s_Instance;
+
+ shared::SharedMemoryT m_ConfigurationSHM;
+ SharedParameters* m_Parameters{nullptr};
+ RedirectionTreeContainer m_Tree;
+ RedirectionTreeContainer m_InverseTree;
+
+ std::vector<std::future<int>> m_Futures;
+
+ mutable std::map<DataIDT, boost::any> m_CustomData;
+
+ HMODULE m_DLLModule;
+
+ // mutable std::recursive_mutex m_Mutex;
+ mutable RecursiveBenaphore m_Mutex;
+};
+
+} // namespace usvfs
+
+extern "C" DLLEXPORT usvfs::HookContext* WINAPI
+usvfsCreateHookContext(const usvfsParameters& params, HMODULE module);
+
+class PreserveGetLastError
+{
+public:
+ PreserveGetLastError() : m_err(GetLastError()) {}
+ ~PreserveGetLastError() { SetLastError(m_err); }
+
+private:
+ DWORD m_err;
+};
+
+// declare an identifier that is guaranteed to be unique across the application
+#define DATA_ID(name) static const usvfs::HookContext::DataIDT name = __COUNTER__
+
+// set of macros. These ensure a call context is created but most of all these
+// ensure exceptions are caught.
+
+#define READ_CONTEXT() HookContext::readAccess(__MYFUNC__)
+#define WRITE_CONTEXT() HookContext::writeAccess(__MYFUNC__)
+
+#define HOOK_START_GROUP(group) \
+ try { \
+ HookCallContext callContext(group);
+
+#define HOOK_START \
+ try { \
+ HookCallContext callContext;
+
+#define HOOK_END \
+ } \
+ catch (const std::exception& e) \
+ { \
+ spdlog::get("usvfs")->error("exception in {0}: {1}", __MYFUNC__, e.what()); \
+ logExtInfo(e); \
+ }
+
+#define HOOK_ENDP(param) \
+ } \
+ catch (const std::exception& e) \
+ { \
+ spdlog::get("usvfs")->error("exception in {0} ({1}): {2}", __MYFUNC__, param, \
+ e.what()); \
+ logExtInfo(e); \
+ }
+
+#define PRE_REALCALL callContext.restoreLastError();
+#define POST_REALCALL callContext.updateLastError();
diff --git a/libs/usvfs/src/usvfs_dll/hookmanager.cpp b/libs/usvfs/src/usvfs_dll/hookmanager.cpp
new file mode 100644
index 0000000..731b856
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hookmanager.cpp
@@ -0,0 +1,333 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include "hookmanager.h"
+#include "../thooklib/ttrampolinepool.h"
+#include "../thooklib/utility.h"
+#include "exceptionex.h"
+#include "hooks/kernel32.h"
+#include "hooks/ntdll.h"
+#include "usvfs.h"
+#include <VersionHelpers.h>
+#include <directory_tree.h>
+#include <logging.h>
+#include <shmlogger.h>
+#include <usvfsparameters.h>
+#include <winapi.h>
+
+using namespace HookLib;
+namespace bf = boost::filesystem;
+
+namespace usvfs
+{
+
+HookManager* HookManager::s_Instance = nullptr;
+
+HookManager::HookManager(const usvfsParameters& params, HMODULE module)
+ : m_Context(params, module)
+{
+ if (s_Instance != nullptr) {
+ throw std::runtime_error("singleton duplicate instantiation (HookManager)");
+ }
+
+ s_Instance = this;
+
+ m_Context.registerProcess(::GetCurrentProcessId());
+ spdlog::get("usvfs")->info("Process registered in shared process list : {}",
+ ::GetCurrentProcessId());
+
+ winapi::ex::OSVersion version = winapi::ex::getOSVersion();
+ spdlog::get("usvfs")->info(
+ "Windows version {}.{}.{} sp {} platform {} ({})", version.major, version.minor,
+ version.build, version.servicpack, version.platformid,
+ shared::string_cast<std::string>(winapi::ex::wide::getWindowsBuildLab(true))
+ .c_str());
+
+ initHooks();
+
+ if (params.debugMode) {
+ while (!::IsDebuggerPresent()) {
+ // wait for debugger to attach
+ ::Sleep(100);
+ }
+ }
+}
+
+HookManager::~HookManager()
+{
+ spdlog::get("hooks")->debug("end hook of process {}", GetCurrentProcessId());
+ removeHooks();
+ m_Context.unregisterCurrentProcess();
+}
+
+HookManager& HookManager::instance()
+{
+ if (s_Instance == nullptr) {
+ throw std::runtime_error("singleton not instantiated");
+ }
+
+ return *s_Instance;
+}
+
+LPVOID HookManager::detour(const char* functionName)
+{
+ auto iter = m_Hooks.find(functionName);
+ if (iter != m_Hooks.end()) {
+ return GetDetour(iter->second);
+ } else {
+ return nullptr;
+ }
+}
+
+void HookManager::removeHook(const std::string& functionName)
+{
+ auto iter = m_Hooks.find(functionName);
+ if (iter != m_Hooks.end()) {
+ try {
+ RemoveHook(iter->second);
+ m_Hooks.erase(iter);
+ spdlog::get("usvfs")->info("removed hook for {}", functionName);
+ } catch (const std::exception& e) {
+ spdlog::get("usvfs")->critical("failed to remove hook of {}: {}", functionName,
+ e.what());
+ }
+ } else {
+ spdlog::get("usvfs")->info("{} wasn't hooked", functionName);
+ }
+}
+
+void HookManager::logStubInt(LPVOID address)
+{
+ if (m_Stubs.find(address) != m_Stubs.end()) {
+ spdlog::get("hooks")->warn("{0} called", m_Stubs[address]);
+ } else {
+ spdlog::get("hooks")->warn("unknown function at {0} called", address);
+ }
+}
+
+void HookManager::logStub(LPVOID address)
+{
+ try {
+ instance().logStubInt(address);
+ } catch (const std::exception& e) {
+ spdlog::get("hooks")->debug("function at {0} called after shutdown: {1}", address,
+ e.what());
+ }
+}
+
+void HookManager::installHook(HMODULE module1, HMODULE module2,
+ const std::string& functionName, LPVOID hook,
+ LPVOID* fillFuncAddr = nullptr)
+{
+ BOOST_ASSERT(hook != nullptr);
+ HOOKHANDLE handle = INVALID_HOOK;
+ HookError err = ERR_NONE;
+ LPVOID funcAddr = nullptr;
+ HMODULE usedModule = nullptr;
+ // both module1 and module2 are allowed to be null
+ if (module1 != nullptr) {
+ funcAddr = MyGetProcAddress(module1, functionName.c_str());
+ if (funcAddr != nullptr) {
+ handle = InstallHook(funcAddr, hook, &err);
+ }
+ if (handle != INVALID_HOOK)
+ usedModule = module1;
+ }
+
+ if ((handle == INVALID_HOOK) && (module2 != nullptr)) {
+ funcAddr = MyGetProcAddress(module2, functionName.c_str());
+ if (funcAddr != nullptr) {
+ handle = InstallHook(funcAddr, hook, &err);
+ }
+ if (handle != INVALID_HOOK)
+ usedModule = module2;
+ }
+
+ if (fillFuncAddr)
+ *fillFuncAddr = funcAddr;
+
+ if (handle == INVALID_HOOK) {
+ spdlog::get("usvfs")->error("failed to hook {0}: {1}", functionName,
+ GetErrorString(err));
+ } else {
+ m_Stubs.insert(make_pair(funcAddr, functionName));
+ m_Hooks.insert(make_pair(std::string(functionName), handle));
+ spdlog::get("usvfs")->info("hooked {0} ({1}) in {2} type {3}", functionName,
+ funcAddr, winapi::ansi::getModuleFileName(usedModule),
+ GetHookType(handle));
+ }
+}
+
+void HookManager::installStub(HMODULE module1, HMODULE module2,
+ const std::string& functionName)
+{
+ HOOKHANDLE handle = INVALID_HOOK;
+ HookError err = ERR_NONE;
+ LPVOID funcAddr = nullptr;
+ HMODULE usedModule = nullptr;
+ // both module1 and module2 are allowed to be null
+ if (module1 != nullptr) {
+ funcAddr = MyGetProcAddress(module1, functionName.c_str());
+ if (funcAddr != nullptr) {
+ handle = InstallStub(funcAddr, logStub, &err);
+ } else {
+ spdlog::get("usvfs")->debug("{} doesn't contain {}",
+ winapi::ansi::getModuleFileName(module1),
+ functionName);
+ }
+ if (handle != INVALID_HOOK)
+ usedModule = module1;
+ }
+
+ if ((handle == INVALID_HOOK) && (module2 != nullptr)) {
+ funcAddr = MyGetProcAddress(module2, functionName.c_str());
+ if (funcAddr != nullptr) {
+ handle = InstallStub(funcAddr, logStub, &err);
+ } else {
+ spdlog::get("usvfs")->debug("{} doesn't contain {}",
+ winapi::ansi::getModuleFileName(module2),
+ functionName);
+ }
+ if (handle != INVALID_HOOK)
+ usedModule = module2;
+ }
+
+ if (handle == INVALID_HOOK) {
+ spdlog::get("usvfs")->error("failed to stub {0}: {1}", functionName,
+ GetErrorString(err));
+ } else {
+ m_Stubs.insert(make_pair(funcAddr, functionName));
+ m_Hooks.insert(make_pair(std::string(functionName), handle));
+ spdlog::get("usvfs")->info("stubbed {0} ({1}) in {2} type {3}", functionName,
+ funcAddr, winapi::ansi::getModuleFileName(usedModule),
+ GetHookType(handle));
+ }
+}
+
+void HookManager::initHooks()
+{
+ TrampolinePool::initialize();
+
+ HookLib::TrampolinePool::instance().setBlock(true);
+
+ HMODULE k32Mod = GetModuleHandleA("kernel32.dll");
+ spdlog::get("usvfs")->debug("kernel32.dll at {0:x}",
+ reinterpret_cast<uintptr_t>(k32Mod));
+ // kernelbase.dll contains the actual implementation for functions formerly in
+ // kernel32.dll and advapi32.dll, starting with Windows 7
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/dd371752(v=vs.85).aspx
+ HMODULE kbaseMod = GetModuleHandleA("kernelbase.dll");
+ spdlog::get("usvfs")->debug("kernelbase.dll at {0:x}",
+ reinterpret_cast<uintptr_t>(kbaseMod));
+
+ installHook(kbaseMod, k32Mod, "GetFileAttributesExA", hook_GetFileAttributesExA);
+ installHook(kbaseMod, k32Mod, "GetFileAttributesA", hook_GetFileAttributesA);
+ installHook(kbaseMod, k32Mod, "GetFileAttributesExW", hook_GetFileAttributesExW);
+ installHook(kbaseMod, k32Mod, "GetFileAttributesW", hook_GetFileAttributesW);
+ installHook(kbaseMod, k32Mod, "SetFileAttributesW", hook_SetFileAttributesW);
+
+ installHook(kbaseMod, k32Mod, "CreateDirectoryW", hook_CreateDirectoryW);
+ installHook(kbaseMod, k32Mod, "RemoveDirectoryW", hook_RemoveDirectoryW);
+ installHook(kbaseMod, k32Mod, "DeleteFileW", hook_DeleteFileW);
+ installHook(kbaseMod, k32Mod, "GetCurrentDirectoryA", hook_GetCurrentDirectoryA);
+ installHook(kbaseMod, k32Mod, "GetCurrentDirectoryW", hook_GetCurrentDirectoryW);
+ installHook(kbaseMod, k32Mod, "SetCurrentDirectoryA", hook_SetCurrentDirectoryA);
+ installHook(kbaseMod, k32Mod, "SetCurrentDirectoryW", hook_SetCurrentDirectoryW);
+
+ installHook(kbaseMod, k32Mod, "ExitProcess", hook_ExitProcess);
+
+ installHook(kbaseMod, k32Mod, "CreateProcessInternalW", hook_CreateProcessInternalW,
+ reinterpret_cast<LPVOID*>(&CreateProcessInternalW));
+
+ installHook(kbaseMod, k32Mod, "MoveFileA", hook_MoveFileA);
+ installHook(kbaseMod, k32Mod, "MoveFileW", hook_MoveFileW);
+ installHook(kbaseMod, k32Mod, "MoveFileExA", hook_MoveFileExA);
+ installHook(kbaseMod, k32Mod, "MoveFileExW", hook_MoveFileExW);
+ installHook(kbaseMod, k32Mod, "MoveFileWithProgressA", hook_MoveFileWithProgressA);
+ installHook(kbaseMod, k32Mod, "MoveFileWithProgressW", hook_MoveFileWithProgressW);
+
+ installHook(kbaseMod, k32Mod, "CopyFileExW", hook_CopyFileExW);
+ if (IsWindows8OrGreater())
+ installHook(kbaseMod, k32Mod, "CopyFile2", hook_CopyFile2,
+ reinterpret_cast<LPVOID*>(&CopyFile2));
+
+ installHook(kbaseMod, k32Mod, "GetPrivateProfileStringA",
+ hook_GetPrivateProfileStringA);
+ installHook(kbaseMod, k32Mod, "GetPrivateProfileStringW",
+ hook_GetPrivateProfileStringW);
+ installHook(kbaseMod, k32Mod, "GetPrivateProfileSectionA",
+ hook_GetPrivateProfileSectionA);
+ installHook(kbaseMod, k32Mod, "GetPrivateProfileSectionW",
+ hook_GetPrivateProfileSectionW);
+ installHook(kbaseMod, k32Mod, "WritePrivateProfileStringA",
+ hook_WritePrivateProfileStringA);
+ installHook(kbaseMod, k32Mod, "WritePrivateProfileStringW",
+ hook_WritePrivateProfileStringW);
+
+ installHook(kbaseMod, k32Mod, "GetFullPathNameA", hook_GetFullPathNameA);
+ installHook(kbaseMod, k32Mod, "GetFullPathNameW", hook_GetFullPathNameW);
+
+ installHook(kbaseMod, k32Mod, "FindFirstFileExW", hook_FindFirstFileExW);
+
+ HMODULE ntdllMod = GetModuleHandleA("ntdll.dll");
+ spdlog::get("usvfs")->debug("ntdll.dll at {0:x}",
+ reinterpret_cast<uintptr_t>(ntdllMod));
+ installHook(ntdllMod, nullptr, "NtQueryFullAttributesFile",
+ hook_NtQueryFullAttributesFile);
+ installHook(ntdllMod, nullptr, "NtQueryAttributesFile", hook_NtQueryAttributesFile);
+ installHook(ntdllMod, nullptr, "NtQueryDirectoryFile", hook_NtQueryDirectoryFile);
+ installHook(ntdllMod, nullptr, "NtQueryDirectoryFileEx", hook_NtQueryDirectoryFileEx);
+ installHook(ntdllMod, nullptr, "NtQueryObject", hook_NtQueryObject);
+ installHook(ntdllMod, nullptr, "NtQueryInformationFile", hook_NtQueryInformationFile);
+ installHook(ntdllMod, nullptr, "NtQueryInformationByName",
+ hook_NtQueryInformationByName);
+ installHook(ntdllMod, nullptr, "NtOpenFile", hook_NtOpenFile);
+ installHook(ntdllMod, nullptr, "NtCreateFile", hook_NtCreateFile);
+ installHook(ntdllMod, nullptr, "NtClose", hook_NtClose);
+ installHook(ntdllMod, nullptr, "NtTerminateProcess", hook_NtTerminateProcess);
+
+ installHook(kbaseMod, k32Mod, "LoadLibraryExA", hook_LoadLibraryExA);
+ installHook(kbaseMod, k32Mod, "LoadLibraryExW", hook_LoadLibraryExW);
+
+ // install this hook late as usvfs is calling it itself for debugging purposes
+ installHook(kbaseMod, k32Mod, "GetModuleFileNameA", hook_GetModuleFileNameA);
+ installHook(kbaseMod, k32Mod, "GetModuleFileNameW", hook_GetModuleFileNameW);
+
+ spdlog::get("usvfs")->debug("hooks installed");
+ HookLib::TrampolinePool::instance().setBlock(false);
+}
+
+void HookManager::removeHooks()
+{
+ while (m_Hooks.size() > 0) {
+ auto iter = m_Hooks.begin();
+ try {
+ RemoveHook(iter->second);
+ spdlog::get("usvfs")->debug("removed hook {}", iter->first);
+ } catch (const std::exception& e) {
+ spdlog::get("usvfs")->critical("failed to remove hook: {}", e.what());
+ }
+
+ // remove either way, otherwise this is an endless loop
+ m_Hooks.erase(iter);
+ }
+}
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/hookmanager.h b/libs/usvfs/src/usvfs_dll/hookmanager.h
new file mode 100644
index 0000000..636a29c
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hookmanager.h
@@ -0,0 +1,81 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#pragma once
+
+#include "hookcontext.h"
+#include <hooklib.h>
+#include <usvfsparameters.h>
+
+namespace usvfs
+{
+
+class HookManager
+{
+public:
+ HookManager(const usvfsParameters& params, HMODULE module);
+ ~HookManager();
+
+ HookManager(const HookManager& reference) = delete;
+
+ HookManager& operator=(const HookManager& reference) = delete;
+
+ static HookManager& instance();
+
+ HookContext* context() { return &m_Context; }
+
+ ///
+ /// \brief retrieve address of the detour of a function
+ /// \param functionName name of the function to look up
+ /// \return function address that can be used to directly execute the original code
+ ///
+ LPVOID detour(const char* functionName);
+
+ ///
+ /// \brief remove the hook on the specified function.
+ /// \param functionName name of the function to unhook
+ /// \note This function is only exposed to allow a workaround for ExitProcess and may
+ /// be
+ /// removed if a better solution is found there. If you have another legit use
+ /// case, please let me know!
+ ///
+ void removeHook(const std::string& functionName);
+
+private:
+ void logStubInt(LPVOID address);
+ static void logStub(LPVOID address);
+
+ void installHook(HMODULE module1, HMODULE module2, const std::string& functionName,
+ LPVOID hook, LPVOID* fillFuncAddr);
+ void installStub(HMODULE module1, HMODULE module2, const std::string& functionName);
+ void initHooks();
+ void removeHooks();
+
+private:
+ static HookManager* s_Instance;
+
+ std::map<std::string, HookLib::HOOKHANDLE> m_Hooks;
+
+ std::map<LPVOID, std::string> m_Stubs;
+
+ HookContext m_Context;
+};
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h b/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h
new file mode 100644
index 0000000..dbd3597
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hooks/file_information_utils.h
@@ -0,0 +1,263 @@
+#pragma once
+
+/**
+ * This header defines function to deal with the various FileInformationClass
+ * enumeration.
+ */
+
+#include <type_traits>
+
+#include <ntdll_declarations.h>
+
+#include <windows.h>
+
+namespace usvfs::details
+{
+
+#define DECLARE_HAS_FIELD(Field) \
+ template <typename T> \
+ constexpr auto HasFieldImpl##Field(int)->decltype(std::declval<T>().Field, void(), \
+ std::true_type()) \
+ { \
+ return {}; \
+ } \
+ template <typename T> \
+ constexpr auto HasFieldImpl##Field(...) \
+ { \
+ return std::false_type{}; \
+ } \
+ template <typename T> \
+ constexpr auto HasField##Field() \
+ { \
+ return HasFieldImpl##Field<T>(0); \
+ }
+
+DECLARE_HAS_FIELD(FileName)
+DECLARE_HAS_FIELD(NextEntryOffset)
+DECLARE_HAS_FIELD(ShortName)
+
+#undef DECLARE_HAS_FIELD
+
+template <FILE_INFORMATION_CLASS fileInformationClass, class FileInformationClass>
+struct FileInformationClassUtilsImpl
+{
+ static void get_data(LPCVOID address, ULONG& offset, std::wstring& fileName)
+ {
+ const FileInformationClass* info =
+ reinterpret_cast<const FileInformationClass*>(address);
+
+ // default value
+ offset = sizeof(FileInformationClass);
+
+ if constexpr (HasFieldFileName<FileInformationClass>()) {
+ if constexpr (HasFieldNextEntryOffset<FileInformationClass>()) {
+ offset = info->NextEntryOffset;
+ }
+ fileName = std::wstring(info->FileName, info->FileNameLength / sizeof(WCHAR));
+ }
+ }
+
+ static void set_offset(LPVOID address, ULONG offset)
+ {
+ FileInformationClass* info = reinterpret_cast<FileInformationClass*>(address);
+
+ if constexpr (HasFieldNextEntryOffset<FileInformationClass>()) {
+ info->NextEntryOffset = offset;
+ }
+ }
+
+ static void set_filename(LPVOID address, const std::wstring& fileName)
+ {
+ FileInformationClass* info = reinterpret_cast<FileInformationClass*>(address);
+
+ if constexpr (HasFieldFileName<FileInformationClass>()) {
+ memset(info->FileName, L'\0', info->FileNameLength);
+ info->FileNameLength = static_cast<ULONG>(fileName.length() * sizeof(WCHAR));
+
+ // doesn't need to be 0-terminated
+ memcpy(info->FileName, fileName.c_str(), info->FileNameLength);
+
+ if constexpr (HasFieldShortName<FileInformationClass>()) {
+ if (info->ShortNameLength > 0) {
+ info->ShortNameLength = static_cast<CCHAR>(
+ GetShortPathNameW(fileName.c_str(), info->ShortName, 8));
+ }
+ }
+ }
+ }
+};
+
+template <FILE_INFORMATION_CLASS fileInformationClass>
+struct FileInformationClassUtils;
+
+template <>
+struct FileInformationClassUtils<FileDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileDirectoryInformation,
+ FILE_DIRECTORY_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileFullDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileFullDirectoryInformation,
+ FILE_FULL_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileBothDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileBothDirectoryInformation,
+ FILE_BOTH_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileStandardInformation>
+ : FileInformationClassUtilsImpl<FileStandardInformation, FILE_STANDARD_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileNameInformation>
+ : FileInformationClassUtilsImpl<FileNameInformation, FILE_NAME_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileRenameInformation>
+ : FileInformationClassUtilsImpl<FileRenameInformation, FILE_RENAME_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileNamesInformation>
+ : FileInformationClassUtilsImpl<FileNamesInformation, FILE_NAMES_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileObjectIdInformation>
+ : FileInformationClassUtilsImpl<FileObjectIdInformation, FILE_OBJECTID_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileReparsePointInformation>
+ : FileInformationClassUtilsImpl<FileReparsePointInformation,
+ FILE_REPARSE_POINT_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileIdBothDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileIdBothDirectoryInformation,
+ FILE_ID_BOTH_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileIdFullDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileIdFullDirectoryInformation,
+ FILE_ID_FULL_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileNormalizedNameInformation>
+ : FileInformationClassUtilsImpl<FileNormalizedNameInformation,
+ FILE_NAME_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileIdExtdDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileIdExtdDirectoryInformation,
+ FILE_ID_EXTD_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileIdExtdBothDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileIdExtdBothDirectoryInformation,
+ FILE_ID_EXTD_BOTH_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileId64ExtdDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileId64ExtdDirectoryInformation,
+ FILE_ID_64_EXTD_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileId64ExtdBothDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileId64ExtdBothDirectoryInformation,
+ FILE_ID_64_EXTD_BOTH_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileIdAllExtdDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileIdAllExtdDirectoryInformation,
+ FILE_ID_ALL_EXTD_DIR_INFORMATION>
+{};
+template <>
+struct FileInformationClassUtils<FileIdAllExtdBothDirectoryInformation>
+ : FileInformationClassUtilsImpl<FileIdAllExtdBothDirectoryInformation,
+ FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION>
+{};
+
+// FILE_ALL_INFORMATION needs to be handled differently because it has a field that
+// is itself a structure
+template <>
+struct FileInformationClassUtils<FileAllInformation>
+{
+ static void get_data(LPCVOID address, ULONG& offset, std::wstring& fileName)
+ {
+ FileInformationClassUtils<FileNameInformation>::get_data(
+ &reinterpret_cast<const FILE_ALL_INFORMATION*>(address)->NameInformation,
+ offset, fileName);
+ }
+
+ static void set_offset(LPVOID address, ULONG offset)
+ {
+ // this is a no-op but it's consistent to do that everywhere
+ FileInformationClassUtils<FileNameInformation>::set_offset(
+ &reinterpret_cast<FILE_ALL_INFORMATION*>(address)->NameInformation, offset);
+ }
+
+ static void set_filename(LPVOID address, const std::wstring& fileName)
+ {
+ FileInformationClassUtils<FileNameInformation>::set_filename(
+ &reinterpret_cast<FILE_ALL_INFORMATION*>(address)->NameInformation, fileName);
+ }
+};
+
+} // namespace usvfs::details
+
+#define _APP_FINFO_CASE(clazz, fn, ...) \
+ case clazz: \
+ return usvfs::details::FileInformationClassUtils<clazz>::fn(__VA_ARGS__);
+
+#define _APPLY_FILEINFO_FN(fn, ...) \
+ _APP_FINFO_CASE(FileAllInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileFullDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileBothDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileStandardInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileNameInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileRenameInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileNamesInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileObjectIdInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileReparsePointInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileIdBothDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileIdFullDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileNormalizedNameInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileIdExtdDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileIdExtdBothDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileId64ExtdDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileId64ExtdBothDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileIdAllExtdDirectoryInformation, fn, __VA_ARGS__) \
+ _APP_FINFO_CASE(FileIdAllExtdBothDirectoryInformation, fn, __VA_ARGS__)
+
+void GetFileInformationData(FILE_INFORMATION_CLASS fileInformationClass,
+ LPCVOID address, ULONG& offset, std::wstring& fileName)
+{
+ switch (fileInformationClass) {
+ _APPLY_FILEINFO_FN(get_data, address, offset, fileName);
+ default:
+ offset = ULONG_MAX;
+ }
+}
+
+void SetFileInformationOffset(FILE_INFORMATION_CLASS fileInformationClass,
+ LPVOID address, ULONG offset)
+{
+ switch (fileInformationClass) {
+ _APPLY_FILEINFO_FN(set_offset, address, offset);
+ default:
+ break;
+ }
+}
+
+void SetFileInformationFileName(FILE_INFORMATION_CLASS fileInformationClass,
+ LPVOID address, const std::wstring& fileName)
+{
+ switch (fileInformationClass) {
+ _APPLY_FILEINFO_FN(set_filename, address, fileName);
+ default:
+ break;
+ }
+}
+
+#undef _APP_FINFO_CASE
+#undef _APPLY_FILEINFO_FN
diff --git a/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp b/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp
new file mode 100644
index 0000000..6599eb0
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hooks/kernel32.cpp
@@ -0,0 +1,1860 @@
+#include "kernel32.h"
+#include "sharedids.h"
+
+#include "../hookcallcontext.h"
+#include "../hookcontext.h"
+#include "../hookmanager.h"
+#include "../maptracker.h"
+#include <formatters.h>
+#include <inject.h>
+#include <loghelpers.h>
+#include <stringcast.h>
+#include <stringutils.h>
+#include <usvfs.h>
+#include <winapi.h>
+#include <winbase.h>
+
+namespace ush = usvfs::shared;
+using ush::CodePage;
+using ush::string_cast;
+
+namespace usvfs
+{
+MapTracker k32DeleteTracker;
+MapTracker k32FakeDirTracker;
+} // namespace usvfs
+
+class CurrentDirectoryTracker
+{
+public:
+ using wstring = std::wstring;
+
+ bool get(wstring& currentDir, const wchar_t* forRelativePath = nullptr)
+ {
+ int index = m_currentDrive;
+ if (forRelativePath && *forRelativePath && forRelativePath[1] == ':')
+ if (!getDriveIndex(forRelativePath, index))
+ spdlog::get("usvfs")->warn(
+ "CurrentDirectoryTracker::get() invalid drive letter: {}, will use current "
+ "drive {}",
+ string_cast<std::string>(forRelativePath),
+ static_cast<char>('A' + index)); // prints '@' for m_currentDrive == -1
+ if (index < 0)
+ return false;
+
+ std::shared_lock<std::shared_mutex> lock(m_mutex);
+ if (m_perDrive[index].empty())
+ return false;
+ else {
+ currentDir = m_perDrive[index];
+ return true;
+ }
+ }
+
+ bool set(const wstring& currentDir)
+ {
+ int index = -1;
+ bool good = !currentDir.empty() && getDriveIndex(currentDir.c_str(), index) &&
+ currentDir[1] == ':';
+ std::unique_lock<std::shared_mutex> lock(m_mutex);
+ m_currentDrive = good ? index : -1;
+ if (good)
+ m_perDrive[index] = currentDir;
+ return good;
+ }
+
+private:
+ static bool getDriveIndex(const wchar_t* path, int& index)
+ {
+ if (*path >= 'a' && *path <= 'z')
+ index = *path - 'a';
+ else if (*path >= 'A' && *path <= 'Z')
+ index = *path - 'A';
+ else
+ return false;
+ return true;
+ }
+
+ mutable std::shared_mutex m_mutex;
+ wstring m_perDrive['z' - 'a' + 1];
+ int m_currentDrive{-1};
+};
+
+CurrentDirectoryTracker k32CurrentDirectoryTracker;
+
+// attempts to copy source to destination and return the error code
+static inline DWORD copyFileDirect(LPCWSTR source, LPCWSTR destination, bool overwrite)
+{
+ usvfs::FunctionGroupLock lock(usvfs::MutExHookGroup::SHELL_FILEOP);
+ return CopyFileExW(source, destination, NULL, NULL, NULL,
+ overwrite ? 0 : COPY_FILE_FAIL_IF_EXISTS)
+ ? ERROR_SUCCESS
+ : GetLastError();
+}
+
+static inline WCHAR pathNameDriveLetter(LPCWSTR path)
+{
+ if (!path || !path[0])
+ return 0;
+ if (path[1] == ':')
+ return path[0];
+ // if path is not ?: or \* then we need to get absolute path:
+ std::wstring buf;
+ if (path[0] != '\\') {
+ buf = winapi::wide::getFullPathName(path).first;
+ path = buf.c_str();
+ if (!path[0] || path[1] == ':')
+ return path[0];
+ }
+ // check for \??\C:
+ if (path[1] && path[2] && path[3] && path[4] && path[0] == '\\' && path[3] == '\\' &&
+ path[5] == ':')
+ return path[4];
+ // give up
+ return 0;
+}
+
+// returns false also in case we fail to determine the drive letter of the path
+static inline bool pathsOnDifferentDrives(LPCWSTR path1, LPCWSTR path2)
+{
+ WCHAR drive1 = pathNameDriveLetter(path1);
+ WCHAR drive2 = pathNameDriveLetter(path2);
+ return drive1 && drive2 && towupper(drive1) != towupper(drive2);
+}
+
+HMODULE WINAPI usvfs::hook_LoadLibraryExA(LPCSTR lpFileName, HANDLE hFile,
+ DWORD dwFlags)
+{
+ HMODULE res = nullptr;
+
+ HOOK_START_GROUP(MutExHookGroup::LOAD_LIBRARY)
+ const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName);
+
+ PRE_REALCALL
+ res = LoadLibraryExW(fileName.c_str(), hFile, dwFlags);
+ POST_REALCALL
+
+ HOOK_END
+ return res;
+}
+
+HMODULE WINAPI usvfs::hook_LoadLibraryExW(LPCWSTR lpFileName, HANDLE hFile,
+ DWORD dwFlags)
+{
+ HMODULE res = nullptr;
+
+ HOOK_START_GROUP(MutExHookGroup::LOAD_LIBRARY)
+ // Why is the usual if (!callContext.active()... check missing?
+
+ RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName);
+ PRE_REALCALL
+ res = ::LoadLibraryExW(reroute.fileName(), hFile, dwFlags);
+ POST_REALCALL
+
+ if (reroute.wasRerouted()) {
+ LOG_CALL()
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+/// determine name of the binary to run based on parameters for createprocess
+std::wstring getBinaryName(LPCWSTR applicationName, LPCWSTR lpCommandLine)
+{
+ if (applicationName != nullptr) {
+ std::pair<std::wstring, std::wstring> fullPath =
+ winapi::wide::getFullPathName(applicationName);
+ return fullPath.second;
+ } else {
+ if (lpCommandLine[0] == '"') {
+ const wchar_t* endQuote = wcschr(lpCommandLine, '"');
+ if (endQuote != nullptr) {
+ return std::wstring(lpCommandLine + 1, endQuote - 1);
+ }
+ }
+
+ // according to the documentation, if the commandline is unquoted and has
+ // spaces, it will be interpreted in multiple ways, i.e.
+ // c:\program.exe files\sub dir\program name
+ // c:\program files\sub.exe dir\program name
+ // c:\program files\sub dir\program.exe name
+ // c:\program files\sub dir\program name.exe
+ LPCWSTR space = wcschr(lpCommandLine, L' ');
+ while (space != nullptr) {
+ std::wstring subString(lpCommandLine, space);
+ bool isDirectory = true;
+ if (winapi::ex::wide::fileExists(subString.c_str(), &isDirectory) &&
+ !isDirectory) {
+ return subString;
+ } else {
+ space = wcschr(space + 1, L' ');
+ }
+ }
+ return std::wstring(lpCommandLine);
+ }
+}
+
+BOOL(WINAPI* usvfs::CreateProcessInternalW)(
+ LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine,
+ LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes,
+ BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment,
+ LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo,
+ LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken);
+
+BOOL WINAPI usvfs::hook_CreateProcessInternalW(
+ LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine,
+ LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes,
+ BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment,
+ LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo,
+ LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::CREATE_PROCESS)
+ if (!callContext.active()) {
+ res = CreateProcessInternalW(
+ token, lpApplicationName, lpCommandLine, lpProcessAttributes,
+ lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment,
+ lpCurrentDirectory, lpStartupInfo, lpProcessInformation, newToken);
+ callContext.updateLastError();
+ return res;
+ }
+
+ // remember if the caller wanted the process to be suspended. If so, we
+ // don't resume when we're done
+ BOOL susp = dwCreationFlags & CREATE_SUSPENDED;
+ dwCreationFlags |= CREATE_SUSPENDED;
+
+ RerouteW applicationReroute;
+ RerouteW cmdReroute;
+ LPWSTR cend = nullptr;
+
+ std::wstring dllPath;
+ usvfsParameters callParameters;
+
+ { // scope for context lock
+ auto context = READ_CONTEXT();
+
+ if (RerouteW::interestingPath(lpCommandLine)) {
+ // First "argument" in the commandline is the command, we need to identify it and
+ // reroute it:
+ if (*lpCommandLine == '"') {
+ // If the first argument is quoted we trust its is quoted correctly
+ for (cend = lpCommandLine; *cend && *cend != ' '; ++cend)
+ if (*cend == '"') {
+ int escaped = 0;
+ for (++cend; *cend && (*cend != '"' || escaped % 2 != 0); ++cend)
+ escaped = *cend == '\\' ? escaped + 1 : 0;
+ }
+
+ if (*(cend - 1) == '"')
+ --cend;
+ auto old_cend = *cend;
+ *cend = 0;
+ cmdReroute = RerouteW::create(context, callContext, lpCommandLine + 1);
+ *cend = old_cend;
+ if (old_cend == '"')
+ ++cend;
+ } else {
+ // If the first argument we have no choice but to test all the options to quote
+ // the command as the real CreateProcess will do this:
+ cend = lpCommandLine;
+ while (true) {
+ while (*cend && *cend != ' ')
+ ++cend;
+
+ auto old_cend = *cend;
+ *cend = 0;
+ cmdReroute = RerouteW::create(context, callContext, lpCommandLine);
+ *cend = old_cend;
+ if (cmdReroute.wasRerouted() || pathIsFile(cmdReroute.fileName()))
+ break;
+
+ while (*cend == ' ')
+ ++cend;
+
+ if (!*cend) {
+ // if we reached the end of the string we'll just use the whole commandline
+ // as is:
+ cend = nullptr;
+ break;
+ }
+ }
+ }
+ }
+
+ applicationReroute = RerouteW::create(context, callContext, lpApplicationName);
+
+ dllPath = context->dllPath();
+ callParameters = context->callParameters();
+ }
+
+ std::wstring cmdline;
+ if (cend && cmdReroute.fileName()) {
+ auto fileName = cmdReroute.fileName();
+ cmdline.reserve(wcslen(fileName) + wcslen(cend) + 2);
+ if (*fileName != '"')
+ cmdline += L"\"";
+ cmdline += fileName;
+ if (*fileName != '"')
+ cmdline += L"\"";
+ cmdline += cend;
+ }
+
+ PRE_REALCALL
+ res = CreateProcessInternalW(token, applicationReroute.fileName(),
+ cmdline.empty() ? lpCommandLine : &cmdline[0],
+ lpProcessAttributes, lpThreadAttributes, bInheritHandles,
+ dwCreationFlags, lpEnvironment, lpCurrentDirectory,
+ lpStartupInfo, lpProcessInformation, newToken);
+ POST_REALCALL
+
+ BOOL blacklisted = FALSE;
+ { // limit scope of context
+ auto context = READ_CONTEXT();
+ blacklisted = context->executableBlacklisted(applicationReroute.fileName(),
+ cmdReroute.fileName());
+ }
+
+ if (res) {
+ if (!blacklisted) {
+ try {
+ injectProcess(dllPath, callParameters, *lpProcessInformation);
+ } catch (const std::exception& e) {
+ spdlog::get("hooks")->error("failed to inject into {0}: {1}",
+ lpApplicationName != nullptr
+ ? applicationReroute.fileName()
+ : static_cast<LPCWSTR>(lpCommandLine),
+ e.what());
+ }
+ }
+
+ // resume unless process is supposed to start suspended
+ if (!susp && (ResumeThread(lpProcessInformation->hThread) == (DWORD)-1)) {
+ spdlog::get("hooks")->error("failed to inject into spawned process");
+ res = FALSE;
+ }
+ }
+
+ LOG_CALL()
+ .PARAM(lpApplicationName)
+ .PARAM(applicationReroute.fileName())
+ .PARAM(cmdReroute.fileName())
+ .PARAM(res)
+ .PARAM(callContext.lastError())
+ .PARAM(cmdline);
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_GetFileAttributesExA(LPCSTR lpFileName,
+ GET_FILEEX_INFO_LEVELS fInfoLevelId,
+ LPVOID lpFileInformation)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = GetFileAttributesExA(lpFileName, fInfoLevelId, lpFileInformation);
+ callContext.updateLastError();
+ return res;
+ }
+ HOOK_END
+
+ HOOK_START
+ const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName);
+
+ PRE_REALCALL
+ res = GetFileAttributesExW(fileName.c_str(), fInfoLevelId, lpFileInformation);
+ POST_REALCALL
+
+ HOOK_END
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_GetFileAttributesExW(LPCWSTR lpFileName,
+ GET_FILEEX_INFO_LEVELS fInfoLevelId,
+ LPVOID lpFileInformation)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = GetFileAttributesExW(lpFileName, fInfoLevelId, lpFileInformation);
+ callContext.updateLastError();
+ return res;
+ }
+
+ fs::path canonicalFile = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName));
+
+ RerouteW reroute =
+ RerouteW::create(READ_CONTEXT(), callContext, canonicalFile.c_str());
+
+ PRE_REALCALL
+ res = ::GetFileAttributesExW(reroute.fileName(), fInfoLevelId, lpFileInformation);
+ POST_REALCALL
+
+ DWORD originalError = callContext.lastError();
+ DWORD fixedError = originalError;
+ // In case the target does not exist the error value varies to differentiate if the
+ // parent folder exists (ERROR_FILE_NOT_FOUND) or not (ERROR_PATH_NOT_FOUND).
+ // If the original target's parent folder doesn't actually exist it may exist in the
+ // the virtualized sense, or if we rerouted the query the parent of the original path
+ // might exist while the parent of the rerouted path might not:
+ if (!res && fixedError == ERROR_PATH_NOT_FOUND) {
+ // first query original file parent (if we rerouted it):
+ fs::path originalParent = canonicalFile.parent_path();
+ WIN32_FILE_ATTRIBUTE_DATA parentAttr;
+ if (reroute.wasRerouted() &&
+ ::GetFileAttributesExW(originalParent.c_str(), GetFileExInfoStandard,
+ &parentAttr) &&
+ (parentAttr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
+ fixedError = ERROR_FILE_NOT_FOUND;
+ else {
+ // now query the rerouted path for parent (which can be different from the parent
+ // of the rerouted path)
+ RerouteW rerouteParent =
+ RerouteW::create(READ_CONTEXT(), callContext, originalParent.c_str());
+ if (rerouteParent.wasRerouted() &&
+ ::GetFileAttributesExW(rerouteParent.fileName(), GetFileExInfoStandard,
+ &parentAttr) &&
+ (parentAttr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
+ fixedError = ERROR_FILE_NOT_FOUND;
+ }
+ }
+ if (fixedError != originalError)
+ callContext.updateLastError(fixedError);
+
+ if (reroute.wasRerouted() || fixedError != originalError) {
+ DWORD resAttrib;
+ if (res && fInfoLevelId == GetFileExInfoStandard && lpFileInformation)
+ resAttrib = reinterpret_cast<WIN32_FILE_ATTRIBUTE_DATA*>(lpFileInformation)
+ ->dwFileAttributes;
+ else
+ resAttrib = (DWORD)-1;
+ LOG_CALL()
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAMHEX(fInfoLevelId)
+ .PARAMHEX(res)
+ .PARAMHEX(resAttrib)
+ .PARAM(originalError)
+ .PARAM(fixedError);
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetFileAttributesA(LPCSTR lpFileName)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = GetFileAttributesA(lpFileName);
+ callContext.updateLastError();
+ return res;
+ }
+ HOOK_END
+
+ HOOK_START
+ const std::wstring fileName = ush::string_cast<std::wstring>(lpFileName);
+
+ PRE_REALCALL
+ res = GetFileAttributesW(fileName.c_str());
+ POST_REALCALL
+
+ HOOK_END
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetFileAttributesW(LPCWSTR lpFileName)
+{
+ DWORD res = 0UL;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = GetFileAttributesW(lpFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ fs::path canonicalFile = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName));
+
+ RerouteW reroute =
+ RerouteW::create(READ_CONTEXT(), callContext, canonicalFile.c_str());
+
+ if (reroute.wasRerouted())
+ PRE_REALCALL
+ res = ::GetFileAttributesW(reroute.fileName());
+ POST_REALCALL
+
+ DWORD originalError = callContext.lastError();
+ DWORD fixedError = originalError;
+ // In case the target does not exist the error value varies to differentiate if the
+ // parent folder exists (ERROR_FILE_NOT_FOUND) or not (ERROR_PATH_NOT_FOUND).
+ // If the original target's parent folder doesn't actually exist it may exist in the
+ // the virtualized sense, or if we rerouted the query the parent of the original path
+ // might exist while the parent of the rerouted path might not:
+ if (res == INVALID_FILE_ATTRIBUTES && fixedError == ERROR_PATH_NOT_FOUND) {
+ // first query original file parent (if we rerouted it):
+ fs::path originalParent = canonicalFile.parent_path();
+ DWORD attr;
+ if (reroute.wasRerouted() &&
+ (attr = ::GetFileAttributesW(originalParent.c_str())) !=
+ INVALID_FILE_ATTRIBUTES &&
+ (attr & FILE_ATTRIBUTE_DIRECTORY))
+ fixedError = ERROR_FILE_NOT_FOUND;
+ else {
+ // now query the rerouted path for parent (which can be different from the parent
+ // of the rerouted path)
+ RerouteW rerouteParent =
+ RerouteW::create(READ_CONTEXT(), callContext, originalParent.c_str());
+ if (rerouteParent.wasRerouted() &&
+ (attr = ::GetFileAttributesW(rerouteParent.fileName())) !=
+ INVALID_FILE_ATTRIBUTES &&
+ (attr & FILE_ATTRIBUTE_DIRECTORY))
+ fixedError = ERROR_FILE_NOT_FOUND;
+ }
+ }
+ if (fixedError != originalError)
+ callContext.updateLastError(fixedError);
+
+ if (reroute.wasRerouted() || fixedError != originalError) {
+ LOG_CALL()
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAMHEX(res)
+ .PARAM(originalError)
+ .PARAM(fixedError);
+ }
+
+ HOOK_ENDP(lpFileName);
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_SetFileAttributesW(LPCWSTR lpFileName, DWORD dwFileAttributes)
+{
+ DWORD res = 0UL;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+ // Why is the usual if (!callContext.active()... check missing?
+
+ RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName);
+ PRE_REALCALL
+ res = ::SetFileAttributesW(reroute.fileName(), dwFileAttributes);
+ POST_REALCALL
+
+ if (reroute.wasRerouted()) {
+ LOG_CALL().PARAM(reroute.fileName()).PARAM(res).PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::DELETE_FILE)
+ // Why is the usual if (!callContext.active()... check missing?
+
+ const std::wstring path =
+ RerouteW::canonizePath(RerouteW::absolutePath(lpFileName)).wstring();
+
+ RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, path.c_str());
+
+ PRE_REALCALL
+ if (reroute.wasRerouted()) {
+ res = ::DeleteFileW(reroute.fileName());
+ } else {
+ res = ::DeleteFileW(path.c_str());
+ }
+ POST_REALCALL
+
+ if (res) {
+ reroute.removeMapping(READ_CONTEXT());
+ }
+
+ if (reroute.wasRerouted())
+ LOG_CALL()
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL rewriteChangedDrives(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName,
+ const usvfs::RerouteW& readReroute,
+ const usvfs::CreateRerouter& writeReroute)
+{
+ return ((readReroute.wasRerouted() || writeReroute.wasRerouted()) &&
+ pathsOnDifferentDrives(readReroute.fileName(), writeReroute.fileName()) &&
+ !pathsOnDifferentDrives(lpExistingFileName, lpNewFileName));
+}
+
+BOOL WINAPI usvfs::hook_MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP)
+
+ if (!callContext.active()) {
+ res = MoveFileA(lpExistingFileName, lpNewFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ HOOK_END
+ HOOK_START
+
+ const auto& existingFileName = ush::string_cast<std::wstring>(lpExistingFileName);
+ const auto& newFileName = ush::string_cast<std::wstring>(lpNewFileName);
+
+ PRE_REALCALL
+ res = MoveFileW(existingFileName.c_str(), newFileName.c_str());
+ POST_REALCALL
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP)
+ if (!callContext.active()) {
+ res = MoveFileW(lpExistingFileName, lpNewFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW readReroute;
+ CreateRerouter writeReroute;
+ bool callOriginal = true;
+ DWORD newFlags = 0;
+
+ {
+ auto context = READ_CONTEXT();
+ readReroute = RerouteW::create(context, callContext, lpExistingFileName);
+ callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName, false,
+ "hook_MoveFileW");
+ }
+
+ if (callOriginal) {
+ bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName,
+ readReroute, writeReroute);
+ if (movedDrives)
+ newFlags |= MOVEFILE_COPY_ALLOWED;
+
+ bool isDirectory = pathIsDirectory(readReroute.fileName());
+
+ PRE_REALCALL
+ if (isDirectory && movedDrives) {
+ SHFILEOPSTRUCTW sf = {0};
+ sf.wFunc = FO_MOVE;
+ sf.hwnd = 0;
+ sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI;
+ sf.pFrom = readReroute.fileName();
+ sf.pTo = writeReroute.fileName();
+ int shRes = ::SHFileOperationW(&sf);
+ switch (shRes) {
+ case 0x78:
+ callContext.updateLastError(ERROR_ACCESS_DENIED);
+ break;
+ case 0x7C:
+ callContext.updateLastError(ERROR_FILE_NOT_FOUND);
+ break;
+ case 0x7E:
+ case 0x80:
+ callContext.updateLastError(ERROR_FILE_EXISTS);
+ break;
+ default:
+ callContext.updateLastError(shRes);
+ }
+ res = shRes == 0;
+ } else if (newFlags)
+ res = ::MoveFileExW(readReroute.fileName(), writeReroute.fileName(), newFlags);
+ else
+ res = ::MoveFileW(readReroute.fileName(), writeReroute.fileName());
+ POST_REALCALL
+
+ if (res)
+ SetLastError(ERROR_SUCCESS);
+
+ writeReroute.updateResult(callContext, res);
+
+ if (res) {
+ readReroute.removeMapping(
+ READ_CONTEXT(), isDirectory); // Updating the rerouteCreate to check deleted
+ // file entries should make this okay
+
+ if (writeReroute.newReroute()) {
+ if (isDirectory)
+ RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName),
+ fs::path(writeReroute.fileName()));
+ else
+ writeReroute.insertMapping(WRITE_CONTEXT());
+ }
+ }
+
+ if (readReroute.wasRerouted() || writeReroute.wasRerouted() ||
+ writeReroute.changedError())
+ LOG_CALL()
+ .PARAM(readReroute.fileName())
+ .PARAM(writeReroute.fileName())
+ .PARAMWRAP(newFlags)
+ .PARAM(res)
+ .PARAM(writeReroute.originalError())
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName,
+ DWORD dwFlags)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP)
+
+ if (!callContext.active()) {
+ res = MoveFileExA(lpExistingFileName, lpNewFileName, dwFlags);
+ callContext.updateLastError();
+ return res;
+ }
+
+ HOOK_END
+ HOOK_START
+
+ const std::wstring existingFileName =
+ ush::string_cast<std::wstring>(lpExistingFileName);
+
+ // careful: lpNewFileName can be null if dwFlags is
+ // MOVEFILE_DELAY_UNTIL_REBOOT, so don't blindly cast the string and make sure
+ // the null pointer is forwarded correctly
+ std::wstring newFileNameWstring;
+ const wchar_t* newFileName = nullptr;
+
+ if (lpNewFileName) {
+ newFileNameWstring = ush::string_cast<std::wstring>(lpNewFileName);
+ newFileName = newFileNameWstring.c_str();
+ }
+
+ PRE_REALCALL
+ res = MoveFileExW(existingFileName.c_str(), newFileName, dwFlags);
+ POST_REALCALL
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName,
+ DWORD dwFlags)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP)
+ if (!callContext.active()) {
+ res = MoveFileExW(lpExistingFileName, lpNewFileName, dwFlags);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW readReroute;
+ CreateRerouter writeReroute;
+ bool callOriginal = true;
+ DWORD newFlags = dwFlags;
+
+ {
+ auto context = READ_CONTEXT();
+ readReroute = RerouteW::create(context, callContext, lpExistingFileName);
+ callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName,
+ newFlags & MOVEFILE_REPLACE_EXISTING,
+ "hook_MoveFileExW");
+ }
+
+ if (callOriginal) {
+ bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName,
+ readReroute, writeReroute);
+
+ bool isDirectory = pathIsDirectory(readReroute.fileName());
+
+ PRE_REALCALL
+ if (isDirectory && movedDrives) {
+ SHFILEOPSTRUCTW sf = {0};
+ sf.wFunc = FO_MOVE;
+ sf.hwnd = 0;
+ sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI;
+ sf.pFrom = readReroute.fileName();
+ sf.pTo = writeReroute.fileName();
+ int shRes = ::SHFileOperationW(&sf);
+ switch (shRes) {
+ case 0x78:
+ callContext.updateLastError(ERROR_ACCESS_DENIED);
+ break;
+ case 0x7C:
+ callContext.updateLastError(ERROR_FILE_NOT_FOUND);
+ break;
+ case 0x7E:
+ case 0x80:
+ callContext.updateLastError(ERROR_FILE_EXISTS);
+ break;
+ default:
+ callContext.updateLastError(shRes);
+ }
+ res = shRes == 0;
+ } else
+ res = ::MoveFileExW(readReroute.fileName(), writeReroute.fileName(), newFlags);
+ POST_REALCALL
+
+ if (res)
+ SetLastError(ERROR_SUCCESS);
+
+ writeReroute.updateResult(callContext, res);
+
+ if (res) {
+ readReroute.removeMapping(
+ READ_CONTEXT(), isDirectory); // Updating the rerouteCreate to check deleted
+ // file entries should make this okay
+
+ if (writeReroute.newReroute()) {
+ if (isDirectory)
+ RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName),
+ fs::path(writeReroute.fileName()));
+ else
+ writeReroute.insertMapping(WRITE_CONTEXT());
+ }
+ }
+
+ if (readReroute.wasRerouted() || writeReroute.wasRerouted() ||
+ writeReroute.changedError())
+ LOG_CALL()
+ .PARAM(readReroute.fileName())
+ .PARAM(writeReroute.fileName())
+ .PARAMWRAP(dwFlags)
+ .PARAMWRAP(newFlags)
+ .PARAM(res)
+ .PARAM(writeReroute.originalError())
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_MoveFileWithProgressA(LPCSTR lpExistingFileName,
+ LPCSTR lpNewFileName,
+ LPPROGRESS_ROUTINE lpProgressRoutine,
+ LPVOID lpData, DWORD dwFlags)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP)
+
+ if (!callContext.active()) {
+ res = MoveFileWithProgressA(lpExistingFileName, lpNewFileName, lpProgressRoutine,
+ lpData, dwFlags);
+ callContext.updateLastError();
+ return res;
+ }
+
+ HOOK_END
+ HOOK_START
+
+ const auto& existingFileName = ush::string_cast<std::wstring>(lpExistingFileName);
+
+ // careful: lpNewFileName can be null if dwFlags is
+ // MOVEFILE_DELAY_UNTIL_REBOOT, so don't blindly cast the string and make sure
+ // the null pointer is forwarded correctly
+ std::wstring newFileNameWstring;
+ const wchar_t* newFileName = nullptr;
+
+ if (lpNewFileName) {
+ newFileNameWstring = ush::string_cast<std::wstring>(lpNewFileName);
+ newFileName = newFileNameWstring.c_str();
+ }
+
+ PRE_REALCALL
+ res = MoveFileWithProgressW(existingFileName.c_str(), newFileName, lpProgressRoutine,
+ lpData, dwFlags);
+ POST_REALCALL
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_MoveFileWithProgressW(LPCWSTR lpExistingFileName,
+ LPCWSTR lpNewFileName,
+ LPPROGRESS_ROUTINE lpProgressRoutine,
+ LPVOID lpData, DWORD dwFlags)
+{
+
+ // TODO: Remove all redundant hooks to moveFile alternatives.
+ // it would appear that all other moveFile functions end up calling this one with no
+ // additional code.
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP)
+ if (!callContext.active()) {
+ res = MoveFileWithProgressW(lpExistingFileName, lpNewFileName, lpProgressRoutine,
+ lpData, dwFlags);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW readReroute;
+ usvfs::CreateRerouter writeReroute;
+ bool callOriginal = true;
+ DWORD newFlags = dwFlags;
+
+ {
+ auto context = READ_CONTEXT();
+ readReroute = RerouteW::create(context, callContext, lpExistingFileName);
+ callOriginal = writeReroute.rerouteNew(context, callContext, lpNewFileName,
+ newFlags & MOVEFILE_REPLACE_EXISTING,
+ "hook_MoveFileWithProgressW");
+ }
+
+ if (callOriginal) {
+ bool movedDrives = rewriteChangedDrives(lpExistingFileName, lpNewFileName,
+ readReroute, writeReroute);
+ if (movedDrives)
+ newFlags |= MOVEFILE_COPY_ALLOWED;
+
+ bool isDirectory = pathIsDirectory(readReroute.fileName());
+
+ PRE_REALCALL
+ if (isDirectory && movedDrives) {
+ SHFILEOPSTRUCTW sf = {0};
+ sf.wFunc = FO_MOVE;
+ sf.hwnd = 0;
+ sf.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_NOERRORUI;
+ sf.pFrom = readReroute.fileName();
+ sf.pTo = writeReroute.fileName();
+ int shRes = ::SHFileOperationW(&sf);
+ switch (shRes) {
+ case 0x78:
+ callContext.updateLastError(ERROR_ACCESS_DENIED);
+ break;
+ case 0x7C:
+ callContext.updateLastError(ERROR_FILE_NOT_FOUND);
+ break;
+ case 0x7E:
+ case 0x80:
+ callContext.updateLastError(ERROR_FILE_EXISTS);
+ break;
+ default:
+ callContext.updateLastError(shRes);
+ }
+ res = shRes == 0;
+ } else
+ res = ::MoveFileWithProgressW(readReroute.fileName(), writeReroute.fileName(),
+ lpProgressRoutine, lpData, newFlags);
+ POST_REALCALL
+
+ if (res)
+ SetLastError(ERROR_SUCCESS);
+
+ writeReroute.updateResult(callContext, res);
+
+ if (res) {
+ // TODO: this call causes the node to be removed twice in case of
+ // MOVEFILE_COPY_ALLOWED as the deleteFile hook lower level already takes care of
+ // it, but deleteFile can't be disabled since we are relying on it in case of
+ // MOVEFILE_REPLACE_EXISTING for the destination file.
+ readReroute.removeMapping(
+ READ_CONTEXT(),
+ isDirectory); // Updating the rerouteCreate to check deleted file entries
+ // should make this okay (not related to comments above)
+
+ if (writeReroute.newReroute()) {
+ if (isDirectory)
+ RerouteW::addDirectoryMapping(WRITE_CONTEXT(), fs::path(lpNewFileName),
+ fs::path(writeReroute.fileName()));
+ else
+ writeReroute.insertMapping(WRITE_CONTEXT());
+ }
+ }
+
+ if (readReroute.wasRerouted() || writeReroute.wasRerouted() ||
+ writeReroute.changedError())
+ LOG_CALL()
+ .PARAM(readReroute.fileName())
+ .PARAM(writeReroute.fileName())
+ .PARAMWRAP(dwFlags)
+ .PARAMWRAP(newFlags)
+ .PARAM(res)
+ .PARAM(writeReroute.originalError())
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_CopyFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName,
+ LPPROGRESS_ROUTINE lpProgressRoutine, LPVOID lpData,
+ LPBOOL pbCancel, DWORD dwCopyFlags)
+{
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP)
+ if (!callContext.active()) {
+ res = CopyFileExW(lpExistingFileName, lpNewFileName, lpProgressRoutine, lpData,
+ pbCancel, dwCopyFlags);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW readReroute;
+ usvfs::CreateRerouter writeReroute;
+ bool callOriginal = true;
+
+ {
+ auto context = READ_CONTEXT();
+ readReroute = RerouteW::create(context, callContext, lpExistingFileName);
+ callOriginal = writeReroute.rerouteNew(
+ context, callContext, lpNewFileName,
+ (dwCopyFlags & COPY_FILE_FAIL_IF_EXISTS) == 0, "hook_CopyFileExW");
+ }
+
+ if (callOriginal) {
+ PRE_REALCALL
+ res = ::CopyFileExW(readReroute.fileName(), writeReroute.fileName(),
+ lpProgressRoutine, lpData, pbCancel, dwCopyFlags);
+ POST_REALCALL
+ writeReroute.updateResult(callContext, res);
+
+ if (res && writeReroute.newReroute())
+ writeReroute.insertMapping(WRITE_CONTEXT());
+
+ if (readReroute.wasRerouted() || writeReroute.wasRerouted() ||
+ writeReroute.changedError())
+ LOG_CALL()
+ .PARAM(readReroute.fileName())
+ .PARAM(writeReroute.fileName())
+ .PARAM(res)
+ .PARAM(writeReroute.originalError())
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer)
+{
+ DWORD res = 0;
+
+ HOOK_START
+
+ std::wstring buffer;
+ buffer.resize(nBufferLength);
+
+ PRE_REALCALL
+ res = GetCurrentDirectoryW(nBufferLength, &buffer[0]);
+ POST_REALCALL
+
+ if (res > 0) {
+ res = WideCharToMultiByte(CP_ACP, 0, buffer.c_str(), res + 1, lpBuffer,
+ nBufferLength, nullptr, nullptr);
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer)
+{
+ DWORD res = FALSE;
+
+ HOOK_START
+
+ std::wstring actualCWD;
+
+ if (!k32CurrentDirectoryTracker.get(actualCWD)) {
+ PRE_REALCALL
+ res = ::GetCurrentDirectoryW(nBufferLength, lpBuffer);
+ POST_REALCALL
+ } else {
+ ush::wcsncpy_sz(lpBuffer, &actualCWD[0],
+ std::min(static_cast<size_t>(nBufferLength), actualCWD.size() + 1));
+
+ // yupp, that's how GetCurrentDirectory actually works...
+ if (actualCWD.size() < nBufferLength) {
+ res = static_cast<DWORD>(actualCWD.size());
+ } else {
+ res = static_cast<DWORD>(actualCWD.size() + 1);
+ }
+ }
+
+ if (nBufferLength)
+ LOG_CALL()
+ .PARAM(std::wstring(lpBuffer, res))
+ .PARAM(nBufferLength)
+ .PARAM(actualCWD.size())
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_SetCurrentDirectoryA(LPCSTR lpPathName)
+{
+ return SetCurrentDirectoryW(ush::string_cast<std::wstring>(lpPathName).c_str());
+}
+
+BOOL WINAPI usvfs::hook_SetCurrentDirectoryW(LPCWSTR lpPathName)
+{
+ BOOL res = FALSE;
+
+ HOOK_START
+
+ const fs::path& realPath = RerouteW::canonizePath(RerouteW::absolutePath(lpPathName));
+ const std::wstring& realPathStr = realPath.wstring();
+ std::wstring finalRoute;
+ BOOL found = FALSE;
+
+ if (fs::exists(realPath))
+ finalRoute = realPathStr;
+ else {
+ WCHAR processDir[MAX_PATH];
+ if (::GetModuleFileNameW(NULL, processDir, MAX_PATH) != 0 &&
+ ::PathRemoveFileSpecW(processDir)) {
+ WCHAR processName[MAX_PATH];
+ ::GetModuleFileNameW(NULL, processName, MAX_PATH);
+ fs::path routedName = realPath / processName;
+ RerouteW rerouteTest =
+ RerouteW::create(READ_CONTEXT(), callContext, routedName.wstring().c_str());
+ if (rerouteTest.wasRerouted()) {
+ std::wstring reroutedPath = rerouteTest.fileName();
+ if (routedName.wstring().find(processDir) != std::string::npos) {
+ fs::path finalPath(reroutedPath);
+ finalRoute = finalPath.parent_path().wstring();
+ found = TRUE;
+ }
+ }
+ }
+
+ if (!found) {
+ RerouteW reroute =
+ RerouteW::create(READ_CONTEXT(), callContext, realPathStr.c_str());
+ finalRoute = reroute.fileName();
+ }
+ }
+
+ PRE_REALCALL
+ res = ::SetCurrentDirectoryW(finalRoute.c_str());
+ POST_REALCALL
+
+ if (res)
+ if (!k32CurrentDirectoryTracker.set(realPathStr))
+ spdlog::get("usvfs")->warn("Updating actual current directory failed: {} ?!",
+ string_cast<std::string>(realPathStr));
+
+ LOG_CALL()
+ .PARAM(lpPathName)
+ .PARAM(realPathStr)
+ .PARAM(finalRoute)
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+
+ HOOK_END
+
+ return res;
+}
+
+DLLEXPORT BOOL WINAPI usvfs::hook_CreateDirectoryW(
+ LPCWSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes)
+{
+ BOOL res = FALSE;
+ HOOK_START
+
+ RerouteW reroute = RerouteW::createOrNew(READ_CONTEXT(), callContext, lpPathName);
+
+ PRE_REALCALL
+ res = ::CreateDirectoryW(reroute.fileName(), lpSecurityAttributes);
+ POST_REALCALL
+
+ if (res && reroute.newReroute())
+ reroute.insertMapping(WRITE_CONTEXT(), true);
+
+ if (reroute.wasRerouted())
+ LOG_CALL()
+ .PARAM(lpPathName)
+ .PARAM(reroute.fileName())
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+
+ HOOK_END
+
+ return res;
+}
+
+DLLEXPORT BOOL WINAPI usvfs::hook_RemoveDirectoryW(LPCWSTR lpPathName)
+{
+
+ BOOL res = FALSE;
+
+ HOOK_START_GROUP(MutExHookGroup::DELETE_FILE)
+ // Why is the usual if (!callContext.active()... check missing?
+
+ RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpPathName);
+
+ PRE_REALCALL
+ if (reroute.wasRerouted()) {
+ res = ::RemoveDirectoryW(reroute.fileName());
+ } else {
+ res = ::RemoveDirectoryW(lpPathName);
+ }
+ POST_REALCALL
+
+ if (res) {
+ reroute.removeMapping(READ_CONTEXT(), true);
+ }
+
+ if (reroute.wasRerouted())
+ LOG_CALL()
+ .PARAM(lpPathName)
+ .PARAM(reroute.fileName())
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength,
+ LPSTR lpBuffer, LPSTR* lpFilePart)
+{
+ DWORD res = 0UL;
+
+ HOOK_START_GROUP(MutExHookGroup::FULL_PATHNAME)
+ if (!callContext.active()) {
+ res = GetFullPathNameA(lpFileName, nBufferLength, lpBuffer, lpFilePart);
+ callContext.updateLastError();
+ return res;
+ }
+
+ std::string resolvedWithCMD;
+
+ std::wstring actualCWD;
+ fs::path filePath = ush::string_cast<std::wstring>(lpFileName, CodePage::UTF8);
+ if (k32CurrentDirectoryTracker.get(actualCWD, filePath.wstring().c_str())) {
+ if (!filePath.is_absolute())
+ resolvedWithCMD = ush::string_cast<std::string>(
+ (actualCWD / filePath.relative_path()).wstring());
+ }
+
+ PRE_REALCALL
+ res =
+ ::GetFullPathNameA(resolvedWithCMD.empty() ? lpFileName : resolvedWithCMD.c_str(),
+ nBufferLength, lpBuffer, lpFilePart);
+ POST_REALCALL
+
+ if (false && nBufferLength)
+ LOG_CALL()
+ .PARAM(lpFileName)
+ .PARAM(resolvedWithCMD)
+ .PARAM(std::string(lpBuffer, res))
+ .PARAM(nBufferLength)
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength,
+ LPWSTR lpBuffer, LPWSTR* lpFilePart)
+{
+ DWORD res = 0UL;
+
+ HOOK_START_GROUP(MutExHookGroup::FULL_PATHNAME)
+ if (!callContext.active()) {
+ res = GetFullPathNameW(lpFileName, nBufferLength, lpBuffer, lpFilePart);
+ callContext.updateLastError();
+ return res;
+ }
+
+ std::wstring resolvedWithCMD;
+
+ std::wstring actualCWD;
+ if (k32CurrentDirectoryTracker.get(actualCWD, lpFileName)) {
+ fs::path filePath = lpFileName;
+ if (!filePath.is_absolute())
+ resolvedWithCMD = (actualCWD / filePath.relative_path()).wstring();
+ }
+
+ PRE_REALCALL
+ res =
+ ::GetFullPathNameW(resolvedWithCMD.empty() ? lpFileName : resolvedWithCMD.c_str(),
+ nBufferLength, lpBuffer, lpFilePart);
+ POST_REALCALL
+
+ if (false && nBufferLength)
+ LOG_CALL()
+ .PARAM(lpFileName)
+ .PARAM(resolvedWithCMD)
+ .PARAM(std::wstring(lpBuffer, res))
+ .PARAM(nBufferLength)
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetModuleFileNameA(HMODULE hModule, LPSTR lpFilename,
+ DWORD nSize)
+{
+ DWORD res = 0UL;
+
+ HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS)
+
+ PRE_REALCALL
+ res = ::GetModuleFileNameA(hModule, lpFilename, nSize);
+ POST_REALCALL
+ if ((res != 0) && callContext.active()) {
+ std::vector<char> buf;
+ // If GetModuleFileNameA failed because the buffer is not large enough this
+ // complicates matters because we are dealing with incomplete information
+ // (consider for example the case that we have a long real path which will
+ // be routed to a short virtual so the call should actually succeed in such
+ // a case). To solve this we simply use our own buffer to find the complete
+ // module path:
+ DWORD full_res = res;
+ size_t buf_size = nSize;
+ while (full_res == buf_size) {
+ buf_size = std::max(static_cast<size_t>(MAX_PATH), buf_size * 2);
+ buf.resize(buf_size);
+ full_res = ::GetModuleFileNameA(hModule, buf.data(), buf_size);
+ }
+
+ RerouteW reroute = RerouteW::create(
+ READ_CONTEXT(), callContext,
+ ush::string_cast<std::wstring>(buf.empty() ? lpFilename : buf.data()).c_str(),
+ true);
+ if (reroute.wasRerouted()) {
+ DWORD reroutedSize = static_cast<DWORD>(wcslen(reroute.fileName()));
+ if (reroutedSize >= nSize) {
+ reroutedSize = nSize - 1;
+ callContext.updateLastError(ERROR_INSUFFICIENT_BUFFER);
+ res = nSize;
+ } else
+ res = reroutedSize;
+ memcpy(lpFilename, ush::string_cast<std::string>(reroute.fileName()).c_str(),
+ reroutedSize * sizeof(lpFilename[0]));
+ lpFilename[reroutedSize] = 0;
+
+ LOG_CALL()
+ .PARAM(hModule)
+ .addParam("lpFilename", (res != 0UL) ? lpFilename : "<not set>")
+ .PARAM(nSize)
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+ }
+ }
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename,
+ DWORD nSize)
+{
+ DWORD res = 0UL;
+
+ HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS)
+
+ PRE_REALCALL
+ res = ::GetModuleFileNameW(hModule, lpFilename, nSize);
+ POST_REALCALL
+ if ((res != 0) && callContext.active()) {
+ std::vector<WCHAR> buf;
+ // If GetModuleFileNameW failed because the buffer is not large enough this
+ // complicates matters because we are dealing with incomplete information (consider
+ // for example the case that we have a long real path which will be routed to a
+ // short virtual so the call should actually succeed in such a case). To solve this
+ // we simply use our own buffer to find the complete module path:
+ DWORD full_res = res;
+ size_t buf_size = nSize;
+ while (full_res == buf_size) {
+ buf_size = std::max(static_cast<size_t>(MAX_PATH), buf_size * 2);
+ buf.resize(buf_size);
+ full_res = ::GetModuleFileNameW(hModule, buf.data(), buf_size);
+ }
+
+ RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext,
+ buf.empty() ? lpFilename : buf.data(), true);
+ if (reroute.wasRerouted()) {
+ DWORD reroutedSize = static_cast<DWORD>(wcslen(reroute.fileName()));
+ if (reroutedSize >= nSize) {
+ reroutedSize = nSize - 1;
+ callContext.updateLastError(ERROR_INSUFFICIENT_BUFFER);
+ res = nSize;
+ } else
+ res = reroutedSize;
+ memcpy(lpFilename, reroute.fileName(), reroutedSize * sizeof(lpFilename[0]));
+ lpFilename[reroutedSize] = 0;
+
+ LOG_CALL()
+ .PARAM(hModule)
+ .addParam("lpFilename", (res != 0UL) ? lpFilename : L"<not set>")
+ .PARAM(nSize)
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+ }
+ }
+ HOOK_END
+
+ return res;
+}
+
+HANDLE WINAPI usvfs::hook_FindFirstFileExW(
+ LPCWSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData,
+ FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags)
+{
+ HANDLE res = INVALID_HANDLE_VALUE;
+
+ HOOK_START_GROUP(MutExHookGroup::SEARCH_FILES)
+ if (!callContext.active()) {
+ res = FindFirstFileExW(lpFileName, fInfoLevelId, lpFindFileData, fSearchOp,
+ lpSearchFilter, dwAdditionalFlags);
+ callContext.updateLastError();
+ return res;
+ }
+
+ // FindFirstFileEx() must fail early if the path ends with a slash
+ if (lpFileName) {
+ const auto len = wcslen(lpFileName);
+ if (len > 0) {
+ if (lpFileName[len - 1] == L'\\' || lpFileName[len - 1] == L'/') {
+ spdlog::get("usvfs")->warn(
+ "hook_FindFirstFileExW(): path '{}' ends with slash, always fails",
+ fs::path(lpFileName).string());
+ return INVALID_HANDLE_VALUE;
+ }
+ }
+ }
+
+ fs::path finalPath;
+ RerouteW reroute;
+ fs::path originalPath;
+
+ bool usedRewrite = false;
+
+ // We need to do some trickery here, since we only want to use the hooked
+ // NtQueryDirectoryFile for rerouted locations we need to check if the Directory path
+ // has been routed instead of the full path.
+ originalPath = RerouteW::canonizePath(RerouteW::absolutePath(lpFileName));
+ PRE_REALCALL
+ res = ::FindFirstFileExW(originalPath.c_str(), fInfoLevelId, lpFindFileData,
+ fSearchOp, lpSearchFilter, dwAdditionalFlags);
+ POST_REALCALL
+
+ if (res == INVALID_HANDLE_VALUE) {
+ fs::path searchPath = originalPath.filename();
+ fs::path parentPath = originalPath.parent_path();
+ std::wstring findPath = parentPath.wstring();
+ while (findPath.find(L"*?<>\"", 0, 1) != std::wstring::npos) {
+ searchPath = parentPath.filename() / searchPath;
+ parentPath = parentPath.parent_path();
+ findPath = parentPath.wstring();
+ }
+ reroute = RerouteW::create(READ_CONTEXT(), callContext, parentPath.c_str());
+ if (reroute.wasRerouted()) {
+ finalPath = reroute.fileName();
+ finalPath /= searchPath.wstring();
+ }
+ if (!finalPath.empty()) {
+ PRE_REALCALL
+ usedRewrite = true;
+ res = ::FindFirstFileExW(finalPath.c_str(), fInfoLevelId, lpFindFileData,
+ fSearchOp, lpSearchFilter, dwAdditionalFlags);
+ POST_REALCALL
+ }
+ }
+
+ if (res != INVALID_HANDLE_VALUE) {
+ // store the original search path for use during iteration
+ WRITE_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[res] = lpFileName;
+ }
+
+ LOG_CALL()
+ .PARAM(lpFileName)
+ .PARAM(originalPath.c_str())
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+ if (usedRewrite)
+ LOG_CALL()
+ .PARAM(lpFileName)
+ .PARAM(finalPath.c_str())
+ .PARAM(res)
+ .PARAM(callContext.lastError());
+
+ HOOK_END
+
+ return res;
+}
+
+HRESULT(WINAPI* usvfs::CopyFile2)(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName,
+ COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters);
+
+HRESULT WINAPI usvfs::hook_CopyFile2(PCWSTR pwszExistingFileName,
+ PCWSTR pwszNewFileName,
+ COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters)
+{
+ HRESULT res = E_FAIL;
+
+ typedef HRESULT(WINAPI * CopyFile2_t)(PCWSTR, PCWSTR, COPYFILE2_EXTENDED_PARAMETERS*);
+
+ HOOK_START_GROUP(MutExHookGroup::SHELL_FILEOP)
+ if (!callContext.active()) {
+ res = CopyFile2(pwszExistingFileName, pwszNewFileName, pExtendedParameters);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW readReroute;
+ CreateRerouter writeReroute;
+ bool callOriginal = true;
+
+ {
+ auto context = READ_CONTEXT();
+ readReroute = RerouteW::create(context, callContext, pwszExistingFileName);
+ callOriginal = writeReroute.rerouteNew(
+ context, callContext, pwszNewFileName,
+ pExtendedParameters &&
+ (pExtendedParameters->dwCopyFlags & COPY_FILE_FAIL_IF_EXISTS) == 0,
+ "hook_CopyFile2");
+ }
+
+ if (callOriginal) {
+ PRE_REALCALL
+ res =
+ CopyFile2(readReroute.fileName(), writeReroute.fileName(), pExtendedParameters);
+ POST_REALCALL
+ writeReroute.updateResult(callContext, SUCCEEDED(res));
+
+ if (SUCCEEDED(res) && writeReroute.newReroute())
+ writeReroute.insertMapping(WRITE_CONTEXT());
+
+ if (readReroute.wasRerouted() || writeReroute.wasRerouted() ||
+ writeReroute.changedError())
+ LOG_CALL()
+ .PARAM(readReroute.fileName())
+ .PARAM(writeReroute.fileName())
+ .PARAM(res)
+ .PARAM(writeReroute.originalError())
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetPrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName,
+ LPCSTR lpDefault,
+ LPSTR lpReturnedString, DWORD nSize,
+ LPCSTR lpFileName)
+{
+ DWORD res = 0;
+
+ HOOK_START_GROUP(MutExHookGroup::OPEN_FILE)
+
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = ::GetPrivateProfileStringA(lpAppName, lpKeyName, lpDefault, lpReturnedString,
+ nSize, lpFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW reroute = RerouteW::create(
+ READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str());
+
+ PRE_REALCALL
+ res = ::GetPrivateProfileStringA(
+ lpAppName, lpKeyName, lpDefault, lpReturnedString, nSize,
+ ush::string_cast<std::string>(reroute.fileName()).c_str());
+ POST_REALCALL
+
+ if (reroute.wasRerouted()) {
+ LOG_CALL()
+ .PARAM(lpAppName)
+ .PARAM(lpKeyName)
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAMHEX(res)
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetPrivateProfileStringW(LPCWSTR lpAppName, LPCWSTR lpKeyName,
+ LPCWSTR lpDefault,
+ LPWSTR lpReturnedString, DWORD nSize,
+ LPCWSTR lpFileName)
+{
+ DWORD res = 0;
+
+ HOOK_START_GROUP(MutExHookGroup::OPEN_FILE)
+
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = ::GetPrivateProfileStringW(lpAppName, lpKeyName, lpDefault, lpReturnedString,
+ nSize, lpFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName);
+
+ PRE_REALCALL
+ res = ::GetPrivateProfileStringW(lpAppName, lpKeyName, lpDefault, lpReturnedString,
+ nSize, reroute.fileName());
+ POST_REALCALL
+
+ if (reroute.wasRerouted()) {
+ LOG_CALL()
+ .PARAM(lpAppName)
+ .PARAM(lpKeyName)
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAMHEX(res)
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetPrivateProfileSectionA(LPCSTR lpAppName,
+ LPSTR lpReturnedString, DWORD nSize,
+ LPCSTR lpFileName)
+{
+ DWORD res = 0;
+
+ HOOK_START_GROUP(MutExHookGroup::OPEN_FILE)
+
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = ::GetPrivateProfileSectionA(lpAppName, lpReturnedString, nSize, lpFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW reroute = RerouteW::create(
+ READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str());
+
+ PRE_REALCALL
+ res = ::GetPrivateProfileSectionA(
+ lpAppName, lpReturnedString, nSize,
+ ush::string_cast<std::string>(reroute.fileName()).c_str());
+ POST_REALCALL
+
+ if (reroute.wasRerouted()) {
+ LOG_CALL()
+ .PARAM(lpAppName)
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAMHEX(res)
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+DWORD WINAPI usvfs::hook_GetPrivateProfileSectionW(LPCWSTR lpAppName,
+ LPWSTR lpReturnedString, DWORD nSize,
+ LPCWSTR lpFileName)
+{
+ DWORD res = 0;
+
+ HOOK_START_GROUP(MutExHookGroup::OPEN_FILE)
+
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = ::GetPrivateProfileSectionW(lpAppName, lpReturnedString, nSize, lpFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, lpFileName);
+
+ PRE_REALCALL
+ res = ::GetPrivateProfileSectionW(lpAppName, lpReturnedString, nSize,
+ reroute.fileName());
+ POST_REALCALL
+
+ if (reroute.wasRerouted()) {
+ LOG_CALL()
+ .PARAM(lpAppName)
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAMHEX(res)
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_WritePrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName,
+ LPCSTR lpString, LPCSTR lpFileName)
+{
+ BOOL res = false;
+
+ HOOK_START_GROUP(MutExHookGroup::OPEN_FILE)
+
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = ::WritePrivateProfileStringA(lpAppName, lpKeyName, lpString, lpFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ CreateRerouter reroute;
+ bool callOriginal = reroute.rerouteNew(
+ READ_CONTEXT(), callContext, ush::string_cast<std::wstring>(lpFileName).c_str(),
+ true, "hook_WritePrivateProfileStringA");
+
+ if (callOriginal) {
+ PRE_REALCALL
+ res = ::WritePrivateProfileStringA(
+ lpAppName, lpKeyName, lpString,
+ ush::string_cast<std::string>(reroute.fileName()).c_str());
+ POST_REALCALL
+ reroute.updateResult(callContext, res);
+
+ if (res && reroute.newReroute())
+ reroute.insertMapping(WRITE_CONTEXT());
+
+ if (reroute.wasRerouted() || reroute.changedError())
+ LOG_CALL()
+ .PARAM(lpAppName)
+ .PARAM(lpKeyName)
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAMHEX(res)
+ .PARAM(reroute.originalError())
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+BOOL WINAPI usvfs::hook_WritePrivateProfileStringW(LPCWSTR lpAppName, LPCWSTR lpKeyName,
+ LPCWSTR lpString, LPCWSTR lpFileName)
+{
+ BOOL res = false;
+
+ HOOK_START_GROUP(MutExHookGroup::OPEN_FILE)
+
+ if (!callContext.active() || !RerouteW::interestingPath(lpFileName)) {
+ res = ::WritePrivateProfileStringW(lpAppName, lpKeyName, lpString, lpFileName);
+ callContext.updateLastError();
+ return res;
+ }
+
+ CreateRerouter reroute;
+ bool callOriginal = reroute.rerouteNew(READ_CONTEXT(), callContext, lpFileName, true,
+ "hook_WritePrivateProfileStringW");
+
+ if (callOriginal) {
+ PRE_REALCALL
+ res = ::WritePrivateProfileStringW(lpAppName, lpKeyName, lpString,
+ reroute.fileName());
+ POST_REALCALL
+ reroute.updateResult(callContext, res);
+
+ if (res && reroute.newReroute())
+ reroute.insertMapping(WRITE_CONTEXT());
+
+ if (reroute.wasRerouted() || reroute.changedError())
+ LOG_CALL()
+ .PARAM(lpAppName)
+ .PARAM(lpKeyName)
+ .PARAM(lpFileName)
+ .PARAM(reroute.fileName())
+ .PARAMHEX(res)
+ .PARAM(reroute.originalError())
+ .PARAM(callContext.lastError());
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+VOID WINAPI usvfs::hook_ExitProcess(UINT exitCode)
+{
+ HOOK_START
+
+ {
+ HookContext::Ptr context = WRITE_CONTEXT();
+
+ std::vector<std::future<int>>& delayed = context->delayed();
+
+ if (!delayed.empty()) {
+ // ensure all delayed tasks are completed before we exit the process
+ for (std::future<int>& delayedOp : delayed) {
+ delayedOp.get();
+ }
+ delayed.clear();
+ }
+ }
+
+ // exitprocess doesn't return so logging the call after the real call doesn't
+ // make much sense.
+ // nor does any pre/post call macro
+ LOG_CALL().PARAM(exitCode);
+
+ usvfsDisconnectVFS();
+
+ // HookManager::instance().removeHook("ExitProcess");
+ // PRE_REALCALL
+ ::ExitProcess(exitCode);
+ // POST_REALCALL
+
+ HOOK_END
+}
diff --git a/libs/usvfs/src/usvfs_dll/hooks/kernel32.h b/libs/usvfs/src/usvfs_dll/hooks/kernel32.h
new file mode 100644
index 0000000..7087b2e
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hooks/kernel32.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include "ntdll_declarations.h"
+#include <dllimport.h>
+
+namespace usvfs
+{
+
+DLLEXPORT BOOL WINAPI hook_GetFileAttributesExA(LPCSTR lpFileName,
+ GET_FILEEX_INFO_LEVELS fInfoLevelId,
+ LPVOID lpFileInformation);
+DLLEXPORT BOOL WINAPI hook_GetFileAttributesExW(LPCWSTR lpFileName,
+ GET_FILEEX_INFO_LEVELS fInfoLevelId,
+ LPVOID lpFileInformation);
+DLLEXPORT DWORD WINAPI hook_GetFileAttributesA(LPCSTR lpFileName);
+DLLEXPORT DWORD WINAPI hook_GetFileAttributesW(LPCWSTR lpFileName);
+DLLEXPORT DWORD WINAPI hook_SetFileAttributesW(LPCWSTR lpFileName,
+ DWORD dwFileAttributes);
+
+DLLEXPORT DWORD WINAPI hook_GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer);
+DLLEXPORT DWORD WINAPI hook_GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer);
+DLLEXPORT BOOL WINAPI hook_SetCurrentDirectoryA(LPCSTR lpPathName);
+DLLEXPORT BOOL WINAPI hook_SetCurrentDirectoryW(LPCWSTR lpPathName);
+
+DLLEXPORT DWORD WINAPI hook_GetFullPathNameA(LPCSTR lpFileName, DWORD nBufferLength,
+ LPSTR lpBuffer, LPSTR* lpFilePart);
+DLLEXPORT DWORD WINAPI hook_GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength,
+ LPWSTR lpBuffer, LPWSTR* lpFilePart);
+
+DLLEXPORT BOOL WINAPI hook_CreateDirectoryW(LPCWSTR lpPathName,
+ LPSECURITY_ATTRIBUTES lpSecurityAttributes);
+DLLEXPORT BOOL WINAPI hook_RemoveDirectoryW(LPCWSTR lpPathName);
+
+DLLEXPORT BOOL WINAPI hook_DeleteFileW(LPCWSTR lpFileName);
+
+DLLEXPORT BOOL WINAPI hook_MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName);
+DLLEXPORT BOOL WINAPI hook_MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName);
+DLLEXPORT BOOL WINAPI hook_MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName,
+ DWORD dwFlags);
+DLLEXPORT BOOL WINAPI hook_MoveFileExW(LPCWSTR lpExistingFileName,
+ LPCWSTR lpNewFileName, DWORD dwFlags);
+DLLEXPORT BOOL WINAPI hook_MoveFileWithProgressA(LPCSTR lpExistingFileName,
+ LPCSTR lpNewFileName,
+ LPPROGRESS_ROUTINE lpProgressRoutine,
+ LPVOID lpData, DWORD dwFlags);
+DLLEXPORT BOOL WINAPI hook_MoveFileWithProgressW(LPCWSTR lpExistingFileName,
+ LPCWSTR lpNewFileName,
+ LPPROGRESS_ROUTINE lpProgressRoutine,
+ LPVOID lpData, DWORD dwFlags);
+;
+
+DLLEXPORT BOOL WINAPI hook_CopyFileExW(LPCWSTR lpExistingFileName,
+ LPCWSTR lpNewFileName,
+ LPPROGRESS_ROUTINE lpProgressRoutine,
+ LPVOID lpData, LPBOOL pbCancel,
+ DWORD dwCopyFlags);
+
+extern HRESULT(WINAPI* CopyFile2)(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName,
+ COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters);
+DLLEXPORT HRESULT WINAPI
+hook_CopyFile2(PCWSTR pwszExistingFileName, PCWSTR pwszNewFileName,
+ COPYFILE2_EXTENDED_PARAMETERS* pExtendedParameters);
+
+DLLEXPORT HMODULE WINAPI hook_LoadLibraryExA(LPCSTR lpFileName, HANDLE hFile,
+ DWORD dwFlags);
+DLLEXPORT HMODULE WINAPI hook_LoadLibraryExW(LPCWSTR lpFileName, HANDLE hFile,
+ DWORD dwFlags);
+
+extern BOOL(WINAPI* CreateProcessInternalW)(
+ LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine,
+ LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes,
+ BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment,
+ LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo,
+ LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken);
+DLLEXPORT BOOL WINAPI hook_CreateProcessInternalW(
+ LPVOID token, LPCWSTR lpApplicationName, LPWSTR lpCommandLine,
+ LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes,
+ BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment,
+ LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo,
+ LPPROCESS_INFORMATION lpProcessInformation, LPVOID newToken);
+
+DLLEXPORT DWORD WINAPI hook_GetModuleFileNameA(HMODULE hModule, LPSTR lpFilename,
+ DWORD nSize);
+DLLEXPORT DWORD WINAPI hook_GetModuleFileNameW(HMODULE hModule, LPWSTR lpFilename,
+ DWORD nSize);
+
+DLLEXPORT HANDLE WINAPI hook_FindFirstFileExW(
+ LPCWSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData,
+ FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags);
+
+DLLEXPORT DWORD WINAPI hook_GetPrivateProfileStringA(LPCSTR lpAppName, LPCSTR lpKeyName,
+ LPCSTR lpDefault,
+ LPSTR lpReturnedString,
+ DWORD nSize, LPCSTR lpFileName);
+DLLEXPORT DWORD WINAPI hook_GetPrivateProfileStringW(LPCWSTR lpAppName,
+ LPCWSTR lpKeyName,
+ LPCWSTR lpDefault,
+ LPWSTR lpReturnedString,
+ DWORD nSize, LPCWSTR lpFileName);
+DLLEXPORT DWORD WINAPI hook_GetPrivateProfileSectionA(LPCSTR lpAppName,
+ LPSTR lpReturnedString,
+ DWORD nSize, LPCSTR lpFileName);
+DLLEXPORT DWORD WINAPI hook_GetPrivateProfileSectionW(LPCWSTR lpAppName,
+ LPWSTR lpReturnedString,
+ DWORD nSize, LPCWSTR lpFileName);
+DLLEXPORT BOOL WINAPI hook_WritePrivateProfileStringA(LPCSTR lpAppName,
+ LPCSTR lpKeyName, LPCSTR lpString,
+ LPCSTR lpFileName);
+DLLEXPORT BOOL WINAPI hook_WritePrivateProfileStringW(LPCWSTR lpAppName,
+ LPCWSTR lpKeyName,
+ LPCWSTR lpString,
+ LPCWSTR lpFileName);
+
+DLLEXPORT VOID WINAPI hook_ExitProcess(UINT exitCode);
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp b/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp
new file mode 100644
index 0000000..ad53418
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hooks/ntdll.cpp
@@ -0,0 +1,1520 @@
+#include "ntdll.h"
+
+#include <mutex>
+#include <queue>
+#include <set>
+
+#include <boost/filesystem.hpp>
+
+#include <addrtools.h>
+#include <loghelpers.h>
+#include <stringcast.h>
+#include <stringutils.h>
+#include <unicodestring.h>
+#include <usvfs.h>
+
+#include "../hookcallcontext.h"
+#include "../hookcontext.h"
+#include "../maptracker.h"
+#include "../stringcast_boost.h"
+
+#include "file_information_utils.h"
+#include "sharedids.h"
+
+namespace ulog = usvfs::log;
+namespace ush = usvfs::shared;
+namespace bfs = boost::filesystem;
+
+using usvfs::UnicodeString;
+
+// flag definitions below are copied from winternl.h
+#define FILE_SUPERSEDE 0x00000000
+#define FILE_OPEN 0x00000001
+#define FILE_CREATE 0x00000002
+#define FILE_OPEN_IF 0x00000003
+#define FILE_OVERWRITE 0x00000004
+#define FILE_OVERWRITE_IF 0x00000005
+#define FILE_MAXIMUM_DISPOSITION 0x00000005
+
+template <typename T>
+using unique_ptr_deleter = std::unique_ptr<T, void (*)(T*)>;
+
+class RedirectionInfo
+{
+public:
+ UnicodeString path;
+ bool redirected;
+
+ RedirectionInfo() {}
+ RedirectionInfo(UnicodeString path, bool redirected)
+ : path(path), redirected(redirected)
+ {}
+};
+
+class HandleTracker
+{
+public:
+ using handle_type = HANDLE;
+ using info_type = UnicodeString;
+
+ HandleTracker() { insert_current_directory(); }
+
+ info_type lookup(handle_type handle) const
+ {
+ if (valid_handle(handle)) {
+ std::shared_lock<std::shared_mutex> lock(m_mutex);
+ auto find = m_map.find(handle);
+ if (find != m_map.end())
+ return find->second;
+ }
+ return info_type();
+ }
+
+ void insert(handle_type handle, const info_type& info)
+ {
+ if (!valid_handle(handle))
+ return;
+ std::unique_lock<std::shared_mutex> lock(m_mutex);
+ m_map[handle] = info;
+ }
+
+ void erase(handle_type handle)
+ {
+ if (!valid_handle(handle))
+ return;
+ std::unique_lock<std::shared_mutex> lock(m_mutex);
+ m_map.erase(handle);
+ }
+
+private:
+ static bool valid_handle(handle_type handle)
+ {
+ return handle && handle != INVALID_HANDLE_VALUE;
+ }
+
+ void insert_current_directory()
+ {
+ ntdll_declarations_init();
+
+ UNICODE_STRING p{0};
+ RTL_RELATIVE_NAME r{0};
+ NTSTATUS status =
+ RtlDosPathNameToRelativeNtPathName_U_WithStatus(L"x", &p, nullptr, &r);
+ if (status >= 0) {
+ if (r.ContainingDirectory && p.Buffer) {
+ size_t len = p.Length / sizeof(WCHAR);
+ size_t trim = strlen("\\x") + (p.Buffer[len - 1] ? 0 : 1);
+ if (len > trim)
+ insert(r.ContainingDirectory, info_type(p.Buffer, len - trim));
+ }
+ RtlReleaseRelativeName(&r);
+ if (p.Buffer)
+ HeapFree(GetProcessHeap(), 0, p.Buffer);
+ }
+ }
+
+ mutable std::shared_mutex m_mutex;
+ std::unordered_map<handle_type, info_type> m_map;
+};
+
+HandleTracker ntdllHandleTracker;
+
+UnicodeString CreateUnicodeString(const OBJECT_ATTRIBUTES* objectAttributes)
+{
+ UnicodeString result = ntdllHandleTracker.lookup(objectAttributes->RootDirectory);
+ if (objectAttributes->ObjectName != nullptr) {
+ result.appendPath(objectAttributes->ObjectName);
+ }
+ return result;
+}
+
+std::ostream& operator<<(std::ostream& os, const _UNICODE_STRING& str)
+{
+ try {
+ // TODO this does not correctly support surrogate pairs
+ // since the size used here is the number of 16-bit characters in the buffer
+ // whereas
+ // toNarrow expects the actual number of characters.
+ // It will always underestimate though, so worst case scenario we truncate
+ // the string
+ os << ush::string_cast<std::string>(str.Buffer, ush::CodePage::UTF8,
+ str.Length / sizeof(WCHAR));
+ } catch (const std::exception& e) {
+ os << e.what();
+ }
+
+ return os;
+}
+
+std::ostream& operator<<(std::ostream& os, POBJECT_ATTRIBUTES attr)
+{
+ return operator<<(os, *attr->ObjectName);
+}
+
+RedirectionInfo applyReroute(const usvfs::HookContext::ConstPtr& context,
+ const usvfs::HookCallContext& callContext,
+ const UnicodeString& inPath)
+{
+ RedirectionInfo result;
+ result.path = inPath;
+ result.redirected = false;
+
+ if (callContext.active()) {
+ // see if the file exists in the redirection tree
+ std::string lookupPath = ush::string_cast<std::string>(
+ static_cast<LPCWSTR>(result.path) + 4, ush::CodePage::UTF8);
+ auto node = context->redirectionTable()->findNode(lookupPath.c_str());
+ // if so, replace the file name with the path to the mapped file
+ if ((node.get() != nullptr) &&
+ (!node->data().linkTarget.empty() || node->isDirectory())) {
+ std::wstring reroutePath;
+
+ if (node->data().linkTarget.length() > 0) {
+ reroutePath = ush::string_cast<std::wstring>(node->data().linkTarget.c_str(),
+ ush::CodePage::UTF8);
+ } else {
+ reroutePath =
+ ush::string_cast<std::wstring>(node->path().c_str(), ush::CodePage::UTF8);
+ }
+ if ((*reroutePath.rbegin() == L'\\') && (*lookupPath.rbegin() != '\\')) {
+ reroutePath.resize(reroutePath.size() - 1);
+ }
+ std::replace(reroutePath.begin(), reroutePath.end(), L'/', L'\\');
+ if (reroutePath[1] == L'\\')
+ reroutePath[1] = L'?';
+ result.path = LR"(\??\)" + reroutePath;
+ result.redirected = true;
+ }
+ }
+ return result;
+}
+
+RedirectionInfo applyReroute(const usvfs::CreateRerouter& rerouter)
+{
+ RedirectionInfo result;
+ result.path = rerouter.fileName();
+ result.redirected = rerouter.wasRerouted();
+
+ std::wstring reroutePath(static_cast<LPCWSTR>(result.path));
+ std::replace(reroutePath.begin(), reroutePath.end(), L'/', L'\\');
+ if (reroutePath[1] == L'\\')
+ reroutePath[1] = L'?';
+ if (!((reroutePath[0] == L'\\') && (reroutePath[1] == L'?') &&
+ (reroutePath[2] == L'?') && (reroutePath[3] == L'\\'))) {
+ result.path = LR"(\??\)" + reroutePath;
+ }
+
+ return result;
+}
+
+struct FindCreateTarget
+{
+ usvfs::RedirectionTree::NodePtrT target;
+ void operator()(usvfs::RedirectionTree::NodePtrT node)
+ {
+ if (node->hasFlag(usvfs::shared::FLAG_CREATETARGET)) {
+ target = node;
+ }
+ }
+};
+
+std::pair<UnicodeString, UnicodeString>
+findCreateTarget(const usvfs::HookContext::ConstPtr& context,
+ const UnicodeString& inPath)
+{
+ std::pair<UnicodeString, UnicodeString> result;
+ result.first = inPath;
+ result.second = UnicodeString();
+
+ std::string lookupPath = ush::string_cast<std::string>(
+ static_cast<LPCWSTR>(result.first) + 4, ush::CodePage::UTF8);
+ FindCreateTarget visitor;
+ usvfs::RedirectionTree::VisitorFunction visitorWrapper =
+ [&](const usvfs::RedirectionTree::NodePtrT& node) {
+ visitor(node);
+ };
+ context->redirectionTable()->visitPath(lookupPath, visitorWrapper);
+ if (visitor.target.get() != nullptr) {
+ bfs::path relativePath =
+ ush::make_relative(visitor.target->path(), bfs::path(lookupPath));
+
+ bfs::path target(visitor.target->data().linkTarget.c_str());
+ target /= relativePath;
+
+ result.second = UnicodeString(target.wstring().c_str());
+ winapi::ex::wide::createPath(target.parent_path());
+ }
+ return result;
+}
+
+RedirectionInfo applyReroute(const usvfs::HookContext::ConstPtr& context,
+ const usvfs::HookCallContext& callContext,
+ POBJECT_ATTRIBUTES inAttributes)
+{
+ return applyReroute(context, callContext, CreateUnicodeString(inAttributes));
+}
+
+int NextDividableBy(int number, int divider)
+{
+ return static_cast<int>(
+ ceilf(static_cast<float>(number) / static_cast<float>(divider)) * divider);
+}
+
+// Something is trying to create a variety of files starting with "\Device\",
+// such as "\Device\DeviceApi\Dev\Query", "\Device\MMCSS\MmThread",
+// "\Device\DeviceApi\CMNotify", etc.
+//
+// This used to create a bunch of spurious "Device" and "MMCSS" folders when
+// using Explorer++. Some of these names seem to part of PnP, others from the
+// "Multimedia Class Scheduler Service".
+//
+// There's basically zero information on this stuff online.
+//
+// Regardless, these files ended up being rerouted, so they became something
+// like C:\game\Data\Somewhere\Device\MMCSS\MmThread, which ended up being
+// rerouted to overwrite and created as a fake path
+// "overwrite\Somewhere\Device\MMCSS"
+//
+// These paths are weird, they feel like pipes or something. It's not clear how
+// they should be reliably recognized, so this is a hardcoded check for any
+// path that starts with "\Device\". These paths will be forwarded directly
+// to NtCreateFile/NtOpenFile
+//
+bool isDeviceFile(std::wstring_view name)
+{
+ static const std::wstring_view DevicePrefix(L"\\Device\\");
+
+ // starts with
+ if (name.size() < DevicePrefix.size()) {
+ return false;
+ } else {
+ return (_wcsnicmp(name.data(), DevicePrefix.data(), DevicePrefix.size()) == 0);
+ }
+}
+
+NTSTATUS addNtSearchData(HANDLE hdl, PUNICODE_STRING FileName,
+ const std::wstring& fakeName,
+ FILE_INFORMATION_CLASS FileInformationClass, PVOID& buffer,
+ ULONG& bufferSize, std::set<std::wstring>& foundFiles,
+ HANDLE event, PIO_APC_ROUTINE apcRoutine, PVOID apcContext,
+ BOOLEAN returnSingleEntry)
+{
+ NTSTATUS res = STATUS_NO_SUCH_FILE;
+ if (hdl != INVALID_HANDLE_VALUE) {
+ PVOID lastValidRecord = nullptr;
+ PVOID bufferInit = buffer;
+ IO_STATUS_BLOCK status;
+ res = NtQueryDirectoryFile(hdl, event, apcRoutine, apcContext, &status, buffer,
+ bufferSize, FileInformationClass, returnSingleEntry,
+ FileName, FALSE);
+
+ if ((res != STATUS_SUCCESS) || (status.Information <= 0)) {
+ bufferSize = 0UL;
+ } else {
+ ULONG totalOffset = 0;
+ PVOID lastSkipPos = nullptr;
+
+ while (totalOffset < status.Information) {
+ ULONG offset;
+ std::wstring fileName;
+ GetFileInformationData(FileInformationClass, buffer, offset, fileName);
+ // in case this is a single-file search result and the specified
+ // filename differs from the file name found, replace it in the
+ // information structure
+ if ((totalOffset == 0) && (offset == 0) && (fakeName.length() > 0)) {
+ // if the fake name is larger than what is in the buffer and there is
+ // not enough room, that's a buffer overflow
+ if ((fakeName.length() > fileName.length()) &&
+ ((fakeName.length() - fileName.length()) >
+ (bufferSize - status.Information))) {
+ res = STATUS_BUFFER_OVERFLOW;
+ break;
+ }
+ // WARNING for the case where the fake name is longer this needs to
+ // move back all further results and update the offset first
+ SetFileInformationFileName(FileInformationClass, buffer, fakeName);
+ fileName = fakeName;
+ }
+ bool add = true;
+ if (fileName.length() > 0) {
+ auto insertRes = foundFiles.insert(ush::to_upper(fileName));
+ add = insertRes.second; // add only if we didn't find this file before
+ }
+ if (!add) {
+ if (lastSkipPos == nullptr) {
+ lastSkipPos = buffer;
+ }
+ } else {
+ if (lastSkipPos != nullptr) {
+ memmove(lastSkipPos, buffer, status.Information - totalOffset);
+ ULONG delta = static_cast<ULONG>(ush::AddrDiff(buffer, lastSkipPos));
+ totalOffset -= delta;
+
+ buffer = lastSkipPos;
+ lastSkipPos = nullptr;
+ }
+ lastValidRecord = buffer;
+ }
+
+ if (offset == 0) {
+ offset = static_cast<ULONG>(status.Information) - totalOffset;
+ }
+ buffer = ush::AddrAdd(buffer, offset);
+ totalOffset += offset;
+ }
+
+ if (lastSkipPos != nullptr) {
+ buffer = lastSkipPos;
+ bufferSize = static_cast<ULONG>(ush::AddrDiff(buffer, bufferInit));
+ // null out the unused rest if there is some
+ memset(lastSkipPos, 0, status.Information - bufferSize);
+ } else {
+ bufferSize = static_cast<ULONG>(ush::AddrDiff(buffer, bufferInit));
+ }
+ }
+ if (lastValidRecord != nullptr) {
+ SetFileInformationOffset(FileInformationClass, lastValidRecord, 0);
+ }
+ }
+ return res;
+}
+
+DATA_ID(SearchInfo);
+
+struct Searches
+{
+ struct Info
+ {
+ struct VirtualMatch
+ {
+ // full path to where the file/directory actually is
+ std::wstring realPath;
+ // virtual filename (only filename since it has to be within the searched
+ // directory)
+ // this is left empty when a folder with all its content is mapped to the
+ // search directory
+ std::wstring virtualName;
+ };
+
+ Info() : currentSearchHandle(INVALID_HANDLE_VALUE) {}
+ std::set<std::wstring> foundFiles;
+ HANDLE currentSearchHandle;
+ std::queue<VirtualMatch> virtualMatches;
+ UnicodeString searchPattern;
+ bool regularComplete{false};
+ };
+
+ Searches() = default;
+
+ // must provide a special copy constructor because boost::mutex is
+ // non-copyable
+ Searches(const Searches& reference) : info(reference.info) {}
+
+ Searches& operator=(const Searches&) = delete;
+
+ std::recursive_mutex queryMutex;
+
+ std::map<HANDLE, Info> info;
+};
+
+void gatherVirtualEntries(const UnicodeString& dirName,
+ const usvfs::RedirectionTreeContainer& redir,
+ PUNICODE_STRING FileName, Searches::Info& info)
+{
+ LPCWSTR dirNameW = static_cast<LPCWSTR>(dirName);
+ // fix directory name. I'd love to know why microsoft sometimes uses "\??\" vs
+ // "\\?\"
+ if ((wcsncmp(dirNameW, LR"(\\?\)", 4) == 0) ||
+ (wcsncmp(dirNameW, LR"(\??\)", 4) == 0)) {
+ dirNameW += 4;
+ }
+ auto node = redir->findNode(boost::filesystem::path(dirNameW));
+ if (node.get() != nullptr) {
+ std::string searchPattern =
+ FileName != nullptr
+ ? ush::string_cast<std::string>(FileName->Buffer, ush::CodePage::UTF8)
+ : "*.*";
+
+ boost::replace_all(searchPattern, "\"", ".");
+
+ for (const auto& subNode : node->find(searchPattern)) {
+ if (((subNode->data().linkTarget.length() > 0) || subNode->isDirectory()) &&
+ !subNode->hasFlag(usvfs::shared::FLAG_DUMMY)) {
+ std::wstring vName =
+ ush::string_cast<std::wstring>(subNode->name(), ush::CodePage::UTF8);
+
+ Searches::Info::VirtualMatch m;
+ if (subNode->data().linkTarget.length() > 0) {
+ m = {ush::string_cast<std::wstring>(subNode->data().linkTarget.c_str(),
+ ush::CodePage::UTF8),
+ vName};
+ } else {
+ m = {ush::string_cast<std::wstring>(subNode->path().c_str(),
+ ush::CodePage::UTF8),
+ vName};
+ }
+
+ info.virtualMatches.push(m);
+ info.foundFiles.insert(ush::to_upper(vName));
+ }
+ }
+ }
+}
+
+/**
+ * @brief insert a virtual entry into the search result
+ * @param FileInformation
+ * @param FileInformationClass
+ * @param info
+ * @param realPath path were the actual file resides
+ * @param virtualName virtual file name (without path). will often be the same
+ * as the name component of realpath
+ * @param ReturnSingleEntry
+ * @param dataRead
+ * @return true if a virtual result was added, false if the search handle in the
+ * info object yields no more results
+ */
+bool addVirtualSearchResult(PVOID& FileInformation,
+ FILE_INFORMATION_CLASS FileInformationClass,
+ Searches::Info& info, const std::wstring& realPath,
+ const std::wstring& virtualName, BOOLEAN ReturnSingleEntry,
+ ULONG& dataRead)
+{
+ // this opens a search in the real location, then copies the information about
+ // files we care about (the ones being mapped) to the result we intend to
+ // return
+ bfs::path fullPath(realPath);
+ if (fullPath.filename().wstring() == L".") {
+ fullPath = fullPath.parent_path();
+ }
+ if (info.currentSearchHandle == INVALID_HANDLE_VALUE) {
+ std::wstring dirName = fullPath.parent_path().wstring();
+ if (dirName.length() >= MAX_PATH && !ush::startswith(dirName.c_str(), LR"(\\?\)"))
+ dirName = LR"(\\?\)" + dirName;
+ info.currentSearchHandle =
+ CreateFileW(dirName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
+ }
+ std::wstring fileName =
+ ush::string_cast<std::wstring>(fullPath.filename().string(), ush::CodePage::UTF8);
+ NTSTATUS subRes = addNtSearchData(
+ info.currentSearchHandle,
+ (fileName != L".") ? static_cast<PUNICODE_STRING>(UnicodeString(fileName.c_str()))
+ : nullptr,
+ virtualName, FileInformationClass, FileInformation, dataRead, info.foundFiles,
+ nullptr, nullptr, nullptr, ReturnSingleEntry);
+ if (subRes == STATUS_SUCCESS) {
+ return true;
+ } else {
+ // STATUS_NO_MORE_FILES merely means the search ended, everything else is an
+ // error message. Either way, the search here is finished and we should
+ // resume in the next mapped directory
+ if (subRes != STATUS_NO_MORE_FILES) {
+ spdlog::get("hooks")->warn("error reported listing files in {0}: {1:x}",
+ fullPath.string(), static_cast<uint32_t>(subRes));
+ }
+ return false;
+ }
+}
+
+NTSTATUS WINAPI usvfs::hook_NtQueryDirectoryFile(
+ HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext,
+ PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length,
+ FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry,
+ PUNICODE_STRING FileName, BOOLEAN RestartScan)
+{
+ PreserveGetLastError ntFunctionsDoNotChangeGetLastError;
+
+ // this is quite messy...
+ // first, this will gather the virtual locations mapping to the iterated one
+ // then we return results from the real location, skipping those that exist
+ // in the virtual locations, as those take precedence
+ // finally the virtual results are returned, adding each result to a skip
+ // list, so they don't get added twice
+ //
+ // if we don't add the regular files first, "." and ".." wouldn't be in the
+ // first search result of wildcard searches which may confuse the caller
+ NTSTATUS res = STATUS_NO_MORE_FILES;
+ HOOK_START_GROUP(MutExHookGroup::FIND_FILES)
+ if (!callContext.active()) {
+ return ::NtQueryDirectoryFile(
+ FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, FileInformation,
+ Length, FileInformationClass, ReturnSingleEntry, FileName, RestartScan);
+ }
+
+ // std::unique_lock<std::recursive_mutex> queryLock;
+ std::map<HANDLE, Searches::Info>::iterator infoIter;
+ bool firstSearch = false;
+
+ { // scope to limit context lifetime
+ HookContext::ConstPtr context = READ_CONTEXT();
+ Searches& activeSearches = context->customData<Searches>(SearchInfo);
+ // queryLock = std::unique_lock<std::recursive_mutex>(activeSearches.queryMutex);
+
+ if (RestartScan) {
+ auto iter = activeSearches.info.find(FileHandle);
+ if (iter != activeSearches.info.end()) {
+ activeSearches.info.erase(iter);
+ }
+ }
+
+ // see if we already have a running search
+ infoIter = activeSearches.info.find(FileHandle);
+ firstSearch = (infoIter == activeSearches.info.end());
+ }
+
+ if (firstSearch) {
+ HookContext::Ptr context = WRITE_CONTEXT();
+ Searches& activeSearches = context->customData<Searches>(SearchInfo);
+ // tradeoff time: we store this search status even if no virtual results
+ // were found. This causes a little extra cost here and in NtClose every
+ // time a non-virtual dir is being searched. However if we don't,
+ // whenever NtQueryDirectoryFile is called another time on the same handle,
+ // this (expensive) block would be run again.
+ infoIter =
+ activeSearches.info.insert(std::make_pair(FileHandle, Searches::Info())).first;
+ infoIter->second.searchPattern.appendPath(FileName);
+
+ SearchHandleMap& searchMap = context->customData<SearchHandleMap>(SearchHandles);
+ SearchHandleMap::iterator iter = searchMap.find(FileHandle);
+
+ UnicodeString searchPath;
+ if (iter != searchMap.end()) {
+ searchPath = UnicodeString(iter->second.c_str());
+ infoIter->second.currentSearchHandle = CreateFileW(
+ iter->second.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
+ } else {
+ searchPath = ntdllHandleTracker.lookup(FileHandle);
+ }
+ gatherVirtualEntries(searchPath, context->redirectionTable(), FileName,
+ infoIter->second);
+ }
+
+ ULONG dataRead = Length;
+ PVOID FileInformationCurrent = FileInformation;
+
+ // add regular search results, skipping those files we have in a virtual
+ // location
+ bool moreRegular = !infoIter->second.regularComplete;
+ bool dataReturned = false;
+ while (moreRegular && !dataReturned) {
+ dataRead = Length;
+
+ HANDLE handle = infoIter->second.currentSearchHandle;
+ if (handle == INVALID_HANDLE_VALUE) {
+ handle = FileHandle;
+ }
+ NTSTATUS subRes = addNtSearchData(
+ handle, FileName, L"", FileInformationClass, FileInformationCurrent, dataRead,
+ infoIter->second.foundFiles, Event, ApcRoutine, ApcContext, ReturnSingleEntry);
+ moreRegular = subRes == STATUS_SUCCESS;
+ if (moreRegular) {
+ dataReturned = dataRead != 0;
+ } else {
+ infoIter->second.regularComplete = true;
+ infoIter->second.foundFiles.clear();
+ if (infoIter->second.currentSearchHandle != INVALID_HANDLE_VALUE) {
+ ::CloseHandle(infoIter->second.currentSearchHandle);
+ infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE;
+ }
+ }
+ }
+ if (!moreRegular) {
+ // add virtual results
+ while (!dataReturned && infoIter->second.virtualMatches.size() > 0) {
+ auto match = infoIter->second.virtualMatches.front();
+ if (match.realPath.size() != 0) {
+ dataRead = Length;
+ if (addVirtualSearchResult(FileInformationCurrent, FileInformationClass,
+ infoIter->second, match.realPath, match.virtualName,
+ ReturnSingleEntry, dataRead)) {
+ // a positive result here means the call returned data and there may
+ // be further objects to be retrieved by repeating the call
+ dataReturned = true;
+ } else {
+ // proceed to next search handle
+
+ // TODO: doesn't append search results from more than one redirection
+ // per call. This is bad for performance but otherwise we'd need to
+ // re-write the offsets between information objects
+ infoIter->second.virtualMatches.pop();
+ CloseHandle(infoIter->second.currentSearchHandle);
+ infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE;
+ }
+ }
+ }
+ }
+
+ if (!dataReturned) {
+ if (firstSearch) {
+ res = STATUS_NO_SUCH_FILE;
+ } else {
+ res = STATUS_NO_MORE_FILES;
+ }
+ } else {
+ res = STATUS_SUCCESS;
+ }
+ IoStatusBlock->Status = res;
+ IoStatusBlock->Information = dataRead;
+
+ size_t numVirtualFiles = infoIter->second.virtualMatches.size();
+ if ((numVirtualFiles > 0)) {
+ LOG_CALL()
+ .addParam("path", ntdllHandleTracker.lookup(FileHandle))
+ .PARAM(FileInformationClass)
+ .PARAM(FileName)
+ .PARAM(numVirtualFiles)
+ .PARAMWRAP(res);
+ }
+
+ HOOK_END
+ return res;
+}
+
+NTSTATUS WINAPI usvfs::hook_NtQueryDirectoryFileEx(
+ HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext,
+ PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length,
+ FILE_INFORMATION_CLASS FileInformationClass, ULONG QueryFlags,
+ PUNICODE_STRING FileName)
+{
+ PreserveGetLastError ntFunctionsDoNotChangeGetLastError;
+ NTSTATUS res = STATUS_NO_MORE_FILES;
+
+ // this is quite messy...
+ // first, this will gather the virtual locations mapping to the iterated one
+ // then we return results from the real location, skipping those that exist
+ // in the virtual locations, as those take precedence
+ // finally the virtual results are returned, adding each result to a skip
+ // list, so they don't get added twice
+ //
+ // if we don't add the regular files first, "." and ".." wouldn't be in the
+ // first search result of wildcard searches which may confuse the caller
+ HOOK_START_GROUP(MutExHookGroup::FIND_FILES)
+ if (!callContext.active()) {
+ return ::NtQueryDirectoryFileEx(FileHandle, Event, ApcRoutine, ApcContext,
+ IoStatusBlock, FileInformation, Length,
+ FileInformationClass, QueryFlags, FileName);
+ }
+
+ // std::unique_lock<std::recursive_mutex> queryLock;
+ std::map<HANDLE, Searches::Info>::iterator infoIter;
+ bool firstSearch = false;
+
+ { // scope to limit context lifetime
+ HookContext::ConstPtr context = READ_CONTEXT();
+ Searches& activeSearches = context->customData<Searches>(SearchInfo);
+ // queryLock = std::unique_lock<std::recursive_mutex>(activeSearches.queryMutex);
+
+ if (QueryFlags & SL_RESTART_SCAN) {
+ auto iter = activeSearches.info.find(FileHandle);
+ if (iter != activeSearches.info.end()) {
+ activeSearches.info.erase(iter);
+ }
+ }
+
+ // see if we already have a running search
+ infoIter = activeSearches.info.find(FileHandle);
+ firstSearch = (infoIter == activeSearches.info.end());
+ }
+
+ if (firstSearch) {
+ HookContext::Ptr context = WRITE_CONTEXT();
+ Searches& activeSearches = context->customData<Searches>(SearchInfo);
+ // tradeoff time: we store this search status even if no virtual results
+ // were found. This causes a little extra cost here and in NtClose every
+ // time a non-virtual dir is being searched. However if we don't,
+ // whenever NtQueryDirectoryFile is called another time on the same handle,
+ // this (expensive) block would be run again.
+ infoIter =
+ activeSearches.info.insert(std::make_pair(FileHandle, Searches::Info())).first;
+ infoIter->second.searchPattern.appendPath(FileName);
+
+ SearchHandleMap& searchMap = context->customData<SearchHandleMap>(SearchHandles);
+ SearchHandleMap::iterator iter = searchMap.find(FileHandle);
+
+ UnicodeString searchPath;
+ if (iter != searchMap.end()) {
+ searchPath = UnicodeString(iter->second.c_str());
+ infoIter->second.currentSearchHandle = CreateFileW(
+ iter->second.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
+ } else {
+ searchPath = ntdllHandleTracker.lookup(FileHandle);
+ }
+ gatherVirtualEntries(searchPath, context->redirectionTable(), FileName,
+ infoIter->second);
+ }
+
+ ULONG dataRead = Length;
+ PVOID FileInformationCurrent = FileInformation;
+
+ // add regular search results, skipping those files we have in a virtual
+ // location
+ bool moreRegular = !infoIter->second.regularComplete;
+ bool dataReturned = false;
+ while (moreRegular && !dataReturned) {
+ dataRead = Length;
+
+ HANDLE handle = infoIter->second.currentSearchHandle;
+ if (handle == INVALID_HANDLE_VALUE) {
+ handle = FileHandle;
+ }
+ NTSTATUS subRes = addNtSearchData(handle, FileName, L"", FileInformationClass,
+ FileInformationCurrent, dataRead,
+ infoIter->second.foundFiles, Event, ApcRoutine,
+ ApcContext, QueryFlags & SL_RETURN_SINGLE_ENTRY);
+ moreRegular = subRes == STATUS_SUCCESS;
+ if (moreRegular) {
+ dataReturned = dataRead != 0;
+ } else {
+ infoIter->second.regularComplete = true;
+ infoIter->second.foundFiles.clear();
+ if (infoIter->second.currentSearchHandle != INVALID_HANDLE_VALUE) {
+ ::CloseHandle(infoIter->second.currentSearchHandle);
+ infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE;
+ }
+ }
+ }
+ if (!moreRegular) {
+ // add virtual results
+ while (!dataReturned && infoIter->second.virtualMatches.size() > 0) {
+ auto match = infoIter->second.virtualMatches.front();
+ if (match.realPath.size() != 0) {
+ dataRead = Length;
+ if (addVirtualSearchResult(FileInformationCurrent, FileInformationClass,
+ infoIter->second, match.realPath, match.virtualName,
+ QueryFlags & SL_RETURN_SINGLE_ENTRY, dataRead)) {
+ // a positive result here means the call returned data and there may
+ // be further objects to be retrieved by repeating the call
+ dataReturned = true;
+ } else {
+ // proceed to next search handle
+
+ // TODO: doesn't append search results from more than one redirection
+ // per call. This is bad for performance but otherwise we'd need to
+ // re-write the offsets between information objects
+ infoIter->second.virtualMatches.pop();
+ CloseHandle(infoIter->second.currentSearchHandle);
+ infoIter->second.currentSearchHandle = INVALID_HANDLE_VALUE;
+ }
+ }
+ }
+ }
+
+ if (!dataReturned) {
+ if (firstSearch) {
+ res = STATUS_NO_SUCH_FILE;
+ } else {
+ res = STATUS_NO_MORE_FILES;
+ }
+ } else {
+ res = STATUS_SUCCESS;
+ }
+ IoStatusBlock->Status = res;
+ IoStatusBlock->Information = dataRead;
+
+ size_t numVirtualFiles = infoIter->second.virtualMatches.size();
+ if ((numVirtualFiles > 0)) {
+ LOG_CALL()
+ .addParam("path", ntdllHandleTracker.lookup(FileHandle))
+ .PARAM(FileInformationClass)
+ .PARAM(FileName)
+ .PARAM(QueryFlags)
+ .PARAM(numVirtualFiles)
+ .PARAMWRAP(res);
+ }
+
+ HOOK_END
+ return res;
+}
+
+DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryObject(
+ HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass,
+ PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength)
+{
+ NTSTATUS res = STATUS_SUCCESS;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+ if (!callContext.active()) {
+ return ::NtQueryObject(Handle, ObjectInformationClass, ObjectInformation,
+ ObjectInformationLength, ReturnLength);
+ }
+
+ PRE_REALCALL
+ res = ::NtQueryObject(Handle, ObjectInformationClass, ObjectInformation,
+ ObjectInformationLength, ReturnLength);
+ POST_REALCALL
+
+ // we handle both SUCCESS and BUFFER_OVERFLOW since the fixed name might be
+ // smaller than the original one
+ //
+ // we only handle STATUS_INFO_LENGTH_MISMATCH if ReturnLength is not NULL since
+ // this is only returned if the length is too small to hold the structure itself
+ // (regardless of the name), in which case, we need to compute our own ReturnLength
+ //
+ if ((res == STATUS_SUCCESS || res == STATUS_BUFFER_OVERFLOW ||
+ (res == STATUS_INFO_LENGTH_MISMATCH && ReturnLength)) &&
+ (ObjectInformationClass == ObjectNameInformation)) {
+ const auto trackerInfo = ntdllHandleTracker.lookup(Handle);
+ const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo);
+
+ OBJECT_NAME_INFORMATION* info =
+ reinterpret_cast<OBJECT_NAME_INFORMATION*>(ObjectInformation);
+
+ if (redir.redirected) {
+ // https://learn.microsoft.com/en-us/windows/win32/fileio/displaying-volume-paths
+ //
+
+ // TODO: is that always true?
+ // path should start with \??\X: - we need to replace this by device name
+ //
+ WCHAR deviceName[MAX_PATH];
+ std::wstring buffer(static_cast<LPCWSTR>(trackerInfo));
+ buffer[6] = L'\0';
+
+ QueryDosDeviceW(buffer.data() + 4, deviceName, ARRAYSIZE(deviceName));
+
+ buffer = std::wstring(deviceName) + L'\\' +
+ std::wstring(buffer.data() + 7, buffer.size() - 7);
+
+ // the name is put in the buffer AFTER the struct, so the required size if
+ // sizeof(OBJECT_NAME_INFORMATION) + the number of bytes for the name + 2 bytes
+ // for a wide null character
+ const auto requiredLength =
+ sizeof(OBJECT_NAME_INFORMATION) + buffer.size() * 2 + 2;
+ if (ObjectInformationLength < requiredLength) {
+ // if the status was info length mismatch, we keep it, we are just going to
+ // update *ReturnLength
+ if (res != STATUS_INFO_LENGTH_MISMATCH) {
+ res = STATUS_BUFFER_OVERFLOW;
+ }
+
+ if (ReturnLength) {
+ *ReturnLength = static_cast<ULONG>(requiredLength);
+ }
+ } else {
+ // put the unicode buffer at the end of the object
+ const USHORT unicodeBufferLength = static_cast<USHORT>(std::min(
+ static_cast<unsigned long long>(std::numeric_limits<USHORT>::max()),
+ static_cast<unsigned long long>(ObjectInformationLength -
+ sizeof(OBJECT_NAME_INFORMATION))));
+ LPWSTR unicodeBuffer = reinterpret_cast<LPWSTR>(
+ static_cast<LPSTR>(ObjectInformation) + sizeof(OBJECT_NAME_INFORMATION));
+
+ // copy the path into the buffer
+ wmemcpy_s(unicodeBuffer, unicodeBufferLength, buffer.data(), buffer.size());
+
+ // set the null character
+ unicodeBuffer[buffer.size()] = L'\0';
+
+ // update the actual unicode string
+ info->Name.Buffer = unicodeBuffer;
+ info->Name.Length = static_cast<USHORT>(buffer.size() * 2);
+ info->Name.MaximumLength = unicodeBufferLength;
+
+ res = STATUS_SUCCESS;
+ }
+ }
+
+ auto logger = LOG_CALL()
+ .PARAMWRAP(res)
+ .PARAM(ObjectInformationLength)
+ .addParam("return_length", ReturnLength ? *ReturnLength : -1)
+ .addParam("tracker_path", trackerInfo)
+ .PARAM(ObjectInformationClass)
+ .PARAM(redir.redirected)
+ .PARAM(redir.path);
+
+ if (res == STATUS_SUCCESS) {
+ logger.addParam("name_info", info->Name);
+ } else {
+ logger.addParam("name_info", "");
+ }
+ }
+
+ HOOK_END
+ return res;
+}
+
+DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile(
+ HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation,
+ ULONG Length, FILE_INFORMATION_CLASS FileInformationClass)
+{
+ NTSTATUS res = STATUS_SUCCESS;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+ if (!callContext.active()) {
+ return ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length,
+ FileInformationClass);
+ }
+
+ PRE_REALCALL
+ res = ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length,
+ FileInformationClass);
+ POST_REALCALL
+
+ // we handle both SUCCESS and BUFFER_OVERFLOW since the fixed name might be
+ // smaller than the original one
+ //
+ // we do not handle STATUS_INFO_LENGTH_MISMATCH because this is only returned if
+ // the length is too small to old the structure itself (regardless of the name)
+ //
+ // TODO: currently, this does not handle STATUS_BUFFER_OVERLOW for ALL information
+ // because most of the structures would need to be manually filled, which is very
+ // complicated - this should not pose huge issue
+ //
+ if (((res == STATUS_SUCCESS || res == STATUS_BUFFER_OVERFLOW) &&
+ (FileInformationClass == FileNameInformation ||
+ FileInformationClass == FileNormalizedNameInformation)) ||
+ (res == STATUS_SUCCESS && FileInformationClass == FileAllInformation)) {
+
+ const auto trackerInfo = ntdllHandleTracker.lookup(FileHandle);
+ const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo);
+
+ // TODO: difference between FileNameInformation and FileNormalizedNameInformation
+
+ // maximum length in bytes - the required length is
+ // - for ALL, 100 + the number of bytes in the name (not account for null character)
+ // - for NAME, 4 + the number of bytes in the name (not accounting for null
+ // character)
+ //
+ // it is close to the sizeof the structure + the number of bytes in the name, except
+ // for the alignment that gives us a bit more space
+ //
+ ULONG prefixStructLength;
+ FILE_NAME_INFORMATION* info;
+ if (FileInformationClass == FileAllInformation) {
+ info = &reinterpret_cast<FILE_ALL_INFORMATION*>(FileInformation)->NameInformation;
+ prefixStructLength = sizeof(FILE_ALL_INFORMATION) - 4;
+ } else {
+ info = reinterpret_cast<FILE_NAME_INFORMATION*>(FileInformation);
+ prefixStructLength = sizeof(FILE_NAME_INFORMATION) - 4;
+ }
+
+ if (redir.redirected) {
+ auto requiredLength = prefixStructLength + 2 * (trackerInfo.size() - 6);
+ if (Length < requiredLength) {
+ res = STATUS_BUFFER_OVERFLOW;
+ } else {
+ // strip the \??\X: prefix (X being the drive name)
+ LPCWSTR filenameFixed = static_cast<LPCWSTR>(trackerInfo) + 6;
+
+ // not using SetInfoFilename because the length is not set and we do not need to
+ // 0-out the memory here
+ info->FileNameLength = static_cast<ULONG>((trackerInfo.size() - 6) * 2);
+ wmemcpy(info->FileName, filenameFixed, trackerInfo.size() - 6);
+ res = STATUS_SUCCESS;
+
+ // update status block, Information is the number of bytes written, basically
+ // the required length
+ IoStatusBlock->Status = STATUS_SUCCESS;
+ IoStatusBlock->Information = static_cast<ULONG_PTR>(requiredLength);
+ }
+ }
+
+ LOG_CALL()
+ .PARAMWRAP(res)
+ .addParam("tracker_path", trackerInfo)
+ .PARAM(FileInformationClass)
+ .PARAM(redir.redirected)
+ .PARAM(redir.path)
+ .addParam("name_info", res == STATUS_SUCCESS
+ ? std::wstring{info->FileName,
+ info->FileNameLength / sizeof(WCHAR)}
+ : std::wstring{});
+ }
+
+ HOOK_END
+ return res;
+}
+
+unique_ptr_deleter<OBJECT_ATTRIBUTES>
+makeObjectAttributes(RedirectionInfo& redirInfo, POBJECT_ATTRIBUTES attributeTemplate)
+{
+ if (redirInfo.redirected) {
+ unique_ptr_deleter<OBJECT_ATTRIBUTES> result(new OBJECT_ATTRIBUTES,
+ [](OBJECT_ATTRIBUTES* ptr) {
+ delete ptr;
+ });
+ memcpy(result.get(), attributeTemplate, sizeof(OBJECT_ATTRIBUTES));
+ result->RootDirectory = nullptr;
+ result->ObjectName = static_cast<PUNICODE_STRING>(redirInfo.path);
+ return result;
+ } else {
+ // just reuse the template with a dummy deleter
+ return unique_ptr_deleter<OBJECT_ATTRIBUTES>(attributeTemplate,
+ [](OBJECT_ATTRIBUTES*) {});
+ }
+}
+
+DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationByName(
+ POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock,
+ PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass)
+{
+ NTSTATUS res = STATUS_SUCCESS;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+
+ if (!callContext.active()) {
+ res = ::NtQueryInformationByName(ObjectAttributes, IoStatusBlock, FileInformation,
+ Length, FileInformationClass);
+ callContext.updateLastError();
+ return res;
+ }
+
+ RedirectionInfo redir =
+ applyReroute(READ_CONTEXT(), callContext, CreateUnicodeString(ObjectAttributes));
+
+ if (redir.redirected) {
+ auto newObjectAttributes = makeObjectAttributes(redir, ObjectAttributes);
+
+ PRE_REALCALL
+ res = ::NtQueryInformationByName(newObjectAttributes.get(), IoStatusBlock,
+ FileInformation, Length, FileInformationClass);
+ POST_REALCALL
+
+ LOG_CALL()
+ .PARAMWRAP(res)
+ .addParam("input_path", *ObjectAttributes->ObjectName)
+ .addParam("reroute_path", redir.path);
+ } else {
+ PRE_REALCALL
+ res = ::NtQueryInformationByName(ObjectAttributes, IoStatusBlock, FileInformation,
+ Length, FileInformationClass);
+ POST_REALCALL
+ }
+
+ HOOK_END
+ return res;
+}
+
+NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess,
+ POBJECT_ATTRIBUTES ObjectAttributes,
+ PIO_STATUS_BLOCK IoStatusBlock, ULONG ShareAccess,
+ ULONG OpenOptions)
+{
+ using namespace usvfs;
+
+ PreserveGetLastError ntFunctionsDoNotChangeGetLastError;
+
+ NTSTATUS res = STATUS_NO_SUCH_FILE;
+
+ HOOK_START_GROUP(MutExHookGroup::OPEN_FILE)
+ // Why is the usual if (!callContext.active()... check missing?
+
+ bool storePath = false;
+ if (((OpenOptions & FILE_DIRECTORY_FILE) != 0UL) &&
+ ((OpenOptions & FILE_OPEN_FOR_BACKUP_INTENT) != 0UL)) {
+ // this may be an attempt to open a directory handle for iterating.
+ // If so we need to treat it a little bit differently
+ /* usvfs::FunctionGroupLock lock(usvfs::MutExHookGroup::FILE_ATTRIBUTES);
+ FILE_BASIC_INFORMATION dummy;
+ storePath = FAILED(NtQueryAttributesFile(ObjectAttributes, &dummy));*/
+ storePath = true;
+ }
+
+ UnicodeString fullName = CreateUnicodeString(ObjectAttributes);
+
+ if (isDeviceFile(static_cast<LPCWSTR>(fullName))) {
+ return ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
+ ShareAccess, OpenOptions);
+ }
+
+ UnicodeString Path = ntdllHandleTracker.lookup(ObjectAttributes->RootDirectory);
+
+ std::wstring checkpath =
+ ush::string_cast<std::wstring>(static_cast<LPCWSTR>(Path), ush::CodePage::UTF8);
+
+ if ((fullName.size() == 0) ||
+ (GetFileSize(ObjectAttributes->RootDirectory, nullptr) != INVALID_FILE_SIZE)) {
+ // //relative paths that we don't have permission over will fail here due that we
+ // can't get the filesize of the root directory
+ // //We should try again to see if it is a directory using another method
+ if ((fullName.size() == 0) ||
+ (GetFileAttributesW((LPCWSTR)checkpath.c_str()) == INVALID_FILE_ATTRIBUTES)) {
+ return ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
+ ShareAccess, OpenOptions);
+ }
+ }
+
+ try {
+ RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, fullName);
+ unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes =
+ makeObjectAttributes(redir, ObjectAttributes);
+
+ PRE_REALCALL
+ res = ::NtOpenFile(FileHandle, DesiredAccess, adjustedAttributes.get(),
+ IoStatusBlock, ShareAccess, OpenOptions);
+ POST_REALCALL
+ if (SUCCEEDED(res) && storePath) {
+ // store the original search path for use during iteration
+ READ_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[*FileHandle] =
+ static_cast<LPCWSTR>(fullName);
+#pragma message("need to clean up this handle in CloseHandle call")
+ }
+
+ if (redir.redirected) {
+ LOG_CALL()
+ .addParam("source", ObjectAttributes)
+ .addParam("rerouted", adjustedAttributes.get())
+ .PARAM(*FileHandle)
+ .PARAM(OpenOptions)
+ .PARAMWRAP(res);
+ }
+ } catch (const std::exception&) {
+ PRE_REALCALL
+ res = ::NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
+ ShareAccess, OpenOptions);
+ POST_REALCALL
+ }
+ HOOK_END
+
+ return res;
+}
+
+NTSTATUS WINAPI usvfs::hook_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess,
+ POBJECT_ATTRIBUTES ObjectAttributes,
+ PIO_STATUS_BLOCK IoStatusBlock,
+ ULONG ShareAccess, ULONG OpenOptions)
+{
+ NTSTATUS res = ntdll_mess_NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes,
+ IoStatusBlock, ShareAccess, OpenOptions);
+ if (res >= 0 && ObjectAttributes && FileHandle &&
+ GetFileType(*FileHandle) == FILE_TYPE_DISK)
+ ntdllHandleTracker.insert(*FileHandle, CreateUnicodeString(ObjectAttributes));
+ return res;
+}
+
+NTSTATUS ntdll_mess_NtCreateFile(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)
+{
+ using namespace usvfs;
+
+ NTSTATUS res = STATUS_NO_SUCH_FILE;
+
+ PreserveGetLastError ntFunctionsDoNotChangeGetLastError;
+
+ HOOK_START_GROUP(MutExHookGroup::OPEN_FILE)
+ if (!callContext.active()) {
+ return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
+ AllocationSize, FileAttributes, ShareAccess,
+ CreateDisposition, CreateOptions, EaBuffer, EaLength);
+ }
+
+ UnicodeString inPath = CreateUnicodeString(ObjectAttributes);
+ LPCWSTR inPathW = static_cast<LPCWSTR>(inPath);
+
+ if (inPath.size() == 0) {
+ spdlog::get("hooks")->info(
+ "failed to set from handle: {0}",
+ ush::string_cast<std::string>(ObjectAttributes->ObjectName->Buffer));
+ return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
+ AllocationSize, FileAttributes, ShareAccess,
+ CreateDisposition, CreateOptions, EaBuffer, EaLength);
+ }
+
+ if (isDeviceFile(inPathW)) {
+ return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
+ AllocationSize, FileAttributes, ShareAccess,
+ CreateDisposition, CreateOptions, EaBuffer, EaLength);
+ }
+
+ DWORD convertedDisposition = OPEN_EXISTING;
+ switch (CreateDisposition) {
+ case FILE_SUPERSEDE:
+ convertedDisposition = CREATE_ALWAYS;
+ break;
+ case FILE_OPEN:
+ convertedDisposition = OPEN_EXISTING;
+ break;
+ case FILE_CREATE:
+ convertedDisposition = CREATE_NEW;
+ break;
+ case FILE_OPEN_IF:
+ convertedDisposition = OPEN_ALWAYS;
+ break;
+ case FILE_OVERWRITE:
+ convertedDisposition = TRUNCATE_EXISTING;
+ break;
+ case FILE_OVERWRITE_IF:
+ convertedDisposition = CREATE_ALWAYS;
+ break;
+ default:
+ spdlog::get("hooks")->error("invalid disposition: {0}", CreateDisposition);
+ break;
+ }
+
+ DWORD convertedAccess = 0;
+ if ((DesiredAccess & FILE_GENERIC_READ) == FILE_GENERIC_READ)
+ convertedAccess |= GENERIC_READ;
+ if ((DesiredAccess & FILE_GENERIC_WRITE) == FILE_GENERIC_WRITE)
+ convertedAccess |= GENERIC_WRITE;
+ if ((DesiredAccess & FILE_GENERIC_EXECUTE) == FILE_GENERIC_EXECUTE)
+ convertedAccess |= GENERIC_EXECUTE;
+ if ((DesiredAccess & FILE_ALL_ACCESS) == FILE_ALL_ACCESS)
+ convertedAccess |= GENERIC_ALL;
+
+ ULONG originalDisposition = CreateDisposition;
+ CreateRerouter rerouter;
+ if (rerouter.rerouteCreate(
+ READ_CONTEXT(), callContext, inPathW, convertedDisposition, convertedAccess,
+ (LPSECURITY_ATTRIBUTES)ObjectAttributes->SecurityDescriptor)) {
+ switch (convertedDisposition) {
+ case CREATE_NEW:
+ CreateDisposition = FILE_CREATE;
+ break;
+ case CREATE_ALWAYS:
+ if (CreateDisposition != FILE_SUPERSEDE)
+ CreateDisposition = FILE_OVERWRITE_IF;
+ break;
+ case OPEN_EXISTING:
+ CreateDisposition = FILE_OPEN;
+ break;
+ case OPEN_ALWAYS:
+ CreateDisposition = FILE_OPEN_IF;
+ break;
+ case TRUNCATE_EXISTING:
+ CreateDisposition = FILE_OVERWRITE;
+ break;
+ }
+
+ RedirectionInfo redir = applyReroute(rerouter);
+
+ unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes =
+ makeObjectAttributes(redir, ObjectAttributes);
+
+ PRE_REALCALL
+ res = ::NtCreateFile(FileHandle, DesiredAccess, adjustedAttributes.get(),
+ IoStatusBlock, AllocationSize, FileAttributes, ShareAccess,
+ CreateDisposition, CreateOptions, EaBuffer, EaLength);
+ POST_REALCALL
+ rerouter.updateResult(callContext, res == STATUS_SUCCESS);
+
+ if (res == STATUS_SUCCESS) {
+ if (rerouter.newReroute())
+ rerouter.insertMapping(WRITE_CONTEXT());
+
+ if (rerouter.isDir() && rerouter.wasRerouted() &&
+ ((FileAttributes & FILE_OPEN_FOR_BACKUP_INTENT) ==
+ FILE_OPEN_FOR_BACKUP_INTENT)) {
+ // store the original search path for use during iteration
+ WRITE_CONTEXT()->customData<SearchHandleMap>(SearchHandles)[*FileHandle] =
+ inPathW;
+ }
+ }
+
+ if (rerouter.wasRerouted() || rerouter.changedError() ||
+ originalDisposition != CreateDisposition) {
+ LOG_CALL()
+ .PARAM(inPathW)
+ .PARAM(rerouter.fileName())
+ .PARAMHEX(DesiredAccess)
+ .PARAMHEX(originalDisposition)
+ .PARAMHEX(CreateDisposition)
+ .PARAMHEX(FileAttributes)
+ .PARAMHEX(res)
+ .PARAMHEX(*FileHandle)
+ .PARAM(rerouter.originalError())
+ .PARAM(rerouter.error());
+ }
+ } else {
+ // make the original call to set up the proper errors and return statuses
+ PRE_REALCALL
+ res = ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock,
+ AllocationSize, FileAttributes, ShareAccess, CreateDisposition,
+ CreateOptions, EaBuffer, EaLength);
+ POST_REALCALL
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+NTSTATUS WINAPI usvfs::hook_NtCreateFile(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)
+{
+ NTSTATUS res = ntdll_mess_NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes,
+ IoStatusBlock, AllocationSize, FileAttributes,
+ ShareAccess, CreateDisposition, CreateOptions,
+ EaBuffer, EaLength);
+ if (res >= 0 && ObjectAttributes && FileHandle &&
+ GetFileType(*FileHandle) == FILE_TYPE_DISK)
+ ntdllHandleTracker.insert(*FileHandle, CreateUnicodeString(ObjectAttributes));
+ return res;
+}
+
+NTSTATUS WINAPI usvfs::hook_NtClose(HANDLE Handle)
+{
+ PreserveGetLastError ntFunctionsDoNotChangeGetLastError;
+
+ NTSTATUS res = STATUS_NO_SUCH_FILE;
+
+ HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS)
+ bool log = false;
+
+ if ((::GetFileType(Handle) == FILE_TYPE_DISK)) {
+ HookContext::Ptr context = WRITE_CONTEXT();
+
+ { // clean up search data associated with this handle part 1
+ Searches& activeSearches = context->customData<Searches>(SearchInfo);
+ // std::lock_guard<std::recursive_mutex> lock(activeSearches.queryMutex);
+ auto iter = activeSearches.info.find(Handle);
+ if (iter != activeSearches.info.end()) {
+ if (iter->second.currentSearchHandle != INVALID_HANDLE_VALUE) {
+ ::CloseHandle(iter->second.currentSearchHandle);
+ }
+
+ activeSearches.info.erase(iter);
+ log = true;
+ }
+ }
+
+ {
+ SearchHandleMap& searchHandles =
+ context->customData<SearchHandleMap>(SearchHandles);
+ auto iter = searchHandles.find(Handle);
+ if (iter != searchHandles.end()) {
+ searchHandles.erase(iter);
+ log = true;
+ }
+ }
+ }
+
+ if (GetFileType(Handle) == FILE_TYPE_DISK)
+ ntdllHandleTracker.erase(Handle);
+
+ PRE_REALCALL
+ res = ::NtClose(Handle);
+ POST_REALCALL
+
+ if (log) {
+ LOG_CALL().PARAM(Handle).PARAMWRAP(res);
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+NTSTATUS WINAPI usvfs::hook_NtQueryAttributesFile(
+ POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation)
+{
+ PreserveGetLastError ntFunctionsDoNotChangeGetLastError;
+
+ NTSTATUS res = STATUS_SUCCESS;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+ // Why is the usual if (!callContext.active()... check missing?
+
+ UnicodeString inPath = CreateUnicodeString(ObjectAttributes);
+
+ RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, inPath);
+ unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes =
+ makeObjectAttributes(redir, ObjectAttributes);
+
+ PRE_REALCALL
+ res = ::NtQueryAttributesFile(adjustedAttributes.get(), FileInformation);
+ POST_REALCALL
+
+ if (redir.redirected) {
+ LOG_CALL()
+ .addParam("source", ObjectAttributes)
+ .addParam("rerouted", adjustedAttributes.get())
+ .PARAMWRAP(res);
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+NTSTATUS WINAPI usvfs::hook_NtQueryFullAttributesFile(
+ POBJECT_ATTRIBUTES ObjectAttributes, PFILE_NETWORK_OPEN_INFORMATION FileInformation)
+{
+ PreserveGetLastError ntFunctionsDoNotChangeGetLastError;
+
+ NTSTATUS res = STATUS_SUCCESS;
+
+ HOOK_START_GROUP(MutExHookGroup::FILE_ATTRIBUTES)
+
+ if (!callContext.active()) {
+ return ::NtQueryFullAttributesFile(ObjectAttributes, FileInformation);
+ }
+
+ UnicodeString inPath;
+ try {
+ inPath = CreateUnicodeString(ObjectAttributes);
+ } catch (const std::exception&) {
+ return ::NtQueryFullAttributesFile(ObjectAttributes, FileInformation);
+ }
+
+ RedirectionInfo redir = applyReroute(READ_CONTEXT(), callContext, inPath);
+ unique_ptr_deleter<OBJECT_ATTRIBUTES> adjustedAttributes =
+ makeObjectAttributes(redir, ObjectAttributes);
+
+ PRE_REALCALL
+ res = ::NtQueryFullAttributesFile(adjustedAttributes.get(), FileInformation);
+ POST_REALCALL
+
+ if (redir.redirected) {
+ LOG_CALL()
+ .addParam("source", ObjectAttributes)
+ .addParam("rerouted", adjustedAttributes.get())
+ .PARAMWRAP(res);
+ }
+
+ HOOK_END
+
+ return res;
+}
+
+NTSTATUS WINAPI usvfs::hook_NtTerminateProcess(HANDLE ProcessHandle,
+ NTSTATUS ExitStatus)
+{
+ // this hook is normally called when terminating another process, in which
+ // case there's nothing to do
+ //
+ // if the current process exits normally, the ExitProcess() hook is called
+ // and disconnects from the vfs; if the current process crashes, this hook
+ // is not called, the process just dies
+ //
+ // but a process can also terminate itself, bypassing ExitProcess(), and
+ // ending up here, in which case the vfs should be disconnected
+ //
+ // NtTerminateProcess() can be called two different ways to terminate the
+ // current process:
+ // - with a valid handle for the current process, or
+ // - with -1, because that's what GetCurrentProcess() returns
+ //
+ // it's unclear what a NULL handle represents, the behaviour is not
+ // documented anywhere, but looking at ReactOS, it's also interpreted as the
+ // current process
+
+ NTSTATUS res = STATUS_SUCCESS;
+
+ HOOK_START
+
+ const bool isCurrentProcess = ProcessHandle == (HANDLE)-1 || ProcessHandle == 0 ||
+ GetProcessId(ProcessHandle) == GetCurrentProcessId();
+
+ if (isCurrentProcess) {
+ usvfsDisconnectVFS();
+ }
+
+ res = ::NtTerminateProcess(ProcessHandle, ExitStatus);
+
+ HOOK_END
+
+ return res;
+}
diff --git a/libs/usvfs/src/usvfs_dll/hooks/ntdll.h b/libs/usvfs/src/usvfs_dll/hooks/ntdll.h
new file mode 100644
index 0000000..f3db4a6
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hooks/ntdll.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include <dllimport.h>
+#include <loghelpers.h>
+#include <ntdll_declarations.h>
+
+namespace usvfs
+{
+
+DLLEXPORT NTSTATUS WINAPI
+hook_NtQueryFullAttributesFile(POBJECT_ATTRIBUTES ObjectAttributes,
+ PFILE_NETWORK_OPEN_INFORMATION FileInformation);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtQueryAttributesFile(
+ POBJECT_ATTRIBUTES ObjectAttributes, PFILE_BASIC_INFORMATION FileInformation);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtQueryDirectoryFile(
+ HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext,
+ PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length,
+ FILE_INFORMATION_CLASS FileInformationClass, BOOLEAN ReturnSingleEntry,
+ PUNICODE_STRING FileName, BOOLEAN RestartScan);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtQueryDirectoryFileEx(
+ HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext,
+ PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length,
+ FILE_INFORMATION_CLASS FileInformationClass, ULONG QueryFlags,
+ PUNICODE_STRING FileName);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtQueryObject(
+ HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass,
+ PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationFile(
+ HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation,
+ ULONG Length, FILE_INFORMATION_CLASS FileInformationClass);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationByName(
+ POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock,
+ PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess,
+ POBJECT_ATTRIBUTES ObjectAttributes,
+ PIO_STATUS_BLOCK IoStatusBlock,
+ ULONG ShareAccess, ULONG OpenOptions);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtCreateFile(
+ 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);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtClose(HANDLE Handle);
+
+DLLEXPORT NTSTATUS WINAPI hook_NtTerminateProcess(HANDLE ProcessHandle,
+ NTSTATUS ExitStatus);
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/hooks/sharedids.h b/libs/usvfs/src/usvfs_dll/hooks/sharedids.h
new file mode 100644
index 0000000..9a7531c
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/hooks/sharedids.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "../hookcontext.h"
+
+typedef std::map<HANDLE, std::wstring> SearchHandleMap;
+
+// maps handles opened for searching to the original search path, which is
+// necessary if the handle creation was rerouted
+DATA_ID(SearchHandles);
diff --git a/libs/usvfs/src/usvfs_dll/maptracker.h b/libs/usvfs/src/usvfs_dll/maptracker.h
new file mode 100644
index 0000000..6ddf471
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/maptracker.h
@@ -0,0 +1,691 @@
+#pragma once
+
+#include "hookcallcontext.h"
+#include "hookcontext.h"
+#include "stringcast.h"
+
+namespace usvfs
+{
+
+// returns true iff the path exists (checks only real paths)
+static inline bool pathExists(LPCWSTR fileName)
+{
+ FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES);
+ DWORD attrib = GetFileAttributesW(fileName);
+ return attrib != INVALID_FILE_ATTRIBUTES;
+}
+
+// returns true iff the path exists and is a file (checks only real paths)
+static inline bool pathIsFile(LPCWSTR fileName)
+{
+ FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES);
+ DWORD attrib = GetFileAttributesW(fileName);
+ return attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY) == 0;
+}
+
+// returns true iff the path exists and is a file (checks only real paths)
+static inline bool pathIsDirectory(LPCWSTR fileName)
+{
+ FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES);
+ DWORD attrib = GetFileAttributesW(fileName);
+ return attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY);
+}
+
+// returns true iff the path does not exist but it parent directory does (checks only
+// real paths)
+static inline bool pathDirectlyAvailable(LPCWSTR pathName)
+{
+ FunctionGroupLock lock(MutExHookGroup::FILE_ATTRIBUTES);
+ DWORD attrib = GetFileAttributesW(pathName);
+ return attrib == INVALID_FILE_ATTRIBUTES && GetLastError() == ERROR_FILE_NOT_FOUND;
+}
+
+class MapTracker
+{
+public:
+ std::wstring lookup(const std::wstring& fromPath) const
+ {
+ if (!fromPath.empty()) {
+ std::shared_lock<std::shared_mutex> lock(m_mutex);
+ auto find = m_map.find(fromPath);
+ if (find != m_map.end())
+ return find->second;
+ }
+ return std::wstring();
+ }
+
+ bool contains(const std::wstring& fromPath) const
+ {
+ if (!fromPath.empty()) {
+ std::shared_lock<std::shared_mutex> lock(m_mutex);
+ auto find = m_map.find(fromPath);
+ if (find != m_map.end())
+ return true;
+ }
+ return false;
+ }
+
+ void insert(const std::wstring& fromPath, const std::wstring& toPath)
+ {
+ if (fromPath.empty())
+ return;
+ std::unique_lock<std::shared_mutex> lock(m_mutex);
+ m_map[fromPath] = toPath;
+ }
+
+ bool erase(const std::wstring& fromPath)
+ {
+ if (fromPath.empty())
+ return false;
+ std::unique_lock<std::shared_mutex> lock(m_mutex);
+ return m_map.erase(fromPath);
+ }
+
+private:
+ mutable std::shared_mutex m_mutex;
+ std::unordered_map<std::wstring, std::wstring> m_map;
+};
+
+extern MapTracker k32DeleteTracker;
+extern MapTracker k32FakeDirTracker;
+
+class RerouteW
+{
+ std::wstring m_Buffer{};
+ std::wstring m_RealPath{};
+ bool m_Rerouted{false};
+ LPCWSTR m_FileName{nullptr};
+ bool m_PathCreated{false};
+ bool m_NewReroute{false};
+
+ RedirectionTree::NodePtrT m_FileNode;
+
+public:
+ RerouteW() = default;
+
+ RerouteW(RerouteW&& reference)
+ : m_Buffer(std::move(reference.m_Buffer)),
+ m_RealPath(std::move(reference.m_RealPath)), m_Rerouted(reference.m_Rerouted),
+ m_PathCreated(reference.m_PathCreated), m_NewReroute(reference.m_NewReroute),
+ m_FileNode(std::move(reference.m_FileNode))
+ {
+ m_FileName = reference.m_FileName != nullptr ? m_Buffer.c_str() : nullptr;
+ reference.m_FileName = nullptr;
+ }
+
+ RerouteW& operator=(RerouteW&& reference)
+ {
+ m_Buffer = std::move(reference.m_Buffer);
+ m_RealPath = std::move(reference.m_RealPath);
+ m_Rerouted = reference.m_Rerouted;
+ m_PathCreated = reference.m_PathCreated;
+ m_NewReroute = reference.m_NewReroute;
+ m_FileName = reference.m_FileName != nullptr ? m_Buffer.c_str() : nullptr;
+ m_FileNode = std::move(reference.m_FileNode);
+ return *this;
+ }
+
+ RerouteW(const RerouteW& reference) = delete;
+ RerouteW& operator=(const RerouteW&) = delete;
+
+ LPCWSTR fileName() const { return m_FileName; }
+
+ const std::wstring& buffer() const { return m_Buffer; }
+
+ bool wasRerouted() const { return m_Rerouted; }
+
+ bool newReroute() const { return m_NewReroute; }
+
+ void insertMapping(const HookContext::Ptr& context, bool directory = false)
+ {
+ if (directory) {
+ addDirectoryMapping(context, m_RealPath, m_FileName);
+
+ // In case we have just created a "fake" directory, it is no longer fake and need
+ // to remove it and all its parent folders from the fake map:
+ std::wstring dir = m_FileName;
+ while (k32FakeDirTracker.erase(dir))
+ dir = fs::path(dir).parent_path().wstring();
+ } else {
+ // if (m_PathCreated)
+ // addDirectoryMapping(context, fs::path(m_RealPath).parent_path(),
+ // fs::path(m_FileName).parent_path());
+
+ spdlog::get("hooks")->info(
+ "mapping file in vfs: {}, {}",
+ shared::string_cast<std::string>(m_RealPath, shared::CodePage::UTF8),
+ shared::string_cast<std::string>(m_FileName, shared::CodePage::UTF8));
+ m_FileNode = context->redirectionTable().addFile(
+ m_RealPath, RedirectionDataLocal(shared::string_cast<std::string>(
+ m_FileName, shared::CodePage::UTF8)));
+
+ k32DeleteTracker.erase(m_RealPath);
+ }
+ }
+
+ void removeMapping(const HookContext::ConstPtr& readContext, bool directory = false)
+ {
+ bool addToDelete = false;
+ bool dontAddToDelete = false;
+
+ // We need to track deleted files even if they were not rerouted (i.e. files deleted
+ // from the real folder which there is a virtualized mapped folder on top of it).
+ // Since we don't want to add, *every* file which is deleted we check this:
+ bool found = wasRerouted();
+ if (!found) {
+ FindCreateTarget visitor;
+ RedirectionTree::VisitorFunction visitorWrapper =
+ [&](const RedirectionTree::NodePtrT& node) {
+ visitor(node);
+ };
+ readContext->redirectionTable()->visitPath(m_RealPath, visitorWrapper);
+ if (visitor.target.get())
+ found = true;
+ }
+ if (found)
+ addToDelete = true;
+
+ if (wasRerouted()) {
+ if (m_FileNode.get())
+ m_FileNode->removeFromTree();
+ else
+ spdlog::get("usvfs")->warn("Node not removed: {}",
+ shared::string_cast<std::string>(m_FileName));
+
+ if (!directory) {
+ // check if this file was the last file inside a "fake" directory then remove it
+ // and possibly also its fake empty parent folders:
+ std::wstring parent = m_FileName;
+ while (true) {
+ parent = fs::path(parent).parent_path().wstring();
+ if (k32FakeDirTracker.contains(parent)) {
+ dontAddToDelete = true;
+ if (RemoveDirectoryW(parent.c_str())) {
+ k32FakeDirTracker.erase(parent);
+ spdlog::get("usvfs")->info("removed empty fake directory: {}",
+ shared::string_cast<std::string>(parent));
+ } else if (GetLastError() != ERROR_DIR_NOT_EMPTY) {
+ auto error = GetLastError();
+ spdlog::get("usvfs")->warn("removing fake directory failed: {}, error={}",
+ shared::string_cast<std::string>(parent),
+ error);
+ break;
+ }
+ } else
+ break;
+ }
+ }
+ }
+ if (addToDelete && !dontAddToDelete) {
+ k32DeleteTracker.insert(m_RealPath, m_FileName);
+ }
+ }
+
+ static bool createFakePath(fs::path path, LPSECURITY_ATTRIBUTES securityAttributes)
+ {
+ // sanity and guaranteed recursion end:
+ if (!path.has_relative_path())
+ throw shared::windows_error(
+ "createFakePath() refusing to create non-existing top level path: " +
+ path.string());
+
+ DWORD attr = GetFileAttributesW(path.c_str());
+ DWORD err = GetLastError();
+ if (attr != INVALID_FILE_ATTRIBUTES) {
+ if (attr & FILE_ATTRIBUTE_DIRECTORY)
+ return false; // if directory already exists all is good
+ else
+ throw shared::windows_error("createFakePath() called on a file: " +
+ path.string());
+ }
+ if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND)
+ throw shared::windows_error(
+ "createFakePath() GetFileAttributesW failed on: " + path.string(), err);
+
+ if (err != ERROR_FILE_NOT_FOUND) // ERROR_FILE_NOT_FOUND means parent directory
+ // already exists
+ createFakePath(
+ path.parent_path(),
+ securityAttributes); // otherwise create parent directory (recursively)
+
+ BOOL res = CreateDirectoryW(path.c_str(), securityAttributes);
+ if (res)
+ k32FakeDirTracker.insert(path.wstring(), std::wstring());
+ else {
+ err = GetLastError();
+ throw shared::windows_error(
+ "createFakePath() CreateDirectoryW failed on: " + path.string(), err);
+ }
+ return true;
+ }
+
+ static bool addDirectoryMapping(const HookContext::Ptr& context,
+ const fs::path& originalPath,
+ const fs::path& reroutedPath)
+ {
+ if (originalPath.empty() || reroutedPath.empty()) {
+ spdlog::get("hooks")->error("RerouteW::addDirectoryMapping failed: {}, {}",
+ shared::string_cast<std::string>(
+ originalPath.wstring(), shared::CodePage::UTF8)
+ .c_str(),
+ shared::string_cast<std::string>(
+ reroutedPath.wstring(), shared::CodePage::UTF8)
+ .c_str());
+ return false;
+ }
+
+ auto lookupParent =
+ context->redirectionTable()->findNode(originalPath.parent_path());
+ if (!lookupParent.get() || lookupParent->data().linkTarget.empty()) {
+ if (!addDirectoryMapping(context, originalPath.parent_path(),
+ reroutedPath.parent_path())) {
+ spdlog::get("hooks")->error("RerouteW::addDirectoryMapping failed: {}, {}",
+ shared::string_cast<std::string>(
+ originalPath.wstring(), shared::CodePage::UTF8)
+ .c_str(),
+ shared::string_cast<std::string>(
+ reroutedPath.wstring(), shared::CodePage::UTF8)
+ .c_str());
+ return false;
+ }
+ }
+
+ std::string reroutedU8 = shared::string_cast<std::string>(reroutedPath.wstring(),
+ shared::CodePage::UTF8);
+ if (reroutedU8.empty() || reroutedU8[reroutedU8.size() - 1] != '\\')
+ reroutedU8 += "\\";
+
+ spdlog::get("hooks")->info("mapping directory in vfs: {}, {}",
+ shared::string_cast<std::string>(originalPath.wstring(),
+ shared::CodePage::UTF8),
+ reroutedU8.c_str());
+
+ context->redirectionTable().addDirectory(
+ originalPath, RedirectionDataLocal(reroutedU8),
+ shared::FLAG_DIRECTORY | shared::FLAG_CREATETARGET);
+
+ fs::directory_iterator end_itr;
+
+ // cycle through the directory
+ for (fs::directory_iterator itr(reroutedPath); itr != end_itr; ++itr) {
+ // If it's not a directory, add it to the VFS, if it is recurse into it
+ if (is_regular_file(itr->path())) {
+ std::string fileReroutedU8 = shared::string_cast<std::string>(
+ itr->path().wstring(), shared::CodePage::UTF8);
+ spdlog::get("hooks")->info(
+ "mapping file in vfs: {}, {}",
+ shared::string_cast<std::string>(
+ (originalPath / itr->path().filename()).wstring(),
+ shared::CodePage::UTF8),
+ fileReroutedU8.c_str());
+ context->redirectionTable().addFile(
+ fs::path(originalPath / itr->path().filename()),
+ RedirectionDataLocal(fileReroutedU8));
+ } else {
+ addDirectoryMapping(context, originalPath / itr->path().filename(),
+ reroutedPath / itr->path().filename());
+ }
+ }
+
+ return true;
+ }
+
+ template <class char_t>
+ static bool interestingPathImpl(const char_t* inPath)
+ {
+ if (!inPath || !inPath[0])
+ return false;
+ // ignore \\.\ unless its a \\.\?:
+ if (inPath[0] == '\\' && inPath[1] == '\\' && inPath[2] == '.' &&
+ inPath[3] == '\\' && (!inPath[4] || inPath[5] != ':'))
+ return false;
+ // ignore L"hid#":
+ if ((inPath[0] == 'h' || inPath[0] == 'H') &&
+ ((inPath[1] == 'i' || inPath[1] == 'I')) &&
+ ((inPath[2] == 'd' || inPath[2] == 'D')) && inPath[3] == '#')
+ return false;
+ return true;
+ }
+
+ static bool interestingPath(const char* inPath)
+ {
+ return interestingPathImpl(inPath);
+ }
+ static bool interestingPath(const wchar_t* inPath)
+ {
+ return interestingPathImpl(inPath);
+ }
+
+ static fs::path absolutePath(const wchar_t* inPath)
+ {
+ if (shared::startswith(inPath, LR"(\\?\)") ||
+ shared::startswith(inPath, LR"(\??\)")) {
+ inPath += 4;
+ return inPath;
+ } else if ((shared::startswith(inPath, LR"(\\localhost\)") ||
+ shared::startswith(inPath, LR"(\\127.0.0.1\)")) &&
+ inPath[13] == L'$') {
+ std::wstring newPath;
+ newPath += towupper(inPath[12]);
+ newPath += L':';
+ newPath += &inPath[14];
+ return newPath;
+ } else if (inPath[0] == L'\0' || inPath[1] == L':') {
+ return inPath;
+ } else if (inPath[0] == L'\\' || inPath[0] == L'/') {
+ return fs::path(winapi::wide::getFullPathName(inPath).first);
+ }
+ WCHAR currentDirectory[MAX_PATH];
+ ::GetCurrentDirectoryW(MAX_PATH, currentDirectory);
+ fs::path finalPath = fs::path(currentDirectory) / inPath;
+ return finalPath;
+ // winapi::wide::getFullPathName(inPath).first;
+ }
+
+ static fs::path canonizePath(const fs::path& inPath)
+ {
+ fs::path p = inPath.lexically_normal();
+ if (p.filename_is_dot())
+ p = p.remove_filename();
+ return p.make_preferred();
+ }
+
+ static RerouteW create(const HookContext::ConstPtr& context,
+ const HookCallContext& callContext, const wchar_t* inPath,
+ bool inverse = false)
+ {
+ RerouteW result;
+
+ if (interestingPath(inPath) && callContext.active()) {
+ const auto& lookupPath = canonizePath(absolutePath(inPath));
+ result.m_RealPath = lookupPath.wstring();
+
+ result.m_Buffer = k32DeleteTracker.lookup(result.m_RealPath);
+ bool found = !result.m_Buffer.empty();
+ if (found) {
+ spdlog::get("hooks")->info(
+ "Rerouting file open to location of deleted file: {}",
+ shared::string_cast<std::string>(result.m_Buffer));
+ result.m_NewReroute = true;
+ } else {
+ const RedirectionTreeContainer& table =
+ inverse ? context->inverseTable() : context->redirectionTable();
+ result.m_FileNode = table->findNode(lookupPath);
+
+ if (result.m_FileNode.get() && (!result.m_FileNode->data().linkTarget.empty() ||
+ result.m_FileNode->isDirectory())) {
+ if (!result.m_FileNode->data().linkTarget.empty()) {
+ result.m_Buffer = shared::string_cast<std::wstring>(
+ result.m_FileNode->data().linkTarget.c_str(), shared::CodePage::UTF8);
+ } else {
+ result.m_Buffer = result.m_FileNode->path().wstring();
+ }
+ found = true;
+ }
+ }
+ if (found) {
+ result.m_Rerouted = true;
+
+ wchar_t inIt = inPath[wcslen(inPath) - 1];
+ std::wstring::iterator outIt = result.m_Buffer.end() - 1;
+ if ((*outIt == L'\\' || *outIt == L'/') && !(inIt == L'\\' || inIt == L'/'))
+ result.m_Buffer.erase(outIt);
+ std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\');
+ } else
+ result.m_Buffer = inPath;
+ } else if (inPath)
+ result.m_Buffer = inPath;
+
+ if (inPath)
+ result.m_FileName = result.m_Buffer.c_str();
+ return result;
+ }
+
+ static RerouteW createNew(const HookContext::ConstPtr& context,
+ const HookCallContext& callContext, LPCWSTR inPath,
+ bool createPath = true,
+ LPSECURITY_ATTRIBUTES securityAttributes = nullptr)
+ {
+ RerouteW result;
+
+ if (interestingPath(inPath) && callContext.active()) {
+ const auto& lookupPath = canonizePath(absolutePath(inPath));
+ result.m_RealPath = lookupPath.wstring();
+
+ result.m_Buffer = k32DeleteTracker.lookup(result.m_RealPath);
+ bool found = !result.m_Buffer.empty();
+ if (found)
+ spdlog::get("hooks")->info(
+ "Rerouting file creation to original location of deleted file: {}",
+ shared::string_cast<std::string>(result.m_Buffer));
+ else {
+ FindCreateTarget visitor;
+ RedirectionTree::VisitorFunction visitorWrapper =
+ [&](const RedirectionTree::NodePtrT& node) {
+ visitor(node);
+ };
+ context->redirectionTable()->visitPath(lookupPath, visitorWrapper);
+ if (visitor.target.get()) {
+ // the visitor has found the last (deepest in the directory hierarchy)
+ // create-target
+ fs::path relativePath =
+ shared::make_relative(visitor.target->path(), lookupPath);
+ result.m_Buffer =
+ (fs::path(visitor.target->data().linkTarget.c_str()) / relativePath)
+ .wstring();
+ found = true;
+ }
+ }
+
+ if (found) {
+ if (createPath) {
+ try {
+ FunctionGroupLock lock(MutExHookGroup::ALL_GROUPS);
+ result.m_PathCreated = createFakePath(
+ fs::path(result.m_Buffer).parent_path(), securityAttributes);
+ } catch (const std::exception& e) {
+ spdlog::get("hooks")->error(
+ "failed to create {}: {}",
+ shared::string_cast<std::string>(result.m_Buffer), e.what());
+ }
+ }
+
+ wchar_t inIt = inPath[wcslen(inPath) - 1];
+ std::wstring::iterator outIt = result.m_Buffer.end() - 1;
+ if ((*outIt == L'\\' || *outIt == L'/') && !(inIt == L'\\' || inIt == L'/'))
+ result.m_Buffer.erase(outIt);
+ std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\');
+ result.m_Rerouted = true;
+ result.m_NewReroute = true;
+ } else
+ result.m_Buffer = inPath;
+ } else if (inPath)
+ result.m_Buffer = inPath;
+
+ if (inPath)
+ result.m_FileName = result.m_Buffer.c_str();
+ return result;
+ }
+
+ static RerouteW createOrNew(const HookContext::ConstPtr& context,
+ const HookCallContext& callContext, LPCWSTR inPath,
+ bool createPath = true,
+ LPSECURITY_ATTRIBUTES securityAttributes = nullptr)
+ {
+ {
+ auto res = create(context, callContext, inPath);
+ if (res.wasRerouted() || !interestingPath(inPath) || !callContext.active() ||
+ pathExists(inPath))
+ return std::move(res);
+ }
+ return createNew(context, callContext, inPath, createPath, securityAttributes);
+ }
+
+ static RerouteW noReroute(LPCWSTR inPath)
+ {
+ RerouteW result;
+ if (inPath)
+ result.m_Buffer = inPath;
+ if (inPath && inPath[0] && !shared::startswith(inPath, L"hid#"))
+ std::replace(result.m_Buffer.begin(), result.m_Buffer.end(), L'/', L'\\');
+ result.m_FileName = result.m_Buffer.c_str();
+ return result;
+ }
+
+private:
+ struct FindCreateTarget
+ {
+ RedirectionTree::NodePtrT target;
+ void operator()(RedirectionTree::NodePtrT node)
+ {
+ if (node->hasFlag(shared::FLAG_CREATETARGET)) {
+ target = node;
+ }
+ }
+ };
+};
+
+class CreateRerouter
+{
+public:
+ bool rerouteCreate(const HookContext::ConstPtr& context,
+ const HookCallContext& callContext, LPCWSTR lpFileName,
+ DWORD& dwCreationDisposition, DWORD dwDesiredAccess,
+ LPSECURITY_ATTRIBUTES lpSecurityAttributes)
+ {
+ enum class Open
+ {
+ existing,
+ create,
+ empty
+ };
+ Open open = Open::existing;
+
+ // Notice since we are calling our patched GetFileAttributesW here this will also
+ // check virtualized paths
+ DWORD virtAttr = GetFileAttributesW(lpFileName);
+ bool isFile = virtAttr != INVALID_FILE_ATTRIBUTES &&
+ (virtAttr & FILE_ATTRIBUTE_DIRECTORY) == 0;
+ m_isDir =
+ virtAttr != INVALID_FILE_ATTRIBUTES && (virtAttr & FILE_ATTRIBUTE_DIRECTORY);
+
+ switch (dwCreationDisposition) {
+ case CREATE_ALWAYS:
+ open = Open::create;
+ if (isFile || m_isDir) {
+ m_error = ERROR_ALREADY_EXISTS;
+ }
+ break;
+
+ case CREATE_NEW:
+ if (isFile || m_isDir) {
+ m_error = ERROR_FILE_EXISTS;
+ return false;
+ } else {
+ open = Open::create;
+ }
+ break;
+
+ case OPEN_ALWAYS:
+ if (isFile || m_isDir) {
+ m_error = ERROR_ALREADY_EXISTS;
+ } else {
+ open = Open::create;
+ }
+ break;
+
+ case TRUNCATE_EXISTING:
+ if ((dwDesiredAccess & GENERIC_WRITE) == 0) {
+ m_error = ERROR_INVALID_PARAMETER;
+ return false;
+ }
+ if (isFile || m_isDir)
+ open = Open::empty;
+ break;
+ }
+
+ if (m_isDir && pathIsDirectory(lpFileName))
+ m_reroute = RerouteW::noReroute(lpFileName);
+ else
+ m_reroute = RerouteW::create(context, callContext, lpFileName);
+
+ if (m_reroute.wasRerouted() && open == Open::create &&
+ pathIsDirectory(m_reroute.fileName()))
+ m_reroute = RerouteW::createNew(context, callContext, lpFileName, true,
+ lpSecurityAttributes);
+
+ if (!m_isDir && !isFile && !m_reroute.wasRerouted() &&
+ (open == Open::create || open == Open::empty)) {
+ m_reroute = RerouteW::createNew(context, callContext, lpFileName, true,
+ lpSecurityAttributes);
+
+ bool newFile =
+ !m_reroute.wasRerouted() && pathDirectlyAvailable(m_reroute.fileName());
+ if (newFile && open == Open::empty)
+ // TRUNCATE_EXISTING will fail since the new file doesn't exist, so change
+ // disposition:
+ dwCreationDisposition = CREATE_ALWAYS;
+ }
+
+ return true;
+ }
+
+ // rerouteNew is used for rerouting the destination of copy/move operations. Assumes
+ // that the call will be skipped if false is returned.
+ bool rerouteNew(const HookContext::ConstPtr& context, HookCallContext& callContext,
+ LPCWSTR lpFileName, bool replaceExisting, const char* hookName)
+ {
+ DWORD disposition = replaceExisting ? CREATE_ALWAYS : CREATE_NEW;
+ if (!rerouteCreate(context, callContext, lpFileName, disposition, GENERIC_WRITE,
+ nullptr)) {
+ spdlog::get("hooks")->info(
+ "{} guaranteed failure, skipping original call: {}, replaceExisting={}, "
+ "error={}",
+ hookName,
+ shared::string_cast<std::string>(lpFileName, shared::CodePage::UTF8),
+ replaceExisting ? "true" : "false", error());
+
+ callContext.updateLastError(error());
+ return false;
+ }
+ return true;
+ }
+
+ void updateResult(HookCallContext& callContext, bool success)
+ {
+ m_originalError = callContext.lastError();
+ if (success) {
+ // m_error != ERROR_SUCCESS means we are overriding the error on success
+ if (m_error == ERROR_SUCCESS)
+ m_error = m_originalError;
+ } else if (m_originalError == ERROR_PATH_NOT_FOUND && m_directlyAvailable)
+ m_error = ERROR_FILE_NOT_FOUND;
+ else
+ m_error = m_originalError;
+ if (m_error != m_originalError)
+ callContext.updateLastError(m_error);
+ }
+
+ DWORD error() const { return m_error; }
+ DWORD originalError() const { return m_originalError; }
+ bool changedError() const { return m_error != m_originalError; }
+
+ bool isDir() const { return m_isDir; }
+ bool newReroute() const { return m_reroute.newReroute(); }
+ bool wasRerouted() const { return m_reroute.wasRerouted(); }
+ LPCWSTR fileName() const { return m_reroute.fileName(); }
+
+ void insertMapping(const HookContext::Ptr& context, bool directory = false)
+ {
+ m_reroute.insertMapping(context, directory);
+ }
+
+private:
+ DWORD m_error = ERROR_SUCCESS;
+ DWORD m_originalError = ERROR_SUCCESS;
+ bool m_directlyAvailable = false;
+ bool m_isDir = false;
+ RerouteW m_reroute;
+};
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/pch.cpp b/libs/usvfs/src/usvfs_dll/pch.cpp
new file mode 100644
index 0000000..1d9f38c
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/pch.cpp
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/libs/usvfs/src/usvfs_dll/redirectiontree.cpp b/libs/usvfs/src/usvfs_dll/redirectiontree.cpp
new file mode 100644
index 0000000..e521365
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/redirectiontree.cpp
@@ -0,0 +1,27 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include "redirectiontree.h"
+
+std::ostream& usvfs::operator<<(std::ostream& stream, const RedirectionData& data)
+{
+ stream << data.linkTarget;
+ return stream;
+}
diff --git a/libs/usvfs/src/usvfs_dll/redirectiontree.h b/libs/usvfs/src/usvfs_dll/redirectiontree.h
new file mode 100644
index 0000000..6a2238a
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/redirectiontree.h
@@ -0,0 +1,104 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#pragma once
+
+#include <directory_tree.h>
+
+namespace usvfs
+{
+
+// typedef boost::interprocess::basic_string<char, std::char_traits<char>,
+// shared::CharAllocatorT> StringT;
+
+namespace shared
+{
+ static const TreeFlags FLAG_CREATETARGET = FLAG_FIRSTUSERFLAG + 0x00;
+}
+
+struct RedirectionDataLocal
+{
+
+ RedirectionDataLocal(const char* target) : linkTarget(target) {}
+
+ RedirectionDataLocal(const std::string& target) : linkTarget(target) {}
+
+ std::string linkTarget;
+};
+
+struct RedirectionData
+{
+
+ RedirectionData(const RedirectionData& reference,
+ const shared::VoidAllocatorT& allocator)
+ : linkTarget(reference.linkTarget.c_str(), allocator)
+ {}
+
+ RedirectionData(const RedirectionDataLocal& reference,
+ const shared::VoidAllocatorT& allocator)
+ : linkTarget(reference.linkTarget.c_str(), allocator)
+ {}
+
+ RedirectionData(const char* target, const shared::VoidAllocatorT& allocator)
+ : linkTarget(target, allocator)
+ {}
+
+ shared::StringT linkTarget;
+};
+
+std::ostream& operator<<(std::ostream& stream, const RedirectionData& data);
+
+template <>
+inline void shared::dataAssign<RedirectionData>(RedirectionData& destination,
+ const RedirectionData& source)
+{
+ destination.linkTarget.assign(source.linkTarget.c_str());
+}
+
+template <>
+inline RedirectionData
+shared::createDataEmpty<RedirectionData>(const VoidAllocatorT& allocator)
+{
+ return RedirectionData("", allocator);
+}
+
+template <typename T>
+struct shared::SHMDataCreator<RedirectionData, T>
+{
+ static RedirectionData create(T source, const VoidAllocatorT& allocator)
+ {
+ return RedirectionData(source, allocator);
+ }
+};
+
+template <>
+struct shared::SHMDataCreator<RedirectionData, RedirectionData>
+{
+ static RedirectionData create(const RedirectionData& source,
+ const VoidAllocatorT& allocator)
+ {
+ return RedirectionData(source, allocator);
+ }
+};
+
+using RedirectionTree = shared::DirectoryTree<RedirectionData>;
+using RedirectionTreeContainer = shared::TreeContainer<RedirectionTree>;
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/semaphore.cpp b/libs/usvfs/src/usvfs_dll/semaphore.cpp
new file mode 100644
index 0000000..609b2f5
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/semaphore.cpp
@@ -0,0 +1,57 @@
+#include "semaphore.h"
+#include "exceptionex.h"
+
+RecursiveBenaphore::RecursiveBenaphore() : m_Counter(0), m_OwnerId(0UL), m_Recursion(0)
+{
+ m_Semaphore = ::CreateSemaphore(nullptr, 1, 1, nullptr);
+}
+
+RecursiveBenaphore::~RecursiveBenaphore()
+{
+ ::CloseHandle(m_Semaphore);
+}
+
+void RecursiveBenaphore::wait(DWORD timeout)
+{
+ DWORD tid = ::GetCurrentThreadId();
+
+ if (::_InterlockedIncrement(&m_Counter) > 1) {
+ if (tid != m_OwnerId) {
+ int tries = 3;
+ while (::WaitForSingleObject(m_Semaphore, timeout) != WAIT_OBJECT_0) {
+ HANDLE owner = ::OpenThread(SYNCHRONIZE, FALSE, m_OwnerId);
+ ON_BLOCK_EXIT([owner]() {
+ ::CloseHandle(owner);
+ });
+ if ((tries <= 0) || (::WaitForSingleObject(owner, 0) == WAIT_OBJECT_0)) {
+ // owner has quit without releasing the semaphore!
+ m_Recursion = 0;
+ spdlog::get("usvfs")->error("thread {} never released the mutex", m_OwnerId);
+ break;
+ } else {
+ --tries;
+ }
+ }
+ }
+ }
+ m_OwnerId = tid;
+ ++m_Recursion;
+}
+
+void RecursiveBenaphore::signal()
+{
+ if (m_Recursion == 0) {
+ return;
+ }
+ // no validation the signaling thread is the one owning the lock
+ DWORD recursion = --m_Recursion;
+ if (recursion == 0) {
+ m_OwnerId = 0;
+ }
+ DWORD result = ::_InterlockedDecrement(&m_Counter);
+ if (result > 0) {
+ if (recursion == 0) {
+ ::ReleaseSemaphore(m_Semaphore, 1, nullptr);
+ }
+ }
+}
diff --git a/libs/usvfs/src/usvfs_dll/semaphore.h b/libs/usvfs/src/usvfs_dll/semaphore.h
new file mode 100644
index 0000000..1048c47
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/semaphore.h
@@ -0,0 +1,27 @@
+#pragma once
+
+// based on code by Jeff Preshing
+
+// this is a synchronization class that prefers
+// undefined behaviour over deadlock. It's utterly broken
+// and needs to be replaced in time.
+
+class RecursiveBenaphore
+{
+
+public:
+ RecursiveBenaphore();
+ ~RecursiveBenaphore();
+
+ // wait on the semaphore. after timeout this will check if the current owner
+ // thread is still alive and steal the semaphore if it isn't. Otherwise this
+ // will continue to wait.
+ void wait(DWORD timeout = INFINITE);
+ void signal();
+
+private:
+ LONG m_Counter;
+ DWORD m_OwnerId;
+ int m_Recursion;
+ HANDLE m_Semaphore;
+};
diff --git a/libs/usvfs/src/usvfs_dll/sharedparameters.cpp b/libs/usvfs/src/usvfs_dll/sharedparameters.cpp
new file mode 100644
index 0000000..95cd0b2
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/sharedparameters.cpp
@@ -0,0 +1,264 @@
+#include <logging.h>
+#include <sharedparameters.h>
+#include <usvfsparametersprivate.h>
+
+namespace usvfs
+{
+
+ForcedLibrary::ForcedLibrary(const std::string& process, const std::string& path,
+ const shared::VoidAllocatorT& alloc)
+ : m_processName(process.begin(), process.end(), alloc),
+ m_libraryPath(path.begin(), path.end(), alloc)
+{}
+
+std::string ForcedLibrary::processName() const
+{
+ return {m_processName.begin(), m_processName.end()};
+}
+
+std::string ForcedLibrary::libraryPath() const
+{
+ return {m_libraryPath.begin(), m_libraryPath.end()};
+}
+
+SharedParameters::SharedParameters(const usvfsParameters& reference,
+ const shared::VoidAllocatorT& allocator)
+ : m_instanceName(reference.instanceName, allocator),
+ m_currentSHMName(reference.currentSHMName, allocator),
+ m_currentInverseSHMName(reference.currentInverseSHMName, allocator),
+ m_debugMode(reference.debugMode), m_logLevel(reference.logLevel),
+ m_crashDumpsType(reference.crashDumpsType),
+ m_crashDumpsPath(reference.crashDumpsPath, allocator),
+ m_delayProcess(reference.delayProcessMs), m_userCount(1),
+ m_processBlacklist(allocator), m_processList(allocator),
+ m_fileSuffixSkipList(allocator), m_directorySkipList(allocator),
+ m_forcedLibraries(allocator)
+{}
+
+usvfsParameters SharedParameters::makeLocal() const
+{
+ bi::scoped_lock lock(m_mutex);
+
+ return usvfsParameters(m_instanceName.c_str(), m_currentSHMName.c_str(),
+ m_currentInverseSHMName.c_str(), m_debugMode, m_logLevel,
+ m_crashDumpsType, m_crashDumpsPath.c_str(),
+ m_delayProcess.count());
+}
+
+std::string SharedParameters::instanceName() const
+{
+ bi::scoped_lock lock(m_mutex);
+ return {m_instanceName.begin(), m_instanceName.end()};
+}
+
+std::string SharedParameters::currentSHMName() const
+{
+ bi::scoped_lock lock(m_mutex);
+ return {m_currentSHMName.begin(), m_currentSHMName.end()};
+}
+
+std::string SharedParameters::currentInverseSHMName() const
+{
+ bi::scoped_lock lock(m_mutex);
+ return {m_currentInverseSHMName.begin(), m_currentInverseSHMName.end()};
+}
+
+void SharedParameters::setSHMNames(const std::string& current,
+ const std::string& inverse)
+{
+ bi::scoped_lock lock(m_mutex);
+
+ m_currentSHMName.assign(current.begin(), current.end());
+ m_currentInverseSHMName.assign(inverse.begin(), inverse.end());
+}
+
+void SharedParameters::setDebugParameters(LogLevel level, CrashDumpsType dumpType,
+ const std::string& dumpPath,
+ std::chrono::milliseconds delayProcess)
+{
+ bi::scoped_lock lock(m_mutex);
+
+ m_logLevel = level;
+ m_crashDumpsType = dumpType;
+ m_crashDumpsPath.assign(dumpPath.begin(), dumpPath.end());
+ m_delayProcess = delayProcess;
+}
+
+std::size_t SharedParameters::userConnected()
+{
+ bi::scoped_lock lock(m_mutex);
+ return ++m_userCount;
+}
+
+std::size_t SharedParameters::userDisconnected()
+{
+ bi::scoped_lock lock(m_mutex);
+ return --m_userCount;
+}
+
+std::size_t SharedParameters::userCount()
+{
+ bi::scoped_lock lock(m_mutex);
+ return m_userCount;
+}
+
+std::size_t SharedParameters::registeredProcessCount() const
+{
+ bi::scoped_lock lock(m_mutex);
+ return m_processList.size();
+}
+
+std::vector<DWORD> SharedParameters::registeredProcesses() const
+{
+ bi::scoped_lock lock(m_mutex);
+ return {m_processList.begin(), m_processList.end()};
+}
+
+void SharedParameters::registerProcess(DWORD pid)
+{
+ bi::scoped_lock lock(m_mutex);
+ m_processList.insert(pid);
+}
+
+void SharedParameters::unregisterProcess(DWORD pid)
+{
+ {
+ bi::scoped_lock lock(m_mutex);
+
+ auto itor = m_processList.find(pid);
+
+ if (itor != m_processList.end()) {
+ m_processList.erase(itor);
+ return;
+ }
+ }
+
+ spdlog::get("usvfs")->error("cannot unregister process {}, not in list", pid);
+}
+
+void SharedParameters::blacklistExecutable(const std::string& name)
+{
+ bi::scoped_lock lock(m_mutex);
+
+ m_processBlacklist.insert(
+ shared::StringT(name.begin(), name.end(), m_processBlacklist.get_allocator()));
+}
+
+void SharedParameters::clearExecutableBlacklist()
+{
+ bi::scoped_lock lock(m_mutex);
+ m_processBlacklist.clear();
+}
+
+bool SharedParameters::executableBlacklisted(const std::string& appName,
+ const std::string& cmdLine) const
+{
+ bool blacklisted = false;
+ std::string log;
+
+ {
+ bi::scoped_lock lock(m_mutex);
+
+ for (const shared::StringT& sitem : m_processBlacklist) {
+ const auto item = "\\" + std::string(sitem.begin(), sitem.end());
+
+ if (!appName.empty()) {
+ if (boost::algorithm::iends_with(appName, item)) {
+ blacklisted = true;
+ log = std::format("application {} is blacklisted", appName);
+ break;
+ }
+ }
+
+ if (!cmdLine.empty()) {
+ if (boost::algorithm::icontains(cmdLine, item)) {
+ blacklisted = true;
+ log = std::format("command line {} is blacklisted", cmdLine);
+ break;
+ }
+ }
+ }
+ }
+
+ if (blacklisted) {
+ spdlog::get("usvfs")->info(log);
+ return true;
+ }
+
+ return false;
+}
+
+void SharedParameters::addSkipFileSuffix(const std::string& fileSuffix)
+{
+ bi::scoped_lock lock(m_mutex);
+
+ m_fileSuffixSkipList.insert(shared::StringT(fileSuffix.begin(), fileSuffix.end(),
+ m_fileSuffixSkipList.get_allocator()));
+}
+
+void SharedParameters::clearSkipFileSuffixes()
+{
+ bi::scoped_lock lock(m_mutex);
+ m_fileSuffixSkipList.clear();
+}
+
+std::vector<std::string> SharedParameters::skipFileSuffixes() const
+{
+ bi::scoped_lock lock(m_mutex);
+ return {m_fileSuffixSkipList.begin(), m_fileSuffixSkipList.end()};
+}
+
+void SharedParameters::addSkipDirectory(const std::string& directory)
+{
+ bi::scoped_lock lock(m_mutex);
+
+ m_directorySkipList.insert(shared::StringT(directory.begin(), directory.end(),
+ m_directorySkipList.get_allocator()));
+}
+
+void SharedParameters::clearSkipDirectories()
+{
+ bi::scoped_lock lock(m_mutex);
+ m_directorySkipList.clear();
+}
+
+std::vector<std::string> SharedParameters::skipDirectories() const
+{
+ bi::scoped_lock lock(m_mutex);
+ return {m_directorySkipList.begin(), m_directorySkipList.end()};
+}
+
+void SharedParameters::addForcedLibrary(const std::string& processName,
+ const std::string& libraryPath)
+{
+ bi::scoped_lock lock(m_mutex);
+
+ m_forcedLibraries.push_front(
+ ForcedLibrary(processName, libraryPath, m_forcedLibraries.get_allocator()));
+}
+
+std::vector<std::string>
+SharedParameters::forcedLibraries(const std::string& processName)
+{
+ std::vector<std::string> v;
+
+ {
+ bi::scoped_lock lock(m_mutex);
+
+ for (const auto& lib : m_forcedLibraries) {
+ if (boost::algorithm::iequals(processName, lib.processName())) {
+ v.push_back(lib.libraryPath());
+ }
+ }
+ }
+
+ return v;
+}
+
+void SharedParameters::clearForcedLibraries()
+{
+ bi::scoped_lock lock(m_mutex);
+ m_forcedLibraries.clear();
+}
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/stringcast_boost.h b/libs/usvfs/src/usvfs_dll/stringcast_boost.h
new file mode 100644
index 0000000..d2544ec
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/stringcast_boost.h
@@ -0,0 +1,42 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stringcast.h>
+
+namespace usvfs
+{
+namespace shared
+{
+
+ template <typename ToT, typename CharT, typename Traits, typename Allocator>
+ class string_cast_impl<ToT, boost::container::basic_string<CharT, Traits, Allocator>>
+ {
+ public:
+ static ToT
+ cast(const boost::container::basic_string<CharT, Traits, Allocator>& source,
+ CodePage codePage, size_t sourceLength)
+ {
+ return string_cast_impl<ToT, const CharT*>::cast(source.c_str(), codePage,
+ sourceLength);
+ }
+ };
+
+} // namespace shared
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_dll/usvfs.cpp b/libs/usvfs/src/usvfs_dll/usvfs.cpp
new file mode 100644
index 0000000..56d9f71
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/usvfs.cpp
@@ -0,0 +1,965 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This file is part of usvfs.
+
+usvfs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+usvfs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with usvfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include "usvfs.h"
+#include "hookmanager.h"
+#include "loghelpers.h"
+#include "redirectiontree.h"
+#include "usvfs_version.h"
+#include "usvfsparametersprivate.h"
+#include <inject.h>
+#include <shmlogger.h>
+#include <spdlog/sinks/null_sink.h>
+#include <spdlog/sinks/stdout_sinks.h>
+#include <stringcast.h>
+#include <ttrampolinepool.h>
+#include <winapi.h>
+
+// note that there's a mix of boost and std filesystem stuff in this file and
+// that they're not completely compatible
+#include <filesystem>
+
+namespace bfs = boost::filesystem;
+namespace ush = usvfs::shared;
+namespace bip = boost::interprocess;
+namespace ba = boost::algorithm;
+
+using usvfs::log::ConvertLogLevel;
+
+usvfs::HookManager* manager = nullptr;
+usvfs::HookContext* context = nullptr;
+HMODULE dllModule = nullptr;
+PVOID exceptionHandler = nullptr;
+CrashDumpsType usvfs_dump_type = CrashDumpsType::None;
+std::wstring usvfs_dump_path;
+
+// this is called for every single file, so it's a bit long winded, but it's
+// as fast as it gets, probably
+//
+template <std::size_t LongestExtension, std::size_t ExtensionsCount>
+bool extensionMatchesCI(
+ std::string_view name,
+ const std::array<std::string_view, ExtensionsCount>& extensionsLC,
+ const std::array<std::string_view, ExtensionsCount>& extensionsUC)
+{
+ constexpr std::size_t longestExtensionWithDot = LongestExtension + 1;
+
+ // quick check
+ if (name.size() < longestExtensionWithDot) {
+ return false;
+ }
+
+ // for each extension
+ for (std::size_t i = 0; i < ExtensionsCount; ++i) {
+ const std::size_t extensionLength = extensionsLC[i].size();
+ const std::size_t extensionLengthWithDot = extensionLength + 1;
+
+ // check size
+ if (name.size() < extensionLengthWithDot) {
+ continue;
+ }
+
+ // check dot
+ if (name[name.size() - extensionLengthWithDot] != '.') {
+ continue;
+ }
+
+ // starts at one past the dot
+ const auto* p = name.data() + name.size() - extensionLength;
+
+ // set to false as soon as a character doesn't match
+ bool found = true;
+
+ // for each character in extension
+ for (std::size_t c = 0; c < extensionLength; ++c) {
+ // checking both lowercase and uppercase
+ if (*p != extensionsLC[i][c] && *p != extensionsUC[i][c]) {
+ // neither
+ found = false;
+ break;
+ }
+
+ // matches, check next
+ ++p;
+ }
+
+ if (found) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool shouldAddToInverseTree(std::string_view name)
+{
+ static std::array<std::string_view, 3> extensionsLC{"exe", "dll"};
+ static std::array<std::string_view, 3> extensionsUC{"EXE", "DLL"};
+
+ // must be changed if any extension longer than 3 letters is added
+ constexpr std::size_t longestExtension = 3;
+
+ return extensionMatchesCI<longestExtension>(name, extensionsLC, extensionsUC);
+}
+
+//
+// Logging
+//
+
+void InitLoggingInternal(bool toConsole, bool connectExistingSHM)
+{
+ try {
+ if (!toConsole && !SHMLogger::isInstantiated()) {
+ if (connectExistingSHM) {
+ SHMLogger::open("usvfs");
+ } else {
+ SHMLogger::create("usvfs");
+ }
+ }
+
+ // a temporary logger was created in DllMain
+ spdlog::drop("usvfs");
+#pragma message("need a customized name for the shm")
+ auto logger = spdlog::get("usvfs");
+ if (logger.get() == nullptr) {
+ logger = toConsole ? spdlog::create<spdlog::sinks::stdout_sink_mt>("usvfs")
+ : spdlog::create<usvfs::sinks::shm_sink>("usvfs", "usvfs");
+ logger->set_pattern("%H:%M:%S.%e [%L] %v");
+ }
+ logger->set_level(spdlog::level::debug);
+
+ spdlog::drop("hooks");
+ logger = spdlog::get("hooks");
+ if (logger.get() == nullptr) {
+ logger = toConsole ? spdlog::create<spdlog::sinks::stdout_sink_mt>("hooks")
+ : spdlog::create<usvfs::sinks::shm_sink>("hooks", "usvfs");
+ logger->set_pattern("%H:%M:%S.%e <%P:%t> [%L] %v");
+ }
+ logger->set_level(spdlog::level::debug);
+ } catch (const std::exception&) {
+ // TODO should really report this
+ // OutputDebugStringA((boost::format("init exception: %1%\n") %
+ // e.what()).str().c_str());
+ if (spdlog::get("usvfs").get() == nullptr) {
+ spdlog::create<spdlog::sinks::null_sink_mt>("usvfs");
+ }
+ if (spdlog::get("hooks").get() == nullptr) {
+ spdlog::create<spdlog::sinks::null_sink_mt>("hooks");
+ }
+ }
+
+ spdlog::get("usvfs")->info("usvfs dll {} initialized in process {}",
+ USVFS_VERSION_STRING, GetCurrentProcessId());
+}
+
+void WINAPI usvfsInitLogging(bool toConsole)
+{
+ InitLoggingInternal(toConsole, false);
+}
+
+extern "C" DLLEXPORT bool WINAPI usvfsGetLogMessages(LPSTR buffer, size_t size,
+ bool blocking)
+{
+ buffer[0] = '\0';
+ try {
+ if (blocking) {
+ SHMLogger::instance().get(buffer, size);
+ return true;
+ } else {
+ return SHMLogger::instance().tryGet(buffer, size);
+ }
+ } catch (const std::exception& e) {
+ _snprintf_s(buffer, size, _TRUNCATE, "Failed to retrieve log messages: %s",
+ e.what());
+ return false;
+ }
+}
+
+void SetLogLevel(LogLevel level)
+{
+ spdlog::get("usvfs")->set_level(ConvertLogLevel(level));
+ spdlog::get("hooks")->set_level(ConvertLogLevel(level));
+}
+
+void WINAPI usvfsUpdateParameters(usvfsParameters* p)
+{
+ spdlog::get("usvfs")->info("updating parameters:\n"
+ " . debugMode: {}\n"
+ " . log level: {}\n"
+ " . dump type: {}\n"
+ " . dump path: {}\n"
+ " . delay process: {}ms",
+ p->debugMode, usvfsLogLevelToString(p->logLevel),
+ usvfsCrashDumpTypeToString(p->crashDumpsType),
+ p->crashDumpsPath, p->delayProcessMs);
+
+ // update actual values used:
+ usvfs_dump_type = p->crashDumpsType;
+ usvfs_dump_path =
+ ush::string_cast<std::wstring>(p->crashDumpsPath, ush::CodePage::UTF8);
+ SetLogLevel(p->logLevel);
+
+ // update parameters in context so spawned process will inherit changes:
+ context->setDebugParameters(p->logLevel, p->crashDumpsType, p->crashDumpsPath,
+ std::chrono::milliseconds(p->delayProcessMs));
+}
+
+//
+// Structured Exception handling
+//
+
+std::wstring generate_minidump_name(const wchar_t* dumpPath)
+{
+ DWORD pid = GetCurrentProcessId();
+ wchar_t pname[100];
+ if (GetModuleBaseName(GetCurrentProcess(), NULL, pname, _countof(pname)) == 0)
+ return std::wstring();
+
+ // find an available name:
+ wchar_t dmpFile[MAX_PATH];
+ int count = 0;
+ _snwprintf_s(dmpFile, _TRUNCATE, L"%s\\%s-%lu.dmp", dumpPath, pname, pid);
+ while (winapi::ex::wide::fileExists(dmpFile)) {
+ if (++count > 99)
+ return std::wstring();
+ _snwprintf_s(dmpFile, _TRUNCATE, L"%s\\%s-%lu_%02d.dmp", dumpPath, pname, pid,
+ count);
+ }
+ return dmpFile;
+}
+
+int createMiniDumpImpl(PEXCEPTION_POINTERS exceptionPtrs, CrashDumpsType type,
+ const wchar_t* dumpPath, HMODULE dbgDLL)
+{
+ typedef BOOL(WINAPI * FuncMiniDumpWriteDump)(
+ HANDLE process, DWORD pid, HANDLE file, MINIDUMP_TYPE dumpType,
+ const PMINIDUMP_EXCEPTION_INFORMATION exceptionParam,
+ const PMINIDUMP_USER_STREAM_INFORMATION userStreamParam,
+ const PMINIDUMP_CALLBACK_INFORMATION callbackParam);
+
+ // notice we avoid logging here on purpose because this is called from the VEHandler
+ // and the logger can crash it in extreme cases.
+ // additionally it is also called for MO crashes which use it's own logging.
+ winapi::ex::wide::createPath(dumpPath);
+
+ auto dmpName = generate_minidump_name(dumpPath);
+ if (dmpName.empty())
+ return 4;
+
+ FuncMiniDumpWriteDump funcDump = reinterpret_cast<FuncMiniDumpWriteDump>(
+ GetProcAddress(dbgDLL, "MiniDumpWriteDump"));
+ if (!funcDump)
+ return 5;
+
+ HANDLE dumpFile = winapi::wide::createFile(dmpName)
+ .createAlways()
+ .access(GENERIC_WRITE)
+ .share(FILE_SHARE_WRITE)();
+ if (dumpFile != INVALID_HANDLE_VALUE) {
+ DWORD dumpType = MiniDumpNormal | MiniDumpWithHandleData |
+ MiniDumpWithUnloadedModules | MiniDumpWithProcessThreadData;
+ if (type == CrashDumpsType::Data)
+ dumpType |= MiniDumpWithDataSegs;
+ if (type == CrashDumpsType::Full)
+ dumpType |= MiniDumpWithFullMemory;
+
+ _MINIDUMP_EXCEPTION_INFORMATION exceptionInfo;
+ exceptionInfo.ThreadId = GetCurrentThreadId();
+ exceptionInfo.ExceptionPointers = exceptionPtrs;
+ exceptionInfo.ClientPointers = FALSE;
+
+ BOOL success = funcDump(GetCurrentProcess(), GetCurrentProcessId(), dumpFile,
+ static_cast<MINIDUMP_TYPE>(dumpType), &exceptionInfo,
+ nullptr, nullptr);
+
+ CloseHandle(dumpFile);
+
+ return success ? 0 : 7;
+ } else
+ return 6;
+}
+
+int WINAPI usvfsCreateMiniDump(PEXCEPTION_POINTERS exceptionPtrs, CrashDumpsType type,
+ const wchar_t* dumpPath)
+{
+ if (type == CrashDumpsType::None)
+ return 0;
+
+ int res = 1;
+ if (HMODULE dbgDLL = LoadLibraryW(L"dbghelp.dll")) {
+ try {
+ res = createMiniDumpImpl(exceptionPtrs, type, dumpPath, dbgDLL);
+ } catch (...) {
+ res = 2;
+ }
+ FreeLibrary(dbgDLL);
+ }
+ return res;
+}
+
+static bool exceptionInUSVFS(PEXCEPTION_POINTERS exceptionPtrs)
+{
+ if (!dllModule) // shouldn't happen, check just in case
+ return true; // create dump to better understand how this could happen
+
+ std::pair<uintptr_t, uintptr_t> range = winapi::ex::getSectionRange(dllModule);
+
+ uintptr_t exceptionAddress =
+ reinterpret_cast<uintptr_t>(exceptionPtrs->ExceptionRecord->ExceptionAddress);
+
+ return range.first <= exceptionAddress && exceptionAddress < range.second;
+}
+
+LONG WINAPI VEHandler(PEXCEPTION_POINTERS exceptionPtrs)
+{
+ // NOTICE: don't use logger in VEHandler as it can cause another fault causing
+ // VEHandler to be called again and so on.
+
+ if ((exceptionPtrs->ExceptionRecord->ExceptionCode < 0x80000000) // non-critical
+ ||
+ (exceptionPtrs->ExceptionRecord->ExceptionCode == 0xe06d7363)) { // cpp exception
+ // don't report non-critical exceptions
+ return EXCEPTION_CONTINUE_SEARCH;
+ }
+ /*
+ if (((exceptionPtrs->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) != 0)
+ || (exceptionPtrs->ExceptionRecord->ExceptionCode == 0xe06d7363)) {
+ // don't want to break on non-critical exceptions. 0xe06d7363 indicates a C++
+ exception. why are those marked non-continuable? return EXCEPTION_CONTINUE_SEARCH;
+ }
+ */
+
+ // VEHandler is called on "first-chance" exceptions which might be caught and handled.
+ // Ideally we would like to use an UnhandledExceptionFilter but that fails to catch
+ // crashes inside our hooks at least on x64, which is the main reason why want a crash
+ // collection from usvfs. As a workaround/compromise we catch vectored exception but
+ // only ones that originate directly within the usvfs code:
+ if (!exceptionInUSVFS(exceptionPtrs))
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ // disable our hooking mechanism to increase chances the dump writing won't crash
+ HookLib::TrampolinePool& trampPool = HookLib::TrampolinePool::instance();
+ if (&trampPool) { // need to test this in case of crash before TrampolinePool
+ // initialized
+ trampPool.forceUnlockBarrier();
+ trampPool.setBlock(true);
+ }
+
+ usvfsCreateMiniDump(exceptionPtrs, usvfs_dump_type, usvfs_dump_path.c_str());
+
+ return EXCEPTION_CONTINUE_SEARCH;
+}
+
+//
+// Exported functions
+//
+
+void __cdecl InitHooks(LPVOID parameters, size_t)
+{
+ InitLoggingInternal(false, true);
+
+ const usvfsParameters* params = reinterpret_cast<usvfsParameters*>(parameters);
+
+ // there is already a wait in the constructor of HookManager, but this one is useful
+ // to debug code here (from experience... ), should not wait twice since the second
+ // will return true immediately
+ if (params->debugMode) {
+ while (!::IsDebuggerPresent()) {
+ // wait for debugger to attach
+ ::Sleep(100);
+ }
+ }
+
+ usvfs_dump_type = params->crashDumpsType;
+ usvfs_dump_path =
+ ush::string_cast<std::wstring>(params->crashDumpsPath, ush::CodePage::UTF8);
+
+ if (params->delayProcessMs > 0) {
+ ::Sleep(static_cast<unsigned long>(params->delayProcessMs));
+ }
+
+ SetLogLevel(params->logLevel);
+
+ if (exceptionHandler == nullptr) {
+ if (usvfs_dump_type != CrashDumpsType::None)
+ exceptionHandler = ::AddVectoredExceptionHandler(0, VEHandler);
+ } else {
+ spdlog::get("usvfs")->info("vectored exception handler already active");
+ // how did this happen??
+ }
+
+ spdlog::get("usvfs")->info(
+ "inithooks called {0} in process {1}:{2} (log level {3}, dump type {4}, dump "
+ "path {5})",
+ params->instanceName, winapi::ansi::getModuleFileName(nullptr),
+ ::GetCurrentProcessId(), static_cast<int>(params->logLevel),
+ static_cast<int>(params->crashDumpsType), params->crashDumpsPath);
+
+ try {
+ manager = new usvfs::HookManager(*params, dllModule);
+
+ auto context = manager->context();
+ auto exePath = boost::dll::program_location();
+ auto libraries = context->librariesToForceLoad(exePath.filename().c_str());
+ for (auto library : libraries) {
+ if (std::filesystem::exists(library)) {
+ const auto ret = LoadLibraryExW(library.c_str(), NULL, 0);
+ if (ret) {
+ spdlog::get("usvfs")->info("inithooks succeeded to force load {0}",
+ ush::string_cast<std::string>(library).c_str());
+ } else {
+ spdlog::get("usvfs")->critical(
+ "inithooks failed to force load {0}",
+ ush::string_cast<std::string>(library).c_str());
+ }
+ }
+ }
+
+ spdlog::get("usvfs")->info("inithooks in process {0} successful",
+ ::GetCurrentProcessId());
+
+ } catch (const std::exception& e) {
+ spdlog::get("usvfs")->debug("failed to initialise hooks: {0}", e.what());
+ }
+}
+
+void WINAPI usvfsGetCurrentVFSName(char* buffer, size_t size)
+{
+ ush::strncpy_sz(buffer, context->callParameters().currentSHMName, size);
+}
+
+BOOL WINAPI usvfsCreateVFS(const usvfsParameters* p)
+{
+ usvfs::HookContext::remove(p->instanceName);
+ return usvfsConnectVFS(p);
+}
+
+BOOL WINAPI usvfsConnectVFS(const usvfsParameters* params)
+{
+ if (spdlog::get("usvfs").get() == nullptr) {
+ // create temporary logger so we don't get null-pointer exceptions
+ spdlog::create<spdlog::sinks::null_sink_mt>("usvfs");
+ }
+
+ try {
+ usvfsDisconnectVFS();
+ context = new usvfs::HookContext(*params, dllModule);
+
+ return TRUE;
+ } catch (const std::exception& e) {
+ spdlog::get("usvfs")->debug("failed to connect to vfs: {}", e.what());
+ return FALSE;
+ }
+}
+
+void WINAPI usvfsDisconnectVFS()
+{
+ if (spdlog::get("usvfs").get() == nullptr) {
+ // create temporary logger so we don't get null-pointer exceptions
+ spdlog::create<spdlog::sinks::null_sink_mt>("usvfs");
+ }
+
+ spdlog::get("usvfs")->debug("remove from process {}", GetCurrentProcessId());
+
+ if (manager != nullptr) {
+ delete manager;
+ manager = nullptr;
+ }
+
+ if (context != nullptr) {
+ delete context;
+ context = nullptr;
+ spdlog::get("usvfs")->debug("vfs unloaded");
+ }
+}
+
+bool processStillActive(DWORD pid)
+{
+ HANDLE proc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
+
+ if (proc == nullptr) {
+ return false;
+ }
+
+ ON_BLOCK_EXIT([&]() {
+ if (proc != INVALID_HANDLE_VALUE)
+ ::CloseHandle(proc);
+ });
+
+ DWORD exitCode;
+ if (!GetExitCodeProcess(proc, &exitCode)) {
+ spdlog::get("usvfs")->warn("failed to query exit code on process {}: {}", pid,
+ ::GetLastError());
+ return false;
+ } else {
+ return exitCode == STILL_ACTIVE;
+ }
+}
+
+BOOL WINAPI usvfsGetVFSProcessList(size_t* count, LPDWORD processIDs)
+{
+ if (count == nullptr) {
+ SetLastError(ERROR_INVALID_PARAMETER);
+ return FALSE;
+ }
+
+ if (context == nullptr) {
+ *count = 0;
+ } else {
+ std::vector<DWORD> pids = context->registeredProcesses();
+ size_t realCount = 0;
+ for (DWORD pid : pids) {
+ if (processStillActive(pid)) {
+ if ((realCount < *count) && (processIDs != nullptr)) {
+ processIDs[realCount] = pid;
+ }
+
+ ++realCount;
+ } // else the process has already ended
+ }
+ *count = realCount;
+ }
+ return TRUE;
+}
+
+BOOL WINAPI usvfsGetVFSProcessList2(size_t* count, DWORD** buffer)
+{
+ if (!count || !buffer) {
+ SetLastError(ERROR_INVALID_PARAMETER);
+ return FALSE;
+ }
+
+ *count = 0;
+ *buffer = nullptr;
+
+ std::vector<DWORD> pids = context->registeredProcesses();
+ auto last = std::remove_if(pids.begin(), pids.end(), [](DWORD id) {
+ return !processStillActive(id);
+ });
+
+ pids.erase(last, pids.end());
+
+ if (pids.empty()) {
+ return TRUE;
+ }
+
+ *count = pids.size();
+ *buffer = static_cast<DWORD*>(std::calloc(pids.size(), sizeof(DWORD)));
+
+ std::copy(pids.begin(), pids.end(), *buffer);
+
+ return TRUE;
+}
+
+void WINAPI usvfsClearVirtualMappings()
+{
+ context->redirectionTable()->clear();
+ context->inverseTable()->clear();
+}
+
+/// ensure the specified path exists. If a physical path of the same name
+/// exists, it is inserted into the virtual directory as an empty reference. If
+/// the path doesn't exist virtually and can't be cloned from a physical
+/// directory, this returns false
+/// \todo if this fails (i.e. not all intermediate directories exists) any
+/// intermediate directories already created aren't removed
+bool assertPathExists(usvfs::RedirectionTreeContainer& table, LPCWSTR path)
+{
+ bfs::path p(path);
+ p = p.parent_path();
+
+ usvfs::RedirectionTree::NodeT* current = table.get();
+
+ for (auto iter = p.begin(); iter != p.end(); iter = ush::nextIter(iter, p.end())) {
+ if (current->exists(iter->string().c_str())) {
+ // subdirectory exists virtually, all good
+ usvfs::RedirectionTree::NodePtrT found = current->node(iter->string().c_str());
+ current = found.get().get();
+ } else {
+ // targetPath is relative to the last rerouted "real" path. This means
+ // that if virtual c:/foo maps to real c:/windows then creating virtual
+ // c:/foo/bar will map to real c:/windows/bar
+ bfs::path targetPath = current->data().linkTarget.size() > 0
+ ? bfs::path(current->data().linkTarget.c_str()) / *iter
+ : *iter / "\\";
+
+ // is_directory returns false for symlinks and reparse points,
+ // which causes this function to fail if the target path contains
+ // either of those. paths containing reparse points is a common
+ // scenario when running under Wine, so check for those explicitly.
+ // this check could have a false positive if the path contains a
+ // symlink to a file, but such a scenario is extremely unlikely.
+ if (is_directory(targetPath) || is_symlink(targetPath) ||
+ status(targetPath).type() == bfs::file_type::reparse_file) {
+ usvfs::RedirectionTree::NodePtrT newNode =
+ table.addDirectory(current->path() / *iter, targetPath.string().c_str(),
+ ush::FLAG_DUMMY, false);
+ current = newNode.get().get();
+ } else {
+ spdlog::get("usvfs")->info("{} doesn't exist", targetPath.c_str());
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+static bool fileNameInSkipSuffixes(const std::string& fileNameUtf8,
+ const std::vector<std::string>& skipFileSuffixes)
+{
+ for (const auto& skipFileSuffix : skipFileSuffixes) {
+ if (boost::algorithm::iends_with(fileNameUtf8, skipFileSuffix)) {
+ spdlog::get("usvfs")->debug(
+ "file '{}' should be skipped, matches file suffix '{}'", fileNameUtf8,
+ skipFileSuffix);
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool fileNameInSkipDirectories(const std::string& directoryNameUtf8,
+ const std::vector<std::string>& skipDirectories)
+{
+ for (const auto& skipDir : skipDirectories) {
+ if (boost::algorithm::iequals(directoryNameUtf8, skipDir)) {
+ spdlog::get("usvfs")->debug("directory '{}' should be skipped",
+ directoryNameUtf8);
+ return true;
+ }
+ }
+ return false;
+}
+
+BOOL WINAPI usvfsVirtualLinkFile(LPCWSTR source, LPCWSTR destination,
+ unsigned int flags)
+{
+ // TODO difference between winapi and ntdll api regarding system32 vs syswow64
+ // (and other windows links?)
+ try {
+ if (!assertPathExists(context->redirectionTable(), destination)) {
+ SetLastError(ERROR_PATH_NOT_FOUND);
+ return FALSE;
+ }
+
+ const auto skipFileSuffixes = context->skipFileSuffixes();
+
+ std::string sourceU8 = ush::string_cast<std::string>(source, ush::CodePage::UTF8);
+
+ // Check if the file should be skipped
+ if (fileNameInSkipSuffixes(sourceU8, skipFileSuffixes)) {
+ // return false when we want to fail when the file is skipped
+ return (flags & LINKFLAG_FAILIFSKIPPED) ? FALSE : TRUE;
+ }
+
+ auto res = context->redirectionTable().addFile(
+ bfs::path(destination), usvfs::RedirectionDataLocal(sourceU8),
+ !(flags & LINKFLAG_FAILIFEXISTS));
+
+ if (shouldAddToInverseTree(sourceU8)) {
+ std::string destinationU8 =
+ ush::string_cast<std::string>(destination, ush::CodePage::UTF8);
+
+ context->inverseTable().addFile(bfs::path(source),
+ usvfs::RedirectionDataLocal(destinationU8), true);
+ }
+
+ context->updateParameters();
+
+ if (res.get() == nullptr) {
+ // the tree structure currently doesn't provide useful error codes but
+ // this is currently the only reason
+ // we would return a nullptr.
+ SetLastError(ERROR_FILE_EXISTS);
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+ } catch (const std::exception& e) {
+ spdlog::get("usvfs")->error("failed to copy file {}", e.what());
+ // TODO: no clue what's wrong
+ SetLastError(ERROR_INVALID_DATA);
+ return FALSE;
+ }
+}
+
+/**
+ * @brief extract the flags relevant to redirection
+ */
+static usvfs::shared::TreeFlags convertRedirectionFlags(unsigned int flags)
+{
+ usvfs::shared::TreeFlags result = 0;
+ if (flags & LINKFLAG_CREATETARGET) {
+ result |= usvfs::shared::FLAG_CREATETARGET;
+ }
+ return result;
+}
+
+BOOL WINAPI usvfsVirtualLinkDirectoryStatic(LPCWSTR source, LPCWSTR destination,
+ unsigned int flags)
+{
+ // TODO change notification not yet implemented
+ try {
+ if ((flags & LINKFLAG_FAILIFEXISTS) && winapi::ex::wide::fileExists(destination)) {
+ SetLastError(ERROR_FILE_EXISTS);
+ return FALSE;
+ }
+
+ if (!assertPathExists(context->redirectionTable(), destination)) {
+ SetLastError(ERROR_PATH_NOT_FOUND);
+ return FALSE;
+ }
+
+ std::string sourceU8 =
+ ush::string_cast<std::string>(source, ush::CodePage::UTF8) + "\\";
+
+ context->redirectionTable().addDirectory(
+ destination, usvfs::RedirectionDataLocal(sourceU8),
+ usvfs::shared::FLAG_DIRECTORY | convertRedirectionFlags(flags),
+ (flags & LINKFLAG_CREATETARGET) != 0);
+
+ const auto skipDirectories = context->skipDirectories();
+ const auto skipFileSuffixes = context->skipFileSuffixes();
+
+ if ((flags & LINKFLAG_RECURSIVE) != 0) {
+ std::wstring sourceP(source);
+ std::wstring sourceW = sourceP + L"\\";
+ std::wstring destinationW = std::wstring(destination) + L"\\";
+ if (sourceP.length() >= MAX_PATH && !ush::startswith(sourceP.c_str(), LR"(\\?\)"))
+ sourceP = LR"(\\?\)" + sourceP;
+
+ for (winapi::ex::wide::FileResult file :
+ winapi::ex::wide::quickFindFiles(sourceP.c_str(), L"*")) {
+ if (file.attributes & FILE_ATTRIBUTE_DIRECTORY) {
+ if ((file.fileName != L".") && (file.fileName != L"..")) {
+
+ const auto nameU8 = ush::string_cast<std::string>(file.fileName.c_str(),
+ ush::CodePage::UTF8);
+ // Check if the directory should be skipped
+ if (fileNameInSkipDirectories(nameU8, skipDirectories)) {
+ // Fail if we desire to fail when a dir/file is skipped
+ if (flags & LINKFLAG_FAILIFSKIPPED) {
+ spdlog::get("usvfs")->debug(
+ "directory '{}' skipped, failing as defined by link flags", nameU8);
+ return FALSE;
+ }
+
+ continue;
+ }
+
+ usvfsVirtualLinkDirectoryStatic((sourceW + file.fileName).c_str(),
+ (destinationW + file.fileName).c_str(),
+ flags);
+ }
+ } else {
+ const auto nameU8 =
+ ush::string_cast<std::string>(file.fileName.c_str(), ush::CodePage::UTF8);
+
+ // Check if the file should be skipped
+ if (fileNameInSkipSuffixes(nameU8, skipFileSuffixes)) {
+ // Fail if we desire to fail when a dir/file is skipped
+ if (flags & LINKFLAG_FAILIFSKIPPED) {
+ spdlog::get("usvfs")->debug(
+ "file '{}' skipped, failing as defined by link flags", nameU8);
+ return FALSE;
+ }
+
+ continue;
+ }
+
+ // TODO could save memory here by storing only the file name for the
+ // source and constructing the full name using the parent directory
+ context->redirectionTable().addFile(
+ bfs::path(destination) / nameU8,
+ usvfs::RedirectionDataLocal(sourceU8 + nameU8), true);
+
+ if (shouldAddToInverseTree(nameU8)) {
+ std::string destinationU8 =
+ ush::string_cast<std::string>(destination, ush::CodePage::UTF8) + "\\";
+
+ context->inverseTable().addFile(
+ bfs::path(source) / nameU8,
+ usvfs::RedirectionDataLocal(destinationU8 + nameU8), true);
+ }
+ }
+ }
+ }
+
+ context->updateParameters();
+
+ return TRUE;
+ } catch (const std::exception& e) {
+ spdlog::get("usvfs")->error("failed to copy file {}", e.what());
+ // TODO: no clue what's wrong
+ SetLastError(ERROR_INVALID_DATA);
+ return FALSE;
+ }
+}
+
+BOOL WINAPI usvfsCreateProcessHooked(LPCWSTR lpApplicationName, LPWSTR lpCommandLine,
+ LPSECURITY_ATTRIBUTES lpProcessAttributes,
+ LPSECURITY_ATTRIBUTES lpThreadAttributes,
+ BOOL bInheritHandles, DWORD dwCreationFlags,
+ LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory,
+ LPSTARTUPINFOW lpStartupInfo,
+ LPPROCESS_INFORMATION lpProcessInformation)
+{
+ BOOL susp = dwCreationFlags & CREATE_SUSPENDED;
+ DWORD flags = dwCreationFlags | CREATE_SUSPENDED;
+
+ BOOL blacklisted = context->executableBlacklisted(lpApplicationName, lpCommandLine);
+
+ BOOL res = CreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes,
+ lpThreadAttributes, bInheritHandles, flags, lpEnvironment,
+ lpCurrentDirectory, lpStartupInfo, lpProcessInformation);
+ if (!res) {
+ spdlog::get("usvfs")->error("failed to spawn {}",
+ ush::string_cast<std::string>(lpCommandLine));
+ return FALSE;
+ }
+
+ if (!blacklisted) {
+ std::wstring applicationDirPath = winapi::wide::getModuleFileName(dllModule);
+ boost::filesystem::path p(applicationDirPath);
+ try {
+ usvfs::injectProcess(p.parent_path().wstring(), context->callParameters(),
+ *lpProcessInformation);
+ } catch (const std::exception& e) {
+ spdlog::get("usvfs")->error("failed to inject: {}", e.what());
+ logExtInfo(e, LogLevel::Error);
+ ::TerminateProcess(lpProcessInformation->hProcess, 1);
+ ::SetLastError(ERROR_INVALID_PARAMETER);
+ return FALSE;
+ }
+ }
+
+ if (!susp) {
+ ResumeThread(lpProcessInformation->hThread);
+ }
+
+ return TRUE;
+}
+
+BOOL WINAPI usvfsCreateVFSDump(LPSTR buffer, size_t* size)
+{
+ assert(size != nullptr);
+ std::ostringstream output;
+ usvfs::shared::dumpTree(output, *context->redirectionTable().get());
+ std::string str = output.str();
+ if ((buffer != NULL) && (*size > 0)) {
+ strncpy_s(buffer, *size, str.c_str(), _TRUNCATE);
+ }
+ bool success = *size >= str.length();
+ *size = str.length();
+ return success ? TRUE : FALSE;
+}
+
+VOID WINAPI usvfsBlacklistExecutable(LPCWSTR executableName)
+{
+ context->blacklistExecutable(executableName);
+}
+
+VOID WINAPI usvfsClearExecutableBlacklist()
+{
+ context->clearExecutableBlacklist();
+}
+
+VOID WINAPI usvfsAddSkipFileSuffix(LPCWSTR fileSuffix)
+{
+ context->addSkipFileSuffix(fileSuffix);
+}
+
+VOID WINAPI usvfsClearSkipFileSuffixes()
+{
+ context->clearSkipFileSuffixes();
+}
+
+VOID WINAPI usvfsAddSkipDirectory(LPCWSTR directory)
+{
+ context->addSkipDirectory(directory);
+}
+
+VOID WINAPI usvfsClearSkipDirectories()
+{
+ context->clearSkipDirectories();
+}
+
+VOID WINAPI usvfsForceLoadLibrary(LPCWSTR processName, LPCWSTR libraryPath)
+{
+ context->forceLoadLibrary(processName, libraryPath);
+}
+
+VOID WINAPI usvfsClearLibraryForceLoads()
+{
+ context->clearLibraryForceLoads();
+}
+
+VOID WINAPI usvfsPrintDebugInfo()
+{
+ spdlog::get("usvfs")->warn("===== debug {} =====",
+ context->redirectionTable().shmName());
+ void* buffer = nullptr;
+ size_t bufferSize = 0;
+ context->redirectionTable().getBuffer(buffer, bufferSize);
+ std::ostringstream temp;
+ for (size_t i = 0; i < bufferSize; ++i) {
+ temp << std::hex << std::setfill('0') << std::setw(2)
+ << (unsigned)reinterpret_cast<char*>(buffer)[i] << " ";
+ if ((i % 16) == 15) {
+ spdlog::get("usvfs")->info("{}", temp.str());
+ temp.str("");
+ temp.clear();
+ }
+ }
+ if (!temp.str().empty()) {
+ spdlog::get("usvfs")->info("{}", temp.str());
+ }
+ spdlog::get("usvfs")->warn("===== / debug {} =====",
+ context->redirectionTable().shmName());
+}
+
+const char* WINAPI usvfsVersionString()
+{
+ return USVFS_VERSION_STRING;
+}
+
+//
+// DllMain
+//
+
+BOOL APIENTRY DllMain(HMODULE module, DWORD reasonForCall, LPVOID)
+{
+ switch (reasonForCall) {
+ case DLL_PROCESS_ATTACH: {
+ dllModule = module;
+ } break;
+ case DLL_PROCESS_DETACH: {
+ if (exceptionHandler)
+ ::RemoveVectoredExceptionHandler(exceptionHandler);
+ } break;
+ case DLL_THREAD_ATTACH: {
+ } break;
+ case DLL_THREAD_DETACH: {
+ } break;
+ }
+
+ return TRUE;
+}
diff --git a/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp b/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp
new file mode 100644
index 0000000..7059ec2
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/usvfsparameters.cpp
@@ -0,0 +1,193 @@
+#include "usvfsparametersprivate.h"
+#include <algorithm>
+
+usvfsParameters::usvfsParameters()
+ : debugMode(false), logLevel(LogLevel::Debug), crashDumpsType(CrashDumpsType::None),
+ delayProcessMs(0)
+{
+ std::fill(std::begin(instanceName), std::end(instanceName), 0);
+ std::fill(std::begin(currentSHMName), std::end(currentSHMName), 0);
+ std::fill(std::begin(currentInverseSHMName), std::end(currentInverseSHMName), 0);
+ std::fill(std::begin(crashDumpsPath), std::end(crashDumpsPath), 0);
+}
+
+usvfsParameters::usvfsParameters(const char* instanceName, const char* currentSHMName,
+ const char* currentInverseSHMName, bool debugMode,
+ LogLevel logLevel, CrashDumpsType crashDumpsType,
+ const char* crashDumpsPath, int delayProcessMs)
+ : usvfsParameters()
+{
+ strncpy_s(this->instanceName, instanceName, _TRUNCATE);
+ strncpy_s(this->currentSHMName, currentSHMName, _TRUNCATE);
+ strncpy_s(this->currentInverseSHMName, currentInverseSHMName, _TRUNCATE);
+ this->debugMode = debugMode;
+ this->logLevel = logLevel;
+ this->crashDumpsType = crashDumpsType;
+ strncpy_s(this->crashDumpsPath, crashDumpsPath, _TRUNCATE);
+ this->delayProcessMs = delayProcessMs;
+}
+
+usvfsParameters::usvfsParameters(const USVFSParameters& oldParams)
+ : usvfsParameters(oldParams.instanceName, oldParams.currentSHMName,
+ oldParams.currentInverseSHMName, oldParams.debugMode,
+ oldParams.logLevel, oldParams.crashDumpsType,
+ oldParams.crashDumpsPath, 0)
+{}
+
+void usvfsParameters::setInstanceName(const char* name)
+{
+ strncpy_s(instanceName, name, _TRUNCATE);
+ strncpy_s(currentSHMName, 60, name, _TRUNCATE);
+ memset(currentInverseSHMName, '\0', _countof(currentInverseSHMName));
+ _snprintf(currentInverseSHMName, 60, "inv_%s", name);
+}
+
+void usvfsParameters::setDebugMode(bool b)
+{
+ debugMode = b;
+}
+
+void usvfsParameters::setLogLevel(LogLevel level)
+{
+ logLevel = level;
+}
+
+void usvfsParameters::setCrashDumpType(CrashDumpsType type)
+{
+ crashDumpsType = type;
+}
+
+void usvfsParameters::setCrashDumpPath(const char* path)
+{
+ if (path && *path && strlen(path) < _countof(crashDumpsPath)) {
+ memcpy(crashDumpsPath, path, strlen(path) + 1);
+ } else {
+ // crashDumpsPath invalid or overflow of USVFSParameters variable so disable
+ // crash dumps:
+ crashDumpsPath[0] = 0;
+ crashDumpsType = CrashDumpsType::None;
+ }
+}
+
+void usvfsParameters::setProcessDelay(int milliseconds)
+{
+ delayProcessMs = milliseconds;
+}
+
+extern "C"
+{
+
+ const char* usvfsLogLevelToString(LogLevel lv)
+ {
+ switch (lv) {
+ case LogLevel::Debug:
+ return "debug";
+
+ case LogLevel::Info:
+ return "info";
+
+ case LogLevel::Warning:
+ return "warning";
+
+ case LogLevel::Error:
+ return "error";
+
+ default:
+ return "unknown";
+ }
+ }
+
+ const char* usvfsCrashDumpTypeToString(CrashDumpsType t)
+ {
+ switch (t) {
+ case CrashDumpsType::None:
+ return "none";
+
+ case CrashDumpsType::Mini:
+ return "mini";
+
+ case CrashDumpsType::Data:
+ return "data";
+
+ case CrashDumpsType::Full:
+ return "full";
+
+ default:
+ return "unknown";
+ }
+ }
+
+ usvfsParameters* usvfsCreateParameters()
+ {
+ return new (std::nothrow) usvfsParameters;
+ }
+
+ usvfsParameters* usvfsDupeParameters(usvfsParameters* p)
+ {
+ if (!p) {
+ return nullptr;
+ }
+
+ auto* dupe = usvfsCreateParameters();
+ if (!dupe) {
+ return nullptr;
+ }
+
+ *dupe = *p;
+
+ return dupe;
+ }
+
+ void usvfsCopyParameters(const usvfsParameters* source, usvfsParameters* dest)
+ {
+ *dest = *source;
+ }
+
+ void usvfsFreeParameters(usvfsParameters* p)
+ {
+ delete p;
+ }
+
+ void usvfsSetInstanceName(usvfsParameters* p, const char* name)
+ {
+ if (p) {
+ p->setInstanceName(name);
+ }
+ }
+
+ void usvfsSetDebugMode(usvfsParameters* p, BOOL debugMode)
+ {
+ if (p) {
+ p->setDebugMode(debugMode);
+ }
+ }
+
+ void usvfsSetLogLevel(usvfsParameters* p, LogLevel level)
+ {
+ if (p) {
+ p->setLogLevel(level);
+ }
+ }
+
+ void usvfsSetCrashDumpType(usvfsParameters* p, CrashDumpsType type)
+ {
+ if (p) {
+ p->setCrashDumpType(type);
+ }
+ }
+
+ void usvfsSetCrashDumpPath(usvfsParameters* p, const char* path)
+ {
+ if (p) {
+ p->setCrashDumpPath(path);
+ }
+ }
+
+ void usvfsSetProcessDelay(usvfsParameters* p, int milliseconds)
+ {
+ if (p) {
+ p->setProcessDelay(milliseconds);
+ }
+ }
+
+} // extern "C"
diff --git a/libs/usvfs/src/usvfs_dll/version.rc b/libs/usvfs/src/usvfs_dll/version.rc
new file mode 100644
index 0000000..7dbf8e3
--- /dev/null
+++ b/libs/usvfs/src/usvfs_dll/version.rc
@@ -0,0 +1,40 @@
+#include "Winver.h"
+#include "..\..\include\usvfs\usvfs_version.h"
+
+#define VER_FILEVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION
+#define VER_FILEVERSION_STR USVFS_VERSION_STRING
+
+#define VER_PRODUCTVERSION USVFS_VERSION_MAJOR,USVFS_VERSION_MINOR,USVFS_VERSION_BUILD,USVFS_VERSION_REVISION
+#define VER_PRODUCTVERSION_STR USVFS_VERSION_STRING
+
+VS_VERSION_INFO VERSIONINFO
+FILEVERSION VER_FILEVERSION
+PRODUCTVERSION VER_PRODUCTVERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+FILEFLAGS (0)
+FILEOS VOS__WINDOWS32
+FILETYPE VFT_DLL
+FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904B0"
+ BEGIN
+ VALUE "FileVersion", VER_FILEVERSION_STR
+ VALUE "CompanyName", "Mod Organizer 2 Team\0"
+ VALUE "FileDescription", "USVFS\0"
+#ifdef _WIN64
+ VALUE "OriginalFilename", "usvfs_x64.dll\0"
+#else
+ VALUE "OriginalFilename", "usvfs_x86.dll\0"
+#endif
+ VALUE "ProductName", "USVFS\0"
+ VALUE "ProductVersion", VER_PRODUCTVERSION_STR
+ END
+ END
+
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409L, 1200
+ END
+END