aboutsummaryrefslogtreecommitdiff
path: root/libs/usvfs/src
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
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')
-rw-r--r--libs/usvfs/src/shared/CMakeLists.txt23
-rw-r--r--libs/usvfs/src/shared/addrtools.h64
-rw-r--r--libs/usvfs/src/shared/directory_tree.cpp43
-rw-r--r--libs/usvfs/src/shared/directory_tree.h629
-rw-r--r--libs/usvfs/src/shared/exceptionex.cpp82
-rw-r--r--libs/usvfs/src/shared/exceptionex.h99
-rw-r--r--libs/usvfs/src/shared/formatters.h158
-rw-r--r--libs/usvfs/src/shared/loghelpers.cpp96
-rw-r--r--libs/usvfs/src/shared/loghelpers.h123
-rw-r--r--libs/usvfs/src/shared/ntdll_declarations.cpp75
-rw-r--r--libs/usvfs/src/shared/ntdll_declarations.h612
-rw-r--r--libs/usvfs/src/shared/pch.cpp1
-rw-r--r--libs/usvfs/src/shared/pch.h87
-rw-r--r--libs/usvfs/src/shared/shared_memory.h53
-rw-r--r--libs/usvfs/src/shared/shmlogger.cpp205
-rw-r--r--libs/usvfs/src/shared/shmlogger.h105
-rw-r--r--libs/usvfs/src/shared/stringcast.cpp40
-rw-r--r--libs/usvfs/src/shared/stringcast.h170
-rw-r--r--libs/usvfs/src/shared/stringutils.cpp146
-rw-r--r--libs/usvfs/src/shared/stringutils.h57
-rw-r--r--libs/usvfs/src/shared/tree_container.h630
-rw-r--r--libs/usvfs/src/shared/unicodestring.cpp77
-rw-r--r--libs/usvfs/src/shared/unicodestring.h110
-rw-r--r--libs/usvfs/src/shared/wildcard.cpp194
-rw-r--r--libs/usvfs/src/shared/wildcard.h67
-rw-r--r--libs/usvfs/src/shared/winapi.cpp490
-rw-r--r--libs/usvfs/src/shared/winapi.h610
-rw-r--r--libs/usvfs/src/shared/windows_sane.h28
-rw-r--r--libs/usvfs/src/thooklib/CMakeLists.txt14
-rw-r--r--libs/usvfs/src/thooklib/asmjit_sane.h29
-rw-r--r--libs/usvfs/src/thooklib/hooklib.cpp740
-rw-r--r--libs/usvfs/src/thooklib/hooklib.h125
-rw-r--r--libs/usvfs/src/thooklib/pch.cpp1
-rw-r--r--libs/usvfs/src/thooklib/ttrampolinepool.cpp601
-rw-r--r--libs/usvfs/src/thooklib/ttrampolinepool.h222
-rw-r--r--libs/usvfs/src/thooklib/udis86wrapper.cpp102
-rw-r--r--libs/usvfs/src/thooklib/udis86wrapper.h62
-rw-r--r--libs/usvfs/src/thooklib/utility.cpp86
-rw-r--r--libs/usvfs/src/thooklib/utility.h35
-rw-r--r--libs/usvfs/src/tinjectlib/CMakeLists.txt8
-rw-r--r--libs/usvfs/src/tinjectlib/asmjit_sane.h32
-rw-r--r--libs/usvfs/src/tinjectlib/injectlib.cpp482
-rw-r--r--libs/usvfs/src/tinjectlib/injectlib.h50
-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
-rw-r--r--libs/usvfs/src/usvfs_helper/CMakeLists.txt6
-rw-r--r--libs/usvfs/src/usvfs_helper/inject.cpp193
-rw-r--r--libs/usvfs/src/usvfs_helper/inject.h51
-rw-r--r--libs/usvfs/src/usvfs_proxy/CMakeLists.txt15
-rw-r--r--libs/usvfs/src/usvfs_proxy/main.cpp221
-rw-r--r--libs/usvfs/src/usvfs_proxy/version.rc40
73 files changed, 15649 insertions, 0 deletions
diff --git a/libs/usvfs/src/shared/CMakeLists.txt b/libs/usvfs/src/shared/CMakeLists.txt
new file mode 100644
index 0000000..b1a7c05
--- /dev/null
+++ b/libs/usvfs/src/shared/CMakeLists.txt
@@ -0,0 +1,23 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Boost CONFIG REQUIRED COMPONENTS algorithm interprocess filesystem)
+find_package(spdlog CONFIG REQUIRED)
+
+file(GLOB sources "*.cpp" "*.h")
+source_group("" FILES ${sources})
+
+add_library(shared STATIC ${sources})
+target_link_libraries(shared
+ PUBLIC
+ Boost::algorithm Boost::interprocess comsuppw
+ Boost::filesystem spdlog::spdlog_header_only)
+target_include_directories(shared
+ PUBLIC ${PROJECT_SOURCE_DIR}/include/usvfs ${CMAKE_CURRENT_SOURCE_DIR})
+target_precompile_headers(shared PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/pch.h)
+target_compile_options(shared PUBLIC /FI"pch.h")
+target_compile_definitions(shared PUBLIC
+ SPDLOG_USE_STD_FORMAT SPDLOG_NO_NAME SPDLOG_NO_REGISTRY_MUTEX
+ BOOST_INTERPROCESS_SHARED_DIR_FUNC BOOST_CONFIG_SUPPRESS_OUTDATED_MESSAGE
+ NOMINMAX _WINDOWS _UNICODE UNICODE
+ _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR
+)
diff --git a/libs/usvfs/src/shared/addrtools.h b/libs/usvfs/src/shared/addrtools.h
new file mode 100644
index 0000000..b888ebf
--- /dev/null
+++ b/libs/usvfs/src/shared/addrtools.h
@@ -0,0 +1,64 @@
+/*
+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 "windows_sane.h"
+
+namespace usvfs::shared
+{
+
+#ifdef _M_AMD64
+typedef DWORD64 REGWORD;
+#elif _M_IX86
+typedef DWORD REGWORD;
+#endif
+
+inline LPVOID AddrAdd(LPVOID address, size_t offset)
+{
+ return reinterpret_cast<LPVOID>(reinterpret_cast<LPBYTE>(address) + offset);
+}
+
+inline std::ptrdiff_t AddrDiff(LPVOID lhs, LPVOID rhs)
+{
+ return reinterpret_cast<LPBYTE>(lhs) - reinterpret_cast<LPBYTE>(rhs);
+}
+
+// implicitly cast pointer to void*, from there cast to target type.
+// This is supposed to be safer than directly reinterpret-casting
+template <typename T>
+inline T void_ptr_cast(void* ptr)
+{
+ return reinterpret_cast<T>(ptr);
+}
+
+template <>
+inline int64_t void_ptr_cast(void* ptr)
+{
+ return reinterpret_cast<int64_t>(ptr);
+}
+
+template <>
+inline uint64_t void_ptr_cast(void* ptr)
+{
+ return reinterpret_cast<uint64_t>(ptr);
+}
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/directory_tree.cpp b/libs/usvfs/src/shared/directory_tree.cpp
new file mode 100644
index 0000000..34cc797
--- /dev/null
+++ b/libs/usvfs/src/shared/directory_tree.cpp
@@ -0,0 +1,43 @@
+/*
+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 "directory_tree.h"
+#include "tree_container.h"
+
+namespace usvfs::shared
+{
+
+fs::path::iterator nextIter(const fs::path::iterator& iter,
+ const fs::path::iterator& end)
+{
+ fs::path::iterator next = iter;
+ advanceIter(next, end);
+ return next;
+}
+
+void advanceIter(fs::path::iterator& iter, const fs::path::iterator& end)
+{
+ ++iter;
+ while (iter != end && (iter->wstring() == L"/" || iter->wstring() == L"\\" ||
+ iter->wstring() == L"."))
+ ++iter;
+}
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/directory_tree.h b/libs/usvfs/src/shared/directory_tree.h
new file mode 100644
index 0000000..f82187f
--- /dev/null
+++ b/libs/usvfs/src/shared/directory_tree.h
@@ -0,0 +1,629 @@
+/*
+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 "exceptionex.h"
+#include "logging.h"
+#include "shared_memory.h"
+#include "stringutils.h"
+#include "wildcard.h"
+
+// simplify unit tests by allowing access to private members
+#ifndef PRIVATE
+#define PRIVATE private
+#endif // PRIVATE
+
+namespace usvfs::shared
+{
+
+template <typename T, typename U>
+struct SHMDataCreator
+{
+ static T create(const U& source, const VoidAllocatorT& allocator)
+ {
+ return T(source, allocator);
+ }
+};
+
+template <typename T, typename U>
+T createData(const U& source, const VoidAllocatorT& allocator)
+{
+ return SHMDataCreator<T, U>::create(source, allocator);
+}
+
+template <typename T>
+T createDataEmpty(const VoidAllocatorT& allocator);
+
+template <typename T>
+void dataAssign(T& destination, const T& source);
+
+// crappy little workaround for fs::path iterating over path separators
+fs::path::iterator nextIter(const fs::path::iterator& iter,
+ const fs::path::iterator& end);
+
+void advanceIter(fs::path::iterator& iter, const fs::path::iterator& end);
+
+// decomposes a path into its components
+//
+class DecomposablePath
+{
+public:
+ explicit DecomposablePath(std::string s) : m_s(std::move(s)), m_begin(0), m_end(0)
+ {
+ m_end = nextSeparator(m_begin);
+ }
+
+ // move to the next component, returns false when there are no more components
+ //
+ bool next()
+ {
+ for (;;) {
+ if (m_end >= m_s.size()) {
+ // done
+ m_begin = m_end;
+ return false;
+ }
+
+ // move begin to one past the last separator found
+ m_begin = m_end + 1;
+
+ // find the next separator
+ m_end = nextSeparator(m_begin);
+
+ // check for components that should be ignored:
+ // - empty, happens when the path ends with a separator
+ // - slashes, happens with consecutive separators
+ // - dot, unnecessary
+ const auto c = current();
+ if (!c.empty() && c != "\\" && c != "/" && c != ".") {
+ return true;
+ }
+ }
+ }
+
+ // checks if next() would return false
+ //
+ bool peekNext() const
+ {
+ auto copy = *this;
+ return copy.next();
+ }
+
+ // the current component, empty when next() returned false
+ //
+ std::string_view current() const { return {m_s.data() + m_begin, m_end - m_begin}; }
+
+private:
+ const std::string m_s;
+ std::size_t m_begin, m_end;
+
+ // finds the next path separator
+ //
+ std::size_t nextSeparator(std::size_t from) const
+ {
+ while (from < m_s.size()) {
+ if (m_s[from] == '/' || m_s[from] == '\\') {
+ break;
+ }
+
+ ++from;
+ }
+
+ return from;
+ }
+};
+
+namespace bi = boost::interprocess;
+namespace bmi = boost::multi_index;
+
+typedef uint8_t TreeFlags;
+
+static const TreeFlags FLAG_DIRECTORY = 0x01;
+static const TreeFlags FLAG_DUMMY = 0x02;
+static const TreeFlags FLAG_FIRSTUSERFLAG = 0x10;
+
+struct MissingThrowT
+{};
+static const MissingThrowT MissingThrow = MissingThrowT();
+
+template <typename NodeDataT>
+class TreeContainer;
+
+template <typename T1, typename T2, typename Alloc>
+struct mutable_pair
+{
+ typedef T1 first_type;
+ typedef T2 second_type;
+
+ mutable_pair(Alloc alloc) : first(T1(alloc)), second(T2(alloc)) {}
+
+ mutable_pair(const T1& f, const T2& s) : first(f), second(s) {}
+
+ mutable_pair(const std::pair<T1, T2>& p) : first(p.first), second(p.second) {}
+
+ T1 first;
+ mutable T2 second;
+};
+
+template <typename Key, typename T, typename Compare, typename Allocator,
+ typename Element = mutable_pair<Key, T, Allocator>>
+using mimap = bmi::multi_index_container<
+ Element,
+ bmi::indexed_by<
+ bmi::ordered_unique<bmi::member<Element, Key, &Element::first>, Compare>>,
+ typename Allocator::template rebind<Element>::other>;
+
+/**
+ * a representation of a directory tree in memory.
+ * This class is designed to be stored in shared memory.
+ */
+template <typename NodeDataT>
+class DirectoryTree
+{
+ template <typename T>
+ friend class TreeContainer;
+
+public:
+ struct CILess
+ {
+ template <typename U, typename V>
+ bool operator()(const U& lhs, const V& rhs) const
+ {
+ const size_t lhsLength = getLength(lhs);
+ const size_t rhsLength = getLength(rhs);
+
+ const auto r =
+ _strnicmp(getCharPtr(lhs), getCharPtr(rhs), std::min(lhsLength, rhsLength));
+
+ if (r == 0) {
+ return lhsLength < rhsLength;
+ }
+
+ return (r < 0);
+ }
+
+ private:
+ const char* getCharPtr(const StringT& s) const { return s.c_str(); }
+
+ const char* getCharPtr(const std::string& s) const { return s.c_str(); }
+
+ const char* getCharPtr(const char* s) const { return s; }
+
+ const char* getCharPtr(std::string_view s) const { return s.data(); }
+
+ size_t getLength(const StringT& s) const { return s.size(); }
+
+ size_t getLength(const std::string& s) const { return s.size(); }
+
+ size_t getLength(const char* s) const { return strlen(s); }
+
+ size_t getLength(std::string_view s) const { return s.size(); }
+ };
+
+ typedef DirectoryTree<NodeDataT> NodeT;
+ typedef bi::deleter<NodeT, SegmentManagerT> DeleterT;
+ typedef NodeDataT DataT;
+
+ typedef bi::shared_ptr<NodeT, VoidAllocatorT, DeleterT> NodePtrT;
+ typedef bi::weak_ptr<NodeT, VoidAllocatorT, DeleterT> WeakPtrT;
+
+ typedef bi::allocator<std::pair<const StringT, NodePtrT>, SegmentManagerT>
+ NodeEntryAllocatorT;
+
+ typedef mimap<StringT, NodePtrT, CILess, NodeEntryAllocatorT> NodeMapT;
+ typedef typename NodeMapT::iterator file_iterator;
+ typedef typename NodeMapT::const_iterator const_file_iterator;
+
+ typedef std::function<void(const NodePtrT&)> VisitorFunction;
+
+ DirectoryTree() = delete;
+ DirectoryTree(const NodeT& reference) = delete;
+ DirectoryTree(NodeT&& reference) = delete;
+ NodeT& operator=(NodeT reference) = delete;
+
+ /**
+ * @brief construct a new node to be inserted in an existing tree
+ **/
+ DirectoryTree(std::string_view name, TreeFlags flags, const NodePtrT& parent,
+ const NodeDataT& data, const VoidAllocatorT& allocator)
+ : m_Parent(parent), m_Name(name.begin(), name.end(), allocator), m_Data(data),
+ m_Nodes(allocator), m_Flags(flags)
+ {}
+
+ ~DirectoryTree() { m_Nodes.clear(); }
+
+ /**
+ * @return parent node
+ */
+ NodePtrT parent() const { return m_Parent.lock(); }
+
+ /**
+ * @return the full path to the node
+ */
+ fs::path path() const
+ {
+ if (m_Parent.lock().get() == nullptr) {
+ if (m_Name.size() == 0) {
+ return fs::path();
+ } else {
+ return fs::path(m_Name.c_str()) / "\\";
+ }
+ } else {
+ return m_Parent.lock()->path() / m_Name.c_str();
+ }
+ }
+
+ /**
+ * @return data connected to this node
+ **/
+ const NodeDataT& data() const { return m_Data; }
+
+ /**
+ * @return name of this node
+ */
+ std::string name() const { return m_Name.c_str(); }
+
+ /**
+ * @brief setFlag change a flag for this node
+ * @param enabled new state for the specified flag
+ */
+ void setFlag(TreeFlags flag, bool enabled = true)
+ {
+ m_Flags = enabled ? m_Flags | flag : m_Flags & ~flag;
+ }
+
+ /**
+ * @return true if the specified flag is set, false otherwise
+ */
+ bool hasFlag(TreeFlags flag) const { return (m_Flags & flag) != 0; }
+
+ /**
+ * @return true if this node is a directory, false if it's a regular file
+ */
+ bool isDirectory() const { return hasFlag(FLAG_DIRECTORY); }
+
+ /**
+ * @return the number of subnodes (directly) below this one
+ */
+ size_t numNodes() const { return m_Nodes.size(); }
+
+ /**
+ * @return number of nodes in this (sub-)tree including this one
+ */
+ size_t numNodesRecursive() const
+ {
+ size_t result = numNodes() + 1;
+
+ for (const auto& node : m_Nodes) {
+ result += node.second->numNodesRecursive();
+ }
+
+ return result;
+ }
+
+ /**
+ * @brief find a node by its path
+ * @param path the path to look up
+ * @return a pointer to the node or a null ptr
+ */
+ NodePtrT findNode(const fs::path& path)
+ {
+ fs::path::iterator iter = path.begin();
+ return findNode(path, iter);
+ }
+
+ /**
+ * @brief find a node by its path
+ * @param path the path to look up
+ * @return a pointer to the node or a null ptr
+ */
+ const NodePtrT findNode(const fs::path& path) const
+ {
+ fs::path::iterator iter = path.begin();
+ return findNode(path, iter);
+ }
+
+ /**
+ * @brief visit the nodes along the specified path (in order) calling the visitor for
+ * each
+ * @param path the path to visit
+ * @param visitor a function called for each node
+ */
+ void visitPath(const fs::path& path, const VisitorFunction& visitor) const
+ {
+ fs::path::iterator iter = path.begin();
+ visitPath(path, iter, visitor);
+ }
+
+ /**
+ * @brief retrieve a node by the specified name
+ * @param name name of the node
+ * @return the node found or an empty pointer if no such node was found
+ */
+ NodePtrT node(std::string_view name, MissingThrowT) const
+ {
+ auto iter = m_Nodes.find(name);
+
+ if (iter != m_Nodes.end()) {
+ return iter->second;
+ } else {
+ USVFS_THROW_EXCEPTION(node_missing_error());
+ }
+ }
+
+ /**
+ * @brief retrieve a node by the specified name
+ * @param name name of the node
+ * @return the node found or an empty pointer if no such node was found
+ */
+ NodePtrT node(std::string_view name)
+ {
+ auto iter = m_Nodes.find(name);
+
+ if (iter != m_Nodes.end()) {
+ return iter->second;
+ } else {
+ return NodePtrT();
+ }
+ }
+
+ /**
+ * @brief retrieve a node by the specified name
+ * @param name name of the node
+ * @return the node found or an empty pointer if no such node was found
+ */
+ const NodePtrT node(std::string_view name, MissingThrowT)
+ {
+ auto iter = m_Nodes.find(name);
+
+ if (iter != m_Nodes.end()) {
+ return iter->second;
+ } else {
+ USVFS_THROW_EXCEPTION(node_missing_error());
+ }
+ }
+
+ /**
+ * @brief retrieve a node by the specified name
+ * @param name name of the node
+ * @return the node found or an empty pointer if no such node was found
+ */
+ const NodePtrT node(std::string_view name) const
+ {
+ auto iter = m_Nodes.find(name);
+
+ if (iter != m_Nodes.end()) {
+ return iter->second;
+ } else {
+ return NodePtrT();
+ }
+ }
+
+ /**
+ * @brief test if a node by the specified name exists
+ * @param name name of the node
+ * @return true if the node exists, false otherwise
+ */
+ bool exists(std::string_view name) const
+ {
+ return m_Nodes.find(name) != m_Nodes.end();
+ }
+
+ /**
+ * @brief find all matches for a pattern
+ * @param pattern the pattern to look for
+ * @return a vector of the found nodes
+ */
+ std::vector<NodePtrT> find(const std::string& pattern) const
+ {
+ // determine if there is a prefix in the pattern that indicates a specific
+ // directory.
+ size_t fixedPart = pattern.find_first_of("*?");
+
+ if (fixedPart == 0)
+ fixedPart = std::string::npos;
+ if (fixedPart != std::string::npos)
+ fixedPart = pattern.find_last_of(R"(\/)", fixedPart);
+
+ std::vector<NodePtrT> result;
+
+ if (fixedPart != std::string::npos) {
+ // if there is a prefix, search for the node representing that path and
+ // search only on that
+ NodePtrT node = findNode(fs::path(pattern.substr(0, fixedPart)));
+ if (node.get() != nullptr) {
+ node->findLocal(result, pattern.substr(fixedPart + 1));
+ }
+ } else {
+ findLocal(result, pattern);
+ }
+
+ return result;
+ }
+
+ /**
+ * @return an iterator to the first leaf
+ **/
+ file_iterator filesBegin() { return m_Nodes.begin(); }
+
+ /**
+ * @return a const iterator to the first leaf
+ **/
+ const_file_iterator filesBegin() const { return m_Nodes.begin(); }
+
+ /**
+ * @return an iterator one past the last leaf
+ **/
+ file_iterator filesEnd() { return m_Nodes.end(); }
+
+ /**
+ * @return a const iterator one past the last leaf
+ **/
+ const_file_iterator filesEnd() const { return m_Nodes.end(); }
+
+ /**
+ * @brief erase the leaf at the specified iterator
+ * @return an iterator to the following file
+ **/
+ file_iterator erase(file_iterator iter) { return m_Nodes.erase(iter); }
+
+ /**
+ * @brief clear all nodes
+ */
+ void clear() { m_Nodes.clear(); }
+
+ void removeFromTree()
+ {
+ if (auto par = parent()) {
+ spdlog::get("usvfs")->info("remove from tree {}", m_Name.c_str());
+ auto self = par->m_Nodes.find(m_Name.c_str());
+ if (self != par->m_Nodes.end()) {
+ par->erase(self);
+ } else {
+ // trying to remove a node that does not exist, most likely because it was
+ // already removed in a lower level call. this is known to happen when MoveFile
+ // has the MOVEFILE_COPY_ALLOWED flag and moving a mapped file.
+ spdlog::get("usvfs")->warn("Failed to remove inexisting node from tree: {}",
+ m_Name.c_str());
+ }
+ }
+ }
+
+ PRIVATE : void set(StringT key, const NodePtrT& value)
+ {
+ auto res = m_Nodes.emplace(std::move(key), value);
+ if (!res.second) {
+ res.first->second = value;
+ }
+ }
+
+ WeakPtrT findRoot() const
+ {
+ if (m_Parent.lock().get() == nullptr) {
+ return m_Self;
+ } else {
+ return m_Parent.lock()->findRoot();
+ }
+ }
+
+ NodePtrT findNode(const fs::path& name, fs::path::iterator& iter)
+ {
+ std::string l = iter->string();
+ auto subNode = m_Nodes.find(iter->string());
+ advanceIter(iter, name.end());
+
+ if (iter == name.end()) {
+ // last name component, should be a local node
+ if (subNode != m_Nodes.end()) {
+ return subNode->second;
+ } else {
+ return NodePtrT();
+ }
+ } else {
+ if (subNode != m_Nodes.end()) {
+ return subNode->second->findNode(name, iter);
+ } else {
+ return NodePtrT();
+ }
+ }
+ }
+
+ const NodePtrT findNode(const fs::path& name, fs::path::iterator& iter) const
+ {
+ auto subNode = m_Nodes.find(iter->string());
+ advanceIter(iter, name.end());
+
+ if (iter == name.end()) {
+ // last name component, should be a local node
+ if (subNode != m_Nodes.end()) {
+ return subNode->second;
+ } else {
+ return NodePtrT();
+ }
+ } else {
+ if (subNode != m_Nodes.end()) {
+ return subNode->second->findNode(name, iter);
+ } else {
+ return NodePtrT();
+ }
+ }
+ }
+
+ void visitPath(const fs::path& path, fs::path::iterator& iter,
+ const VisitorFunction& visitor) const
+ {
+ auto subNode = m_Nodes.find(iter->string());
+
+ if (subNode != m_Nodes.end()) {
+ visitor(subNode->second);
+ advanceIter(iter, path.end());
+ if (iter != path.end()) {
+ subNode->second->visitPath(path, iter, visitor);
+ }
+ }
+ }
+
+ void findLocal(std::vector<NodePtrT>& output, const std::string& pattern) const
+ {
+ for (auto iter = m_Nodes.begin(); iter != m_Nodes.end(); ++iter) {
+ LPCSTR remainder = nullptr;
+
+ if (pattern.size() > 1 && (pattern[0] == '*') &&
+ ((pattern[1] == '/') || (pattern[1] == '\\')) &&
+ iter->second->isDirectory()) {
+ // the star may represent a directory (one directory level, not
+ // multiple!), search in subdirectory
+ iter->second->findLocal(output, pattern.substr(1));
+ } else if ((remainder = wildcard::PartialMatch(iter->second->name().c_str(),
+ pattern.c_str())) != nullptr) {
+ if ((*remainder == '\0') || (strcmp(remainder, "*") == 0)) {
+ NodePtrT node = iter->second;
+ output.push_back(node);
+ }
+
+ if (iter->second->isDirectory()) {
+ iter->second->findLocal(output, remainder);
+ }
+ }
+ }
+ }
+
+ PRIVATE : TreeFlags m_Flags;
+
+ WeakPtrT m_Parent;
+ WeakPtrT m_Self;
+
+ StringT m_Name;
+ NodeDataT m_Data;
+
+ NodeMapT m_Nodes;
+};
+
+template <typename NodeDataT>
+void dumpTree(std::ostream& stream, const DirectoryTree<NodeDataT>& tree, int level = 0)
+{
+ stream << std::string(level, ' ') << tree.name() << " -> " << tree.data() << "\n";
+ for (auto iter = tree.filesBegin(); iter != tree.filesEnd(); ++iter) {
+ dumpTree<NodeDataT>(stream, *iter->second, level + 1);
+ }
+}
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/exceptionex.cpp b/libs/usvfs/src/shared/exceptionex.cpp
new file mode 100644
index 0000000..4a814b5
--- /dev/null
+++ b/libs/usvfs/src/shared/exceptionex.cpp
@@ -0,0 +1,82 @@
+/*
+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 "exceptionex.h"
+#include "winapi.h"
+#include <spdlog/spdlog.h>
+
+namespace usvfs::shared
+{
+
+std::string windows_error::constructMessage(const std::string& input, int inErrorCode)
+{
+ std::ostringstream finalMessage;
+ finalMessage << input;
+
+ LPSTR buffer = nullptr;
+
+ DWORD errorCode = inErrorCode != -1 ? inErrorCode : GetLastError();
+
+ // TODO: the message is not english?
+ if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
+ nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ (LPSTR)&buffer, 0, nullptr) == 0) {
+ finalMessage << " (errorcode " << errorCode << ")";
+ } else {
+ LPSTR lastChar = buffer + strlen(buffer) - 2;
+ *lastChar = '\0';
+ finalMessage << " (" << buffer << " [" << errorCode << "])";
+ LocalFree(buffer); // allocated by FormatMessage
+ }
+
+ SetLastError(
+ errorCode); // restore error code because FormatMessage might have modified it
+ return finalMessage.str();
+}
+
+} // namespace usvfs::shared
+
+void logExtInfo(const std::exception& e, LogLevel logLevel)
+{
+ std::string content;
+
+ if (const std::string* msg = boost::get_error_info<ex_msg>(e)) {
+ content = *msg;
+ }
+
+ if (const DWORD* errorCode = boost::get_error_info<ex_win_errcode>(e)) {
+ content = std::string("error: ") + winapi::ex::ansi::errorString(*errorCode);
+ }
+
+ switch (logLevel) {
+ case LogLevel::Debug:
+ spdlog::get("usvfs")->debug(content);
+ break;
+ case LogLevel::Info:
+ spdlog::get("usvfs")->info(content);
+ break;
+ case LogLevel::Warning:
+ spdlog::get("usvfs")->warn(content);
+ break;
+ case LogLevel::Error:
+ spdlog::get("usvfs")->error(content);
+ break;
+ }
+}
diff --git a/libs/usvfs/src/shared/exceptionex.h b/libs/usvfs/src/shared/exceptionex.h
new file mode 100644
index 0000000..4c298a5
--- /dev/null
+++ b/libs/usvfs/src/shared/exceptionex.h
@@ -0,0 +1,99 @@
+/*
+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 "logging.h"
+#include <boost/exception/all.hpp>
+#include <stdexcept>
+
+typedef boost::error_info<struct tag_message, DWORD> ex_win_errcode;
+typedef boost::error_info<struct tag_message, std::string> ex_msg;
+
+struct std_boost_exception : virtual boost::exception, virtual std::exception
+{
+ const char* what() const noexcept override
+ {
+ return boost::diagnostic_information_what(*this);
+ }
+};
+
+struct incompatibility_error : std_boost_exception
+{};
+struct usage_error : std_boost_exception
+{};
+struct data_error : std_boost_exception
+{};
+struct file_not_found_error : std_boost_exception
+{};
+struct timeout_error : std_boost_exception
+{};
+struct unknown_error : std_boost_exception
+{};
+struct node_missing_error : std_boost_exception
+{};
+
+#define USVFS_THROW_EXCEPTION(x) BOOST_THROW_EXCEPTION(x)
+
+void logExtInfo(const std::exception& e, LogLevel logLevel = LogLevel::Warning);
+
+namespace usvfs::shared
+{
+
+class windows_error : public std::runtime_error
+{
+public:
+ windows_error(const std::string& message, int errorcode = GetLastError())
+ : runtime_error(constructMessage(message, errorcode)), m_ErrorCode(errorcode)
+ {}
+
+ int getErrorCode() const { return m_ErrorCode; }
+
+private:
+ int m_ErrorCode;
+
+ std::string constructMessage(const std::string& input, int errorcode);
+};
+
+class guard
+{
+public:
+ explicit guard(std::function<void()> f) : m_f(std::move(f)) {}
+
+ ~guard()
+ {
+ if (m_f) {
+ m_f();
+ }
+ }
+
+ guard(const guard&);
+ guard& operator=(const guard&);
+
+private:
+ std::function<void()> m_f;
+};
+
+} // namespace usvfs::shared
+
+#define CONCATENATE_DIRECT(s1, s2) s1##s2
+#define CONCATENATE(s1, s2) CONCATENATE_DIRECT(s1, s2)
+#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__)
+#define ON_BLOCK_EXIT(f) usvfs::shared::guard ANONYMOUS_VARIABLE(guard)(f)
diff --git a/libs/usvfs/src/shared/formatters.h b/libs/usvfs/src/shared/formatters.h
new file mode 100644
index 0000000..3d0e47e
--- /dev/null
+++ b/libs/usvfs/src/shared/formatters.h
@@ -0,0 +1,158 @@
+/*
+Userspace Virtual Filesystem
+
+Copyright (C) 2024. 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 <format>
+#include <type_traits>
+
+#include "ntdll_declarations.h"
+
+// formatters for standard types
+
+namespace usvfs::log
+{
+std::string to_string(LPCWSTR value);
+std::string to_string(PCUNICODE_STRING value);
+} // namespace usvfs::log
+
+template <class Enum, class CharT>
+ requires std::is_enum_v<Enum>
+struct std::formatter<Enum, CharT> : std::formatter<std::underlying_type_t<Enum>, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(Enum v, FmtContext& ctx) const
+ {
+ return std::formatter<std::underlying_type_t<Enum>, CharT>::format(
+ static_cast<std::underlying_type_t<Enum>>(v), ctx);
+ }
+};
+
+template <>
+struct std::formatter<LPCWSTR, char> : std::formatter<std::string, char>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(LPCWSTR v, FmtContext& ctx) const
+ {
+ return std::formatter<std::string, char>::format(usvfs::log::to_string(v), ctx);
+ }
+};
+
+template <>
+struct std::formatter<std::wstring, char> : std::formatter<LPCWSTR, char>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(const std::wstring& v, FmtContext& ctx) const
+ {
+ return std::formatter<LPCWSTR, char>::format(v.c_str(), ctx);
+ }
+};
+
+template <>
+struct std::formatter<PCUNICODE_STRING, char> : std::formatter<std::string, char>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(PCUNICODE_STRING v, FmtContext& ctx) const
+ {
+ return std::formatter<std::string, char>::format(usvfs::log::to_string(v), ctx);
+ }
+};
+
+template <>
+struct std::formatter<UNICODE_STRING, char> : std::formatter<std::string, char>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(UNICODE_STRING v, FmtContext& ctx) const
+ {
+ return std::formatter<std::string, char>::format(usvfs::log::to_string(&v), ctx);
+ }
+};
+
+template <class Pointer>
+ requires(std::is_pointer_v<Pointer> && !std::is_same_v<Pointer, const char*> &&
+ !std::is_same_v<Pointer, char*> && !std::is_same_v<Pointer, const void*> &&
+ !std::is_same_v<Pointer, void*>)
+struct std::formatter<Pointer, char> : std::formatter<const void*, char>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(Pointer v, FmtContext& ctx) const
+ {
+ return std::formatter<const void*, char>::format(v, ctx);
+ }
+};
+
+namespace usvfs::log
+{
+
+/**
+ * a small helper class to wrap any object. The whole point is to give us a way
+ * to ensure our own operator<< is used in addParam calls
+ */
+template <typename T>
+class Wrap
+{
+public:
+ explicit Wrap(const T& data) : m_Data(data) {}
+ Wrap(Wrap<T>&& reference) : m_Data(std::move(reference.m_Data)) {}
+
+ Wrap(const Wrap<T>& reference) = delete;
+ Wrap<T>& operator=(const Wrap<T>& reference) = delete;
+
+private:
+ friend struct ::std::formatter<Wrap<T>, char>;
+ const T& m_Data;
+};
+
+template <typename T>
+Wrap<T> wrap(const T& data)
+{
+ return Wrap<T>(data);
+}
+
+} // namespace usvfs::log
+
+template <>
+struct std::formatter<usvfs::log::Wrap<DWORD>, char> : std::formatter<DWORD, char>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(const usvfs::log::Wrap<DWORD>& v, FmtContext& ctx) const
+ {
+ return std::format_to(ctx.out(), "{:x}", v.m_Data);
+ }
+};
+
+template <>
+struct std::formatter<usvfs::log::Wrap<NTSTATUS>, char> : std::formatter<NTSTATUS, char>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(const usvfs::log::Wrap<NTSTATUS>& v,
+ FmtContext& ctx) const
+ {
+ switch (v.m_Data) {
+ case 0x00000000:
+ return std::format_to(ctx.out(), "ok");
+ case 0xC0000022:
+ return std::format_to(ctx.out(), "access denied");
+ case 0xC0000035:
+ return std::format_to(ctx.out(), "exists already");
+ }
+ return std::format_to(ctx.out(), "err {:x}", v.m_Data);
+ }
+};
diff --git a/libs/usvfs/src/shared/loghelpers.cpp b/libs/usvfs/src/shared/loghelpers.cpp
new file mode 100644
index 0000000..bfcabf1
--- /dev/null
+++ b/libs/usvfs/src/shared/loghelpers.cpp
@@ -0,0 +1,96 @@
+/*
+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 <format>
+
+#include "formatters.h"
+#include "loghelpers.h"
+#include "stringcast.h"
+#include "stringutils.h"
+
+namespace ush = usvfs::shared;
+
+namespace usvfs::log
+{
+
+// this is declared in formatters but is defined here to avoid a whole .cpp
+// file for two small functions
+//
+// comments from old code, taken as is:
+//
+// 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
+//
+std::string to_string(LPCWSTR value)
+{
+ if (value == nullptr) {
+ return "<null>";
+ }
+ try {
+ return ush::string_cast<std::string>(value, ush::CodePage::UTF8);
+ } catch (const std::exception& e) {
+ return std::format("<err: {}>", e.what());
+ }
+}
+std::string to_string(PCUNICODE_STRING value)
+{
+ try {
+ return ush::string_cast<std::string>(value->Buffer, ush::CodePage::UTF8,
+ value->Length / sizeof(WCHAR));
+ } catch (const std::exception& e) {
+ return std::format("<err: {}>", e.what());
+ }
+}
+
+spdlog::level::level_enum ConvertLogLevel(LogLevel level)
+{
+ switch (level) {
+ case LogLevel::Debug:
+ return spdlog::level::debug;
+ case LogLevel::Info:
+ return spdlog::level::info;
+ case LogLevel::Warning:
+ return spdlog::level::warn;
+ case LogLevel::Error:
+ return spdlog::level::err;
+ default:
+ return spdlog::level::debug;
+ }
+}
+
+LogLevel ConvertLogLevel(spdlog::level::level_enum level)
+{
+ switch (level) {
+ case spdlog::level::debug:
+ return LogLevel::Debug;
+ case spdlog::level::info:
+ return LogLevel::Info;
+ case spdlog::level::warn:
+ return LogLevel::Warning;
+ case spdlog::level::err:
+ return LogLevel::Error;
+ default:
+ return LogLevel::Debug;
+ }
+}
+
+} // namespace usvfs::log
diff --git a/libs/usvfs/src/shared/loghelpers.h b/libs/usvfs/src/shared/loghelpers.h
new file mode 100644
index 0000000..071ad9d
--- /dev/null
+++ b/libs/usvfs/src/shared/loghelpers.h
@@ -0,0 +1,123 @@
+/*
+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 "formatters.h"
+#include "ntdll_declarations.h"
+#include "shmlogger.h"
+#include "stringutils.h"
+
+namespace usvfs::log
+{
+
+enum class DisplayStyle : uint8_t
+{
+ Hex = 0x01
+};
+
+class CallLoggerDummy
+{
+public:
+ template <typename T>
+ CallLoggerDummy& addParam(const char*, const T&, uint8_t style = 0)
+ {
+ return *this;
+ }
+};
+
+class CallLogger
+{
+public:
+ explicit CallLogger(const char* function)
+ {
+ const char* namespaceend = strrchr(function, ':');
+
+ if (namespaceend != nullptr) {
+ function = namespaceend + 1;
+ }
+
+ m_Message = function;
+ }
+
+ ~CallLogger()
+ {
+ try {
+ static std::shared_ptr<spdlog::logger> log = spdlog::get("hooks");
+ log->debug("{}", m_Message);
+ } catch (...) {
+ // suppress all exceptions in destructor
+ }
+ }
+
+ template <typename T>
+ CallLogger& addParam(const char* name, const T& value, uint8_t style = 0);
+
+private:
+ std::string m_Message;
+};
+
+template <typename T>
+CallLogger& CallLogger::addParam(const char* name, const T& value, uint8_t style)
+{
+ static bool enabled = spdlog::get("hooks")->should_log(spdlog::level::debug);
+ typedef std::underlying_type<DisplayStyle>::type DSType;
+
+ if (enabled) {
+ if constexpr (std::is_pointer_v<T>) {
+ if (value == nullptr) {
+ std::format_to(std::back_inserter(m_Message), "[{}=<null>]", name);
+ } else {
+ std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value);
+ }
+ } else if constexpr (std::is_integral_v<T>) {
+ if (style & static_cast<DSType>(DisplayStyle::Hex)) {
+ std::format_to(std::back_inserter(m_Message), "[{}={:x}]", name, value);
+ } else {
+ std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value);
+ }
+ } else {
+ std::format_to(std::back_inserter(m_Message), "[{}={}]", name, value);
+ }
+ }
+
+ return *this;
+}
+
+spdlog::level::level_enum ConvertLogLevel(LogLevel level);
+LogLevel ConvertLogLevel(spdlog::level::level_enum level);
+
+} // namespace usvfs::log
+
+// prefer the short variant of the function name, without signature.
+// Fall back to the portable boost macro
+#ifdef __FUNCTION__
+#define __MYFUNC__ __FUNCTION__
+#else
+#define __MYFUNC__ BOOST_CURRENT_FUNCTION
+#endif
+
+#define LOG_CALL() usvfs::log::CallLogger(__MYFUNC__)
+
+#define PARAM(val) addParam(#val, val)
+#define PARAMHEX(val) \
+ addParam(#val, val, static_cast<uint8_t>(usvfs::log::DisplayStyle::Hex))
+#define PARAMWRAP(val) addParam(#val, usvfs::log::wrap(val))
diff --git a/libs/usvfs/src/shared/ntdll_declarations.cpp b/libs/usvfs/src/shared/ntdll_declarations.cpp
new file mode 100644
index 0000000..b4bf0f5
--- /dev/null
+++ b/libs/usvfs/src/shared/ntdll_declarations.cpp
@@ -0,0 +1,75 @@
+/*
+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 "ntdll_declarations.h"
+#include <cassert>
+
+#define LOAD_EXT(mod, name) \
+ name = reinterpret_cast<name##_type>(::GetProcAddress(mod, #name)); \
+ assert(name != nullptr)
+
+NtQueryDirectoryFile_type NtQueryDirectoryFile;
+NtQueryDirectoryFileEx_type NtQueryDirectoryFileEx;
+NtQueryFullAttributesFile_type NtQueryFullAttributesFile;
+NtQueryAttributesFile_type NtQueryAttributesFile;
+NtQueryObject_type NtQueryObject;
+NtQueryInformationFile_type NtQueryInformationFile;
+NtQueryInformationByName_type NtQueryInformationByName;
+NtOpenFile_type NtOpenFile;
+NtCreateFile_type NtCreateFile;
+NtClose_type NtClose;
+RtlDoesFileExists_U_type RtlDoesFileExists_U;
+RtlDosPathNameToRelativeNtPathName_U_WithStatus_type
+ RtlDosPathNameToRelativeNtPathName_U_WithStatus;
+RtlReleaseRelativeName_type RtlReleaseRelativeName;
+RtlGetVersion_type RtlGetVersion;
+NtTerminateProcess_type NtTerminateProcess;
+
+static bool ntdll_initialized;
+
+void ntdll_declarations_init()
+{
+ if (!ntdll_initialized) {
+ HMODULE ntDLLMod = GetModuleHandleW(L"ntdll.dll");
+
+ LOAD_EXT(ntDLLMod, NtQueryDirectoryFile);
+ LOAD_EXT(ntDLLMod, NtQueryDirectoryFileEx);
+ LOAD_EXT(ntDLLMod, NtQueryFullAttributesFile);
+ LOAD_EXT(ntDLLMod, NtQueryAttributesFile);
+ LOAD_EXT(ntDLLMod, NtQueryObject);
+ LOAD_EXT(ntDLLMod, NtQueryInformationFile);
+ LOAD_EXT(ntDLLMod, NtQueryInformationByName);
+ LOAD_EXT(ntDLLMod, NtCreateFile);
+ LOAD_EXT(ntDLLMod, NtOpenFile);
+ LOAD_EXT(ntDLLMod, NtClose);
+ LOAD_EXT(ntDLLMod, RtlDoesFileExists_U);
+ LOAD_EXT(ntDLLMod, RtlDosPathNameToRelativeNtPathName_U_WithStatus);
+ LOAD_EXT(ntDLLMod, RtlReleaseRelativeName);
+ LOAD_EXT(ntDLLMod, RtlGetVersion);
+ LOAD_EXT(ntDLLMod, NtTerminateProcess);
+
+ ntdll_initialized = true;
+ }
+}
+
+static struct __Initializer
+{
+ __Initializer() { ntdll_declarations_init(); }
+} __initializer;
diff --git a/libs/usvfs/src/shared/ntdll_declarations.h b/libs/usvfs/src/shared/ntdll_declarations.h
new file mode 100644
index 0000000..813ad8c
--- /dev/null
+++ b/libs/usvfs/src/shared/ntdll_declarations.h
@@ -0,0 +1,612 @@
+/*
+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 "windows_sane.h"
+
+#pragma warning(push)
+#pragma warning(disable : 4201)
+
+typedef LONG NTSTATUS;
+
+typedef struct _IO_STATUS_BLOCK
+{
+ union
+ {
+ NTSTATUS Status;
+ PVOID Pointer;
+ } DUMMYUNIONNAME;
+
+ ULONG_PTR Information;
+} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
+
+typedef struct _FILE_DIRECTORY_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ WCHAR FileName[1];
+} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
+
+typedef struct _FILE_FULL_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ WCHAR FileName[1];
+} FILE_FULL_DIR_INFORMATION, *PFILE_FULL_DIR_INFORMATION;
+
+typedef struct _FILE_ID_FULL_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ LARGE_INTEGER FileId;
+ WCHAR FileName[1];
+} FILE_ID_FULL_DIR_INFORMATION, *PFILE_ID_FULL_DIR_INFORMATION;
+
+typedef struct _FILE_BOTH_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ CCHAR ShortNameLength;
+ WCHAR ShortName[12];
+ WCHAR FileName[1];
+} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;
+
+typedef struct _FILE_ID_BOTH_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ CCHAR ShortNameLength;
+ WCHAR ShortName[12];
+ LARGE_INTEGER FileId;
+ WCHAR FileName[1];
+} FILE_ID_BOTH_DIR_INFORMATION, *PFILE_ID_BOTH_DIR_INFORMATION;
+
+typedef struct _FILE_BASIC_INFORMATION
+{
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ ULONG FileAttributes;
+} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;
+
+typedef struct _FILE_RENAME_INFORMATION
+{
+#if (_WIN32_WINNT >= _WIN32_WINNT_WIN10_RS1)
+ union
+ {
+ BOOLEAN ReplaceIfExists; // FileRenameInformation
+ ULONG Flags; // FileRenameInformationEx
+ } DUMMYUNIONNAME;
+#else
+ BOOLEAN ReplaceIfExists;
+#endif
+ HANDLE RootDirectory;
+ ULONG FileNameLength;
+ WCHAR FileName[1];
+} FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION;
+
+typedef struct _FILE_STANDARD_INFORMATION
+{
+ LARGE_INTEGER AllocationSize;
+ LARGE_INTEGER EndOfFile;
+ ULONG NumberOfLinks;
+ BOOLEAN DeletePending;
+ BOOLEAN Directory;
+} FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION;
+
+typedef struct _FILE_NAMES_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ ULONG FileNameLength;
+ WCHAR FileName[1];
+} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
+
+typedef struct _FILE_INTERNAL_INFORMATION
+{
+ LARGE_INTEGER IndexNumber;
+} FILE_INTERNAL_INFORMATION, *PFILE_INTERNAL_INFORMATION;
+
+typedef struct _FILE_EA_INFORMATION
+{
+ ULONG EaSize;
+} FILE_EA_INFORMATION, *PFILE_EA_INFORMATION;
+
+typedef struct _FILE_ACCESS_INFORMATION
+{
+ ACCESS_MASK AccessFlags;
+} FILE_ACCESS_INFORMATION, *PFILE_ACCESS_INFORMATION;
+
+typedef struct _FILE_POSITION_INFORMATION
+{
+ LARGE_INTEGER CurrentByteOffset;
+} FILE_POSITION_INFORMATION, *PFILE_POSITION_INFORMATION;
+
+typedef struct _FILE_MODE_INFORMATION
+{
+ ULONG Mode;
+} FILE_MODE_INFORMATION, *PFILE_MODE_INFORMATION;
+
+typedef struct _FILE_ALIGNMENT_INFORMATION
+{
+ ULONG AlignmentRequirement;
+} FILE_ALIGNMENT_INFORMATION, *PFILE_ALIGNMENT_INFORMATION;
+
+typedef struct _FILE_NAME_INFORMATION
+{
+ ULONG FileNameLength;
+ WCHAR FileName[1];
+} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;
+
+typedef struct _FILE_ID_EXTD_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ ULONG ReparsePointTag;
+ FILE_ID_128 FileId;
+ WCHAR FileName[1];
+} FILE_ID_EXTD_DIR_INFORMATION, *PFILE_ID_EXTD_DIR_INFORMATION;
+
+typedef struct _FILE_ID_EXTD_BOTH_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ ULONG ReparsePointTag;
+ FILE_ID_128 FileId;
+ CCHAR ShortNameLength;
+ WCHAR ShortName[12];
+ WCHAR FileName[1];
+} FILE_ID_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_EXTD_BOTH_DIR_INFORMATION;
+
+typedef struct _FILE_ID_64_EXTD_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ ULONG ReparsePointTag;
+ LARGE_INTEGER FileId;
+ WCHAR FileName[1];
+} FILE_ID_64_EXTD_DIR_INFORMATION, *PFILE_ID_64_EXTD_DIR_INFORMATION;
+
+typedef struct _FILE_ID_64_EXTD_BOTH_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ ULONG ReparsePointTag;
+ LARGE_INTEGER FileId;
+ CCHAR ShortNameLength;
+ WCHAR ShortName[12];
+ WCHAR FileName[1];
+} FILE_ID_64_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_64_EXTD_BOTH_DIR_INFORMATION;
+
+typedef struct _FILE_ID_ALL_EXTD_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ ULONG ReparsePointTag;
+ LARGE_INTEGER FileId;
+ FILE_ID_128 FileId128;
+ WCHAR FileName[1];
+} FILE_ID_ALL_EXTD_DIR_INFORMATION, *PFILE_ID_ALL_EXTD_DIR_INFORMATION;
+
+typedef struct _FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION
+{
+ ULONG NextEntryOffset;
+ ULONG FileIndex;
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER EndOfFile;
+ LARGE_INTEGER AllocationSize;
+ ULONG FileAttributes;
+ ULONG FileNameLength;
+ ULONG EaSize;
+ ULONG ReparsePointTag;
+ LARGE_INTEGER FileId;
+ FILE_ID_128 FileId128;
+ CCHAR ShortNameLength;
+ WCHAR ShortName[12];
+ WCHAR FileName[1];
+} FILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_ALL_EXTD_BOTH_DIR_INFORMATION;
+
+typedef struct _FILE_ALL_INFORMATION
+{
+ FILE_BASIC_INFORMATION BasicInformation;
+ FILE_STANDARD_INFORMATION StandardInformation;
+ FILE_INTERNAL_INFORMATION InternalInformation;
+ FILE_EA_INFORMATION EaInformation;
+ FILE_ACCESS_INFORMATION AccessInformation;
+ FILE_POSITION_INFORMATION PositionInformation;
+ FILE_MODE_INFORMATION ModeInformation;
+ FILE_ALIGNMENT_INFORMATION AlignmentInformation;
+ FILE_NAME_INFORMATION NameInformation;
+} FILE_ALL_INFORMATION, *PFILE_ALL_INFORMATION;
+
+typedef struct _FILE_OBJECTID_INFORMATION
+{
+ LONGLONG FileReference;
+ UCHAR ObjectId[16];
+ union
+ {
+ struct
+ {
+ UCHAR BirthVolumeId[16];
+ UCHAR BirthObjectId[16];
+ UCHAR DomainId[16];
+ };
+ UCHAR ExtendedInfo[48];
+ };
+} FILE_OBJECTID_INFORMATION, *PFILE_OBJECTID_INFORMATION;
+
+typedef struct _FILE_REPARSE_POINT_INFORMATION
+{
+ LONGLONG FileReference;
+ ULONG Tag;
+} FILE_REPARSE_POINT_INFORMATION, *PFILE_REPARSE_POINT_INFORMATION;
+
+// copied from ntstatus.h
+#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
+#define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L)
+#define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L)
+#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
+#define STATUS_NO_SUCH_FILE ((NTSTATUS)0xC000000FL)
+
+#define SL_RESTART_SCAN 0x01
+#define SL_RETURN_SINGLE_ENTRY 0x02
+#define SL_INDEX_SPECIFIED 0x04
+#define SL_RETURN_ON_DISK_ENTRIES_ONLY 0x08
+
+#define SL_QUERY_DIRECTORY_MASK 0x0b
+
+typedef enum _FILE_INFORMATION_CLASS
+{
+ FileDirectoryInformation = 1,
+ FileFullDirectoryInformation = 2,
+ FileBothDirectoryInformation = 3,
+ FileStandardInformation = 5,
+ FileNameInformation = 9,
+ FileRenameInformation = 10,
+ FileNamesInformation = 12,
+ FileAllInformation = 18,
+ FileObjectIdInformation = 29,
+ FileReparsePointInformation = 33,
+ FileIdBothDirectoryInformation = 37,
+ FileIdFullDirectoryInformation = 38,
+ FileNormalizedNameInformation = 48,
+ FileIdExtdDirectoryInformation = 60,
+ FileIdExtdBothDirectoryInformation = 63,
+ FileId64ExtdDirectoryInformation = 78,
+ FileId64ExtdBothDirectoryInformation = 79,
+ FileIdAllExtdDirectoryInformation = 80,
+ FileIdAllExtdBothDirectoryInformation = 81
+} FILE_INFORMATION_CLASS,
+ *PFILE_INFORMATION_CLASS;
+
+typedef enum _MODE
+{
+ KernelMode,
+ UserMode,
+ MaximumMode
+} MODE;
+
+typedef struct _IO_STATUS_BLOCK IO_STATUS_BLOCK;
+
+typedef struct _IO_STATUS_BLOCK* PIO_STATUS_BLOCK;
+// typedef VOID (NTAPI *PIO_APC_ROUTINE )(__in PVOID ApcContext, __in
+// PIO_STATUS_BLOCK IoStatusBlock, __in ULONG Reserved);
+typedef VOID(NTAPI* PIO_APC_ROUTINE)(PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock,
+ ULONG Reserved);
+typedef enum _FILE_INFORMATION_CLASS FILE_INFORMATION_CLASS;
+
+typedef struct _UNICODE_STRING
+{
+ USHORT Length;
+ USHORT MaximumLength;
+ PWSTR Buffer;
+} UNICODE_STRING, *PUNICODE_STRING;
+typedef const UNICODE_STRING* PCUNICODE_STRING;
+
+typedef struct _OBJECT_ATTRIBUTES
+{
+ ULONG Length;
+ HANDLE RootDirectory;
+ PUNICODE_STRING ObjectName;
+ ULONG Attributes;
+ PVOID SecurityDescriptor;
+ PVOID SecurityQualityOfService;
+} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
+
+typedef enum _POOL_TYPE
+{
+ NonPagedPool,
+ NonPagedPoolExecute = NonPagedPool,
+ PagedPool,
+ NonPagedPoolMustSucceed = NonPagedPool + 2,
+ DontUseThisType,
+ NonPagedPoolCacheAligned = NonPagedPool + 4,
+ PagedPoolCacheAligned,
+ NonPagedPoolCacheAlignedMustS = NonPagedPool + 6,
+ MaxPoolType,
+ NonPagedPoolBase = 0,
+ NonPagedPoolBaseMustSucceed = NonPagedPoolBase + 2,
+ NonPagedPoolBaseCacheAligned = NonPagedPoolBase + 4,
+ NonPagedPoolBaseCacheAlignedMustS = NonPagedPoolBase + 6,
+ NonPagedPoolSession = 32,
+ PagedPoolSession = NonPagedPoolSession + 1,
+ NonPagedPoolMustSucceedSession = PagedPoolSession + 1,
+ DontUseThisTypeSession = NonPagedPoolMustSucceedSession + 1,
+ NonPagedPoolCacheAlignedSession = DontUseThisTypeSession + 1,
+ PagedPoolCacheAlignedSession = NonPagedPoolCacheAlignedSession + 1,
+ NonPagedPoolCacheAlignedMustSSession = PagedPoolCacheAlignedSession + 1,
+ NonPagedPoolNx = 512,
+ NonPagedPoolNxCacheAligned = NonPagedPoolNx + 4,
+ NonPagedPoolSessionNx = NonPagedPoolNx + 32
+} POOL_TYPE;
+
+typedef struct _OBJECT_TYPE_INITIALIZER
+{
+ WORD Length;
+ UCHAR ObjectTypeFlags;
+ ULONG CaseInsensitive : 1;
+ ULONG UnnamedObjectsOnly : 1;
+ ULONG UseDefaultObject : 1;
+ ULONG SecurityRequired : 1;
+ ULONG MaintainHandleCount : 1;
+ ULONG MaintainTypeList : 1;
+ ULONG ObjectTypeCode;
+ ULONG InvalidAttributes;
+ GENERIC_MAPPING GenericMapping;
+ ULONG ValidAccessMask;
+ POOL_TYPE PoolType;
+ ULONG DefaultPagedPoolCharge;
+ ULONG DefaultNonPagedPoolCharge;
+ PVOID DumpProcedure;
+ LONG* OpenProcedure;
+ PVOID CloseProcedure;
+ PVOID DeleteProcedure;
+ LONG* ParseProcedure;
+ LONG* SecurityProcedure;
+ LONG* QueryNameProcedure;
+ UCHAR* OkayToCloseProcedure;
+} OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER;
+
+typedef CCHAR KPROCESSOR_MODE;
+
+typedef struct _OBJECT_HANDLE_INFORMATION
+{
+ ULONG HandleAttributes;
+ ACCESS_MASK GrantedAccess;
+} OBJECT_HANDLE_INFORMATION, *POBJECT_HANDLE_INFORMATION;
+
+typedef struct _OBJECT_NAME_INFORMATION
+{
+ UNICODE_STRING Name;
+} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION;
+
+typedef enum _OBJECT_INFORMATION_CLASS
+{
+ ObjectBasicInformation = 0,
+ ObjectNameInformation = 1,
+ ObjectTypeInformation = 2
+} OBJECT_INFORMATION_CLASS;
+
+typedef struct _RTL_RELATIVE_NAME
+{
+ UNICODE_STRING RelativeName;
+ HANDLE ContainingDirectory;
+ void* CurDirRef;
+} RTL_RELATIVE_NAME, *PRTL_RELATIVE_NAME;
+
+typedef struct _FILE_NETWORK_OPEN_INFORMATION
+{
+ LARGE_INTEGER CreationTime;
+ LARGE_INTEGER LastAccessTime;
+ LARGE_INTEGER LastWriteTime;
+ LARGE_INTEGER ChangeTime;
+ LARGE_INTEGER AllocationSize;
+ LARGE_INTEGER EndOfFile;
+ ULONG FileAttributes;
+} FILE_NETWORK_OPEN_INFORMATION, *PFILE_NETWORK_OPEN_INFORMATION;
+
+#define FILE_DIRECTORY_FILE 0x00000001
+#define FILE_WRITE_THROUGH 0x00000002
+#define FILE_SEQUENTIAL_ONLY 0x00000004
+#define FILE_NO_INTERMEDIATE_BUFFERING 0x00000008
+#define FILE_SYNCHRONOUS_IO_ALERT 0x00000010
+#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020
+#define FILE_NON_DIRECTORY_FILE 0x00000040
+#define FILE_CREATE_TREE_CONNECTION 0x00000080
+#define FILE_COMPLETE_IF_OPLOCKED 0x00000100
+#define FILE_NO_EA_KNOWLEDGE 0x00000200
+#define FILE_OPEN_REMOTE_INSTANCE 0x00000400
+#define FILE_RANDOM_ACCESS 0x00000800
+#define FILE_DELETE_ON_CLOSE 0x00001000
+#define FILE_OPEN_BY_FILE_ID 0x00002000
+#define FILE_OPEN_FOR_BACKUP_INTENT 0x00004000
+#define FILE_NO_COMPRESSION 0x00008000
+#define FILE_OPEN_REQUIRING_OPLOCK 0x00010000
+#define FILE_DISALLOW_EXCLUSIVE 0x00020000
+#define FILE_SESSION_AWARE 0x00040000
+#define FILE_RESERVE_OPFILTER 0x00100000
+#define FILE_OPEN_REPARSE_POINT 0x00200000
+#define FILE_OPEN_NO_RECALL 0x00400000
+#define FILE_OPEN_FOR_FREE_SPACE_QUERY 0x00800000
+#define FILE_CONTAINS_EXTENDED_CREATE_INFORMATION 0x10000000
+
+// Nt
+
+using NtQueryDirectoryFile_type = NTSTATUS(WINAPI*)(HANDLE, HANDLE, PIO_APC_ROUTINE,
+ PVOID, PIO_STATUS_BLOCK, PVOID,
+ ULONG, FILE_INFORMATION_CLASS,
+ BOOLEAN, PUNICODE_STRING, BOOLEAN);
+using NtQueryDirectoryFileEx_type = NTSTATUS(WINAPI*)(HANDLE, HANDLE, PIO_APC_ROUTINE,
+ PVOID, PIO_STATUS_BLOCK, PVOID,
+ ULONG, FILE_INFORMATION_CLASS,
+ ULONG, PUNICODE_STRING);
+
+using NtQueryFullAttributesFile_type =
+ NTSTATUS(WINAPI*)(POBJECT_ATTRIBUTES, PFILE_NETWORK_OPEN_INFORMATION);
+using NtQueryAttributesFile_type = NTSTATUS(WINAPI*)(POBJECT_ATTRIBUTES,
+ PFILE_BASIC_INFORMATION);
+
+using NtQueryObject_type = NTSTATUS(WINAPI*)(
+ HANDLE Handle, OBJECT_INFORMATION_CLASS ObjectInformationClass,
+ PVOID ObjectInformation, ULONG ObjectInformationLength, PULONG ReturnLength);
+using NtQueryInformationFile_type = NTSTATUS(WINAPI*)(
+ HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation,
+ ULONG Length, FILE_INFORMATION_CLASS FileInformationClass);
+
+using NtQueryInformationByName_type = NTSTATUS(WINAPI*)(
+ HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation,
+ ULONG Length, FILE_INFORMATION_CLASS FileInformationClass);
+
+using NtOpenFile_type = NTSTATUS(WINAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES,
+ PIO_STATUS_BLOCK, ULONG, ULONG);
+using NtCreateFile_type = NTSTATUS(WINAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES,
+ PIO_STATUS_BLOCK, PLARGE_INTEGER, ULONG,
+ ULONG, ULONG, ULONG, PVOID, ULONG);
+
+using NtClose_type = NTSTATUS(WINAPI*)(HANDLE);
+
+using NtTerminateProcess_type = NTSTATUS(WINAPI*)(HANDLE ProcessHandle,
+ NTSTATUS ExitStatus);
+
+// Rtl
+
+using RtlDoesFileExists_U_type = NTSYSAPI BOOLEAN(NTAPI*)(PCWSTR);
+using RtlDosPathNameToRelativeNtPathName_U_WithStatus_type =
+ NTSTATUS(NTAPI*)(PCWSTR DosFileName, PUNICODE_STRING NtFileName, PWSTR* FilePath,
+ PRTL_RELATIVE_NAME RelativeName);
+using RtlReleaseRelativeName_type = void(NTAPI*)(PRTL_RELATIVE_NAME RelativeName);
+using RtlGetVersion_type = NTSTATUS(NTAPI*)(PRTL_OSVERSIONINFOW);
+
+extern NtQueryDirectoryFile_type NtQueryDirectoryFile;
+extern NtQueryDirectoryFileEx_type NtQueryDirectoryFileEx;
+extern NtQueryFullAttributesFile_type NtQueryFullAttributesFile;
+extern NtQueryAttributesFile_type NtQueryAttributesFile;
+extern NtQueryObject_type NtQueryObject;
+extern NtQueryInformationFile_type NtQueryInformationFile;
+extern NtQueryInformationByName_type NtQueryInformationByName;
+extern NtOpenFile_type NtOpenFile;
+extern NtCreateFile_type NtCreateFile;
+extern NtClose_type NtClose;
+extern NtTerminateProcess_type NtTerminateProcess;
+extern RtlDoesFileExists_U_type RtlDoesFileExists_U;
+extern RtlDosPathNameToRelativeNtPathName_U_WithStatus_type
+ RtlDosPathNameToRelativeNtPathName_U_WithStatus;
+extern RtlReleaseRelativeName_type RtlReleaseRelativeName;
+extern RtlGetVersion_type RtlGetVersion;
+
+// ensures ntdll functions have been initialized (only needed during static objects
+// initialization)
+void ntdll_declarations_init();
+
+#pragma warning(pop)
diff --git a/libs/usvfs/src/shared/pch.cpp b/libs/usvfs/src/shared/pch.cpp
new file mode 100644
index 0000000..1d9f38c
--- /dev/null
+++ b/libs/usvfs/src/shared/pch.cpp
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/libs/usvfs/src/shared/pch.h b/libs/usvfs/src/shared/pch.h
new file mode 100644
index 0000000..e787f68
--- /dev/null
+++ b/libs/usvfs/src/shared/pch.h
@@ -0,0 +1,87 @@
+#pragma once
+
+#include <algorithm>
+#include <atomic>
+#include <bitset>
+#include <cassert>
+#include <codecvt>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <ctime>
+#include <exception>
+#include <format>
+#include <functional>
+#include <future>
+#include <iomanip>
+#include <ios>
+#include <limits>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <regex>
+#include <shared_mutex>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <thread>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+// clang-format off
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
+#include <ShlObj.h>
+#include <comutil.h>
+#include <Psapi.h>
+#include <shellapi.h>
+#include <shlwapi.h>
+#include <fileapi.h>
+#include <DbgHelp.h>
+// clang-format on
+
+#define BOOST_INTERPROCESS_SEGMENT_MANAGER_ABI 1
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/any.hpp>
+#include <boost/container/flat_set.hpp>
+#include <boost/container/map.hpp>
+#include <boost/container/scoped_allocator.hpp>
+#include <boost/container/slist.hpp>
+#include <boost/container/string.hpp>
+#include <boost/container/vector.hpp>
+#include <boost/current_function.hpp>
+#include <boost/dll/runtime_symbol_info.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/detail/utf8_codecvt_facet.hpp>
+#include <boost/filesystem/operations.hpp>
+#include <boost/filesystem/path.hpp>
+#include <boost/format.hpp>
+#include <boost/interprocess/ipc/message_queue.hpp>
+#include <boost/interprocess/managed_windows_shared_memory.hpp>
+#include <boost/interprocess/offset_ptr.hpp>
+#include <boost/interprocess/smart_ptr/deleter.hpp>
+#include <boost/interprocess/smart_ptr/shared_ptr.hpp>
+#include <boost/interprocess/smart_ptr/weak_ptr.hpp>
+#include <boost/interprocess/sync/named_mutex.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/locale.hpp>
+#include <boost/multi_index/member.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index_container.hpp>
+#include <boost/predef.h>
+#include <boost/static_assert.hpp>
+#include <boost/thread.hpp>
+#include <boost/thread/shared_lock_guard.hpp>
+#include <boost/thread/shared_mutex.hpp>
+#include <boost/tokenizer.hpp>
+#include <boost/type_traits.hpp>
+
+#include <spdlog/spdlog.h>
+
+namespace fs = boost::filesystem;
diff --git a/libs/usvfs/src/shared/shared_memory.h b/libs/usvfs/src/shared/shared_memory.h
new file mode 100644
index 0000000..28da34b
--- /dev/null
+++ b/libs/usvfs/src/shared/shared_memory.h
@@ -0,0 +1,53 @@
+/*
+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 bi = boost::interprocess;
+namespace bc = boost::container;
+
+namespace usvfs::shared
+{
+
+template <typename T>
+using OffsetPtrT = bi::offset_ptr<T, std::int32_t, std::uint64_t>;
+
+using VoidPointerT = OffsetPtrT<void>;
+
+// important: the windows shared memory mechanism, unlike other implementations,
+// automatically removes the SHM object when there are no more "subscribers".
+// MO currently depends on that feature!
+
+// managed_windows_shared_memory apparently doesn't support sharing between
+// 64bit and 32bit processes
+using managed_windows_shared_memory = bi::basic_managed_windows_shared_memory<
+ char, bi::rbtree_best_fit<bi::mutex_family, VoidPointerT, 8>, bi::iset_index>;
+
+using SharedMemoryT = managed_windows_shared_memory;
+using SegmentManagerT = SharedMemoryT::segment_manager;
+
+using VoidAllocatorT = boost::container::scoped_allocator_adaptor<
+ boost::interprocess::allocator<void, SegmentManagerT>>;
+using CharAllocatorT = VoidAllocatorT::rebind<char>::other;
+
+using StringT = bc::basic_string<char, std::char_traits<char>, CharAllocatorT>;
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/shmlogger.cpp b/libs/usvfs/src/shared/shmlogger.cpp
new file mode 100644
index 0000000..66512de
--- /dev/null
+++ b/libs/usvfs/src/shared/shmlogger.cpp
@@ -0,0 +1,205 @@
+/*
+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 warning(disable : 4714)
+#pragma warning(disable : 4503)
+#pragma warning(push, 3)
+#include "shmlogger.h"
+#pragma warning(pop)
+
+#pragma warning(disable : 4996)
+
+using namespace boost::interprocess;
+using namespace boost::posix_time;
+
+SHMLogger* SHMLogger::s_Instance = nullptr;
+
+SHMLogger::owner_t SHMLogger::owner;
+SHMLogger::client_t SHMLogger::client;
+
+SHMLogger::SHMLogger(owner_t, const std::string& queueName)
+ : m_QueueName(queueName),
+ m_LogQueue(create_only, queueName.c_str(), MESSAGE_COUNT, MESSAGE_SIZE),
+ m_DroppedMessages(0)
+{
+ if (s_Instance != nullptr) {
+ throw std::runtime_error("duplicate shm logger instantiation");
+ } else {
+ s_Instance = this;
+ }
+}
+
+SHMLogger::SHMLogger(client_t, const std::string& queueName)
+ : m_QueueName(queueName), m_LogQueue(open_only, queueName.c_str()),
+ m_DroppedMessages(0)
+{
+ if (s_Instance != nullptr) {
+ throw std::runtime_error("duplicate shm logger instantiation");
+ } else {
+ s_Instance = this;
+ }
+}
+
+SHMLogger::~SHMLogger()
+{
+ s_Instance = nullptr;
+ message_queue_interop::remove(m_QueueName.c_str());
+}
+
+SHMLogger& SHMLogger::create(const char* instanceName)
+{
+ if (s_Instance != nullptr) {
+ throw std::runtime_error("duplicate shm logger instantiation");
+ } else {
+ std::string queueName = std::string("__shm_sink_") + instanceName;
+ message_queue::remove(queueName.c_str());
+ new SHMLogger(owner, queueName);
+ atexit([]() {
+ delete s_Instance;
+ });
+ }
+ return *s_Instance;
+}
+
+SHMLogger& SHMLogger::open(const char* instanceName)
+{
+ if (s_Instance != nullptr) {
+ throw std::runtime_error("duplicate shm logger instantiation");
+ } else {
+ new SHMLogger(client, std::string("__shm_sink_") + instanceName);
+ }
+ return *s_Instance;
+}
+
+void SHMLogger::free()
+{
+ if (s_Instance != nullptr) {
+ SHMLogger* temp = s_Instance;
+ s_Instance = nullptr;
+ delete temp;
+ }
+}
+
+bool SHMLogger::tryGet(char* buffer, size_t bufferSize)
+{
+ message_queue_interop::size_type receivedSize;
+ unsigned int prio;
+ bool res = m_LogQueue.try_receive(buffer, static_cast<unsigned int>(bufferSize),
+ receivedSize, prio);
+ if (res) {
+ buffer[std::min(bufferSize - 1, static_cast<size_t>(receivedSize))] = '\0';
+ }
+ return res;
+}
+
+void SHMLogger::get(char* buffer, size_t bufferSize)
+{
+ message_queue_interop::size_type receivedSize;
+ unsigned int prio;
+ m_LogQueue.receive(buffer, static_cast<unsigned int>(bufferSize), receivedSize, prio);
+ buffer[std::min(bufferSize - 1, static_cast<size_t>(receivedSize))] = '\0';
+}
+
+usvfs::sinks::shm_sink::shm_sink(const char* queueName)
+ : m_LogQueue(open_only, (std::string("__shm_sink_") + queueName).c_str()),
+ m_DroppedMessages(0)
+{}
+
+void usvfs::sinks::shm_sink::flush_() {}
+
+void usvfs::sinks::shm_sink::sink_it_(const spdlog::details::log_msg& msg)
+{
+ int droppedMessages = m_DroppedMessages.load(std::memory_order_relaxed);
+ if (droppedMessages > 0) {
+ auto dropMessage = std::format("{} debug messages dropped", droppedMessages);
+ if (m_LogQueue.try_send(dropMessage.c_str(),
+ static_cast<unsigned int>(dropMessage.size()), 0)) {
+ m_DroppedMessages.store(0, std::memory_order_relaxed);
+ }
+ }
+
+ std::string message{msg.payload};
+
+ // blacklist %USERNAME% because PII
+ static const std::string username = std::string(getenv("USERNAME"));
+ boost::algorithm::ireplace_all(message, std::string("\\") + username, "\\USERNAME");
+ boost::algorithm::ireplace_all(message, std::string("/") + username, "/USERNAME");
+
+ if (message.length() > SHMLogger::MESSAGE_SIZE) {
+ std::vector<std::string> splitVec;
+ boost::split(splitVec, message, boost::is_any_of("\r\n"), boost::token_compress_on);
+ for (const std::string& line : splitVec) {
+ output(msg.level, line);
+ }
+ } else {
+ output(msg.level, message);
+ }
+}
+
+void usvfs::sinks::shm_sink::output(spdlog::level::level_enum lev,
+ const std::string& message)
+{
+ bool sent = true;
+
+ // spdlog auto-append line breaks which we don't need. Probably would be
+ // better to not write the breaks to begin with?
+ size_t count = std::min(message.find_last_not_of("\r\n") + 1,
+ static_cast<size_t>(m_LogQueue.get_max_msg_size()));
+
+ // depending on the log level, drop less important messages if the receiver
+ // can't keep up
+ switch (lev) {
+ case spdlog::level::trace:
+ case spdlog::level::debug:
+ case spdlog::level::info: {
+ // m_LogQueue.send(message.c_str(), count, 0);
+ sent = m_LogQueue.try_send(message.c_str(), static_cast<unsigned int>(count), 0);
+ } break;
+ case spdlog::level::err:
+ case spdlog::level::critical: {
+ m_LogQueue.send(message.c_str(), static_cast<unsigned int>(count), 0);
+ } break;
+ default: {
+ boost::posix_time::ptime time =
+ microsec_clock::universal_time() + boost::posix_time::milliseconds(200);
+ sent = m_LogQueue.timed_send(message.c_str(), static_cast<unsigned int>(count), 0,
+ time);
+ } break;
+ }
+
+ if (!sent) {
+ m_DroppedMessages.fetch_add(1, std::memory_order_relaxed);
+ }
+}
+
+void __cdecl boost::interprocess::ipcdetail::get_shared_dir(std::string& shared_dir)
+{
+ PWSTR path;
+ if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_ProgramData, 0, NULL, &path))) {
+ _bstr_t bPath(path);
+ shared_dir = (char*)bPath;
+ shared_dir += "\\USVFS";
+ } else {
+ shared_dir = "C:\\ProgramData\\USVFS";
+ }
+ boost::filesystem::path boostPath(shared_dir);
+ if (!boost::filesystem::exists(boostPath))
+ boost::filesystem::create_directories(boostPath);
+}
diff --git a/libs/usvfs/src/shared/shmlogger.h b/libs/usvfs/src/shared/shmlogger.h
new file mode 100644
index 0000000..158f941
--- /dev/null
+++ b/libs/usvfs/src/shared/shmlogger.h
@@ -0,0 +1,105 @@
+/*
+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 <spdlog/details/null_mutex.h>
+#include <spdlog/sinks/base_sink.h>
+
+#include "logging.h"
+#include "shared_memory.h"
+#include "windows_sane.h"
+
+typedef boost::interprocess::message_queue_t<usvfs::shared::VoidPointerT>
+ message_queue_interop;
+
+namespace usvfs::sinks
+{
+class shm_sink : public spdlog::sinks::base_sink<spdlog::details::null_mutex>
+{
+public:
+ shm_sink(const char* queueName);
+
+protected:
+ void sink_it_(const spdlog::details::log_msg& msg) override;
+ void output(spdlog::level::level_enum lev, const std::string& message);
+ void flush_() override;
+
+private:
+ message_queue_interop m_LogQueue;
+ std::atomic<int> m_DroppedMessages;
+};
+
+} // namespace usvfs::sinks
+
+class SHMLogger
+{
+public:
+ static const size_t MESSAGE_COUNT = 1024;
+ static const size_t MESSAGE_SIZE = 512;
+
+ static SHMLogger& create(const char* instanceName);
+ static SHMLogger& open(const char* instanceName);
+ static void free();
+
+ static bool isInstantiated() { return s_Instance != nullptr; }
+
+ static inline SHMLogger& instance()
+ {
+ if (s_Instance == nullptr) {
+ throw std::runtime_error("shm logger not instantiated");
+ }
+
+ return *s_Instance;
+ }
+
+ void log(LogLevel logLevel, const std::string& message);
+ bool tryGet(char* buffer, size_t bufferSize);
+ void get(char* buffer, size_t bufferSize);
+
+private:
+ struct owner_t
+ {};
+ static owner_t owner;
+
+ struct client_t
+ {};
+ static client_t client;
+
+private:
+ SHMLogger(owner_t, const std::string& instanceName);
+ SHMLogger(client_t, const std::string& instanceName);
+
+ SHMLogger(const SHMLogger&) = delete;
+ SHMLogger& operator=(const SHMLogger&) = delete;
+
+ ~SHMLogger();
+
+private:
+ static SHMLogger* s_Instance;
+
+ message_queue_interop m_LogQueue;
+
+ std::string m_SHMName;
+ std::string m_LockName;
+ std::string m_QueueName;
+
+ std::atomic<int> m_DroppedMessages;
+};
diff --git a/libs/usvfs/src/shared/stringcast.cpp b/libs/usvfs/src/shared/stringcast.cpp
new file mode 100644
index 0000000..47822d5
--- /dev/null
+++ b/libs/usvfs/src/shared/stringcast.cpp
@@ -0,0 +1,40 @@
+/*
+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::shared
+{
+
+UINT windowsCP(CodePage codePage)
+{
+ switch (codePage) {
+ case CodePage::LOCAL:
+ return CP_ACP;
+ case CodePage::UTF8:
+ return CP_UTF8;
+ case CodePage::LATIN1:
+ return 850;
+ }
+ // this should not be possible in practice
+ throw std::runtime_error("unsupported codePage");
+}
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/stringcast.h b/libs/usvfs/src/shared/stringcast.h
new file mode 100644
index 0000000..0ce56d1
--- /dev/null
+++ b/libs/usvfs/src/shared/stringcast.h
@@ -0,0 +1,170 @@
+/*
+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 "exceptionex.h"
+
+namespace usvfs::shared
+{
+
+enum class CodePage
+{
+ LOCAL,
+ LATIN1,
+ UTF8
+};
+
+template <typename ToT, typename FromT>
+class string_cast_impl
+{
+public:
+ static ToT cast(const FromT& source, CodePage codePage, size_t sourceLength);
+};
+
+template <typename ToT, typename FromT>
+ToT string_cast(FromT source, CodePage codePage = CodePage::LOCAL,
+ size_t sourceLength = std::numeric_limits<size_t>::max())
+{
+ return string_cast_impl<ToT, FromT>::cast(source, codePage, sourceLength);
+}
+
+template <typename ToT, typename CharT>
+class string_cast_impl<ToT, std::basic_string<CharT>>
+{
+public:
+ static ToT cast(const std::basic_string<CharT>& source, CodePage codePage,
+ size_t sourceLength)
+ {
+ return string_cast_impl<ToT, const CharT*>::cast(source.c_str(), codePage,
+ sourceLength);
+ }
+};
+
+template <typename ToT, typename CharT>
+class string_cast_impl<ToT, CharT*>
+{
+ BOOST_STATIC_ASSERT(!boost::is_base_and_derived<ToT, CharT>::value);
+
+public:
+ static ToT cast(CharT* source, CodePage codePage, size_t sourceLength)
+ {
+ return string_cast_impl<ToT, const CharT*>::cast(source, codePage, sourceLength);
+ }
+};
+
+template <typename ToT, typename CharT, int N>
+class string_cast_impl<ToT, CharT[N]>
+{
+public:
+ static ToT cast(CharT (&source)[N], CodePage codePage, size_t sourceLength)
+ {
+ return string_cast_impl<ToT, const CharT*>::cast(source, codePage, sourceLength);
+ }
+};
+
+UINT windowsCP(CodePage codePage);
+
+template <>
+class string_cast_impl<std::string, const wchar_t*>
+{
+public:
+ static std::string cast(const wchar_t* const& source, CodePage codePage,
+ size_t sourceLength)
+ {
+ std::string result;
+
+ if (sourceLength == std::numeric_limits<size_t>::max()) {
+ sourceLength = wcslen(source);
+ }
+
+ if (sourceLength > 0) {
+ // use utf8 or local 8-bit encoding depending on user choice
+ UINT cp = windowsCP(codePage);
+ // preflight to find out the required buffer size
+ int outLength = WideCharToMultiByte(cp, 0, source, static_cast<int>(sourceLength),
+ nullptr, 0, nullptr, nullptr);
+ if (outLength == 0) {
+ throw windows_error("string conversion failed");
+ }
+ result.resize(outLength);
+ outLength = WideCharToMultiByte(cp, 0, source, static_cast<int>(sourceLength),
+ &result[0], outLength, nullptr, nullptr);
+ if (outLength == 0) {
+ throw windows_error("string conversion failed");
+ }
+ // fix output string length (i.e. in case of unconvertible characters
+ while (result[outLength - 1] == L'\0') {
+ result.resize(--outLength);
+ }
+ }
+
+ return result;
+ }
+};
+
+template <>
+class string_cast_impl<std::wstring, const char*>
+{
+public:
+ static std::wstring cast(const char* const& source, CodePage codePage,
+ size_t sourceLength)
+ {
+ std::wstring result;
+
+ if (sourceLength == std::numeric_limits<size_t>::max()) {
+ sourceLength = strlen(source);
+ }
+
+ if (sourceLength > 0) {
+ // use utf8 or local 8-bit encoding depending on user choice
+ UINT cp = windowsCP(codePage);
+ // preflight to find out the required source size
+ int outLength = MultiByteToWideChar(cp, 0, source, static_cast<int>(sourceLength),
+ &result[0], 0);
+ if (outLength == 0) {
+ throw windows_error("string conversion failed");
+ }
+ result.resize(outLength);
+ outLength = MultiByteToWideChar(cp, 0, source, static_cast<int>(sourceLength),
+ &result[0], outLength);
+ if (outLength == 0) {
+ throw windows_error("string conversion failed");
+ }
+ while (result[outLength - 1] == L'\0') {
+ result.resize(--outLength);
+ }
+ }
+
+ return result;
+ }
+};
+
+template <>
+class string_cast_impl<std::wstring, const wchar_t*>
+{
+public:
+ static std::wstring cast(const wchar_t* const& source, CodePage, size_t)
+ {
+ return std::wstring(source);
+ }
+};
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/stringutils.cpp b/libs/usvfs/src/shared/stringutils.cpp
new file mode 100644
index 0000000..2ddbf64
--- /dev/null
+++ b/libs/usvfs/src/shared/stringutils.cpp
@@ -0,0 +1,146 @@
+/*
+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 "stringutils.h"
+#include "windows_sane.h"
+
+namespace usvfs::shared
+{
+
+void strncpy_sz(char* dest, const char* src, size_t destSize)
+{
+ if (destSize > 0) {
+ strncpy(dest, src, destSize - 1);
+ dest[destSize - 1] = '\0';
+ }
+}
+
+void wcsncpy_sz(wchar_t* dest, const wchar_t* src, size_t destSize)
+{
+ if ((destSize > 0) && (dest != nullptr)) {
+ wcsncpy(dest, src, destSize - 1);
+ dest[destSize - 1] = L'\0';
+ }
+}
+
+bool startswith(const wchar_t* string, const wchar_t* subString)
+{
+ while ((*string != '\0') && (*subString != '\0')) {
+ if (towlower(*string) != towlower(*subString)) {
+ return false;
+ }
+ ++string;
+ ++subString;
+ }
+
+ return *subString == '\0';
+}
+
+static fs::path normalize(const fs::path& path)
+{
+ fs::path result;
+
+ boost::locale::generator gen;
+ auto loc = gen("en_US.UTF-8");
+ for (fs::path::iterator iter = path.begin(); iter != path.end(); ++iter) {
+ if (*iter == "..") {
+ result = result.parent_path();
+ } else if (*iter != ".") {
+ result /= boost::to_lower_copy(iter->string(), loc);
+ } // single dot is ignored
+ }
+ return result;
+}
+
+fs::path make_relative(const fs::path& fromIn, const fs::path& toIn)
+{
+ // converting path to lower case to make iterator comparison work correctly
+ // on case-insenstive filesystems
+ fs::path from(fs::absolute(fromIn));
+ fs::path to(fs::absolute(toIn));
+
+ // find common base
+ fs::path::const_iterator fromIter(from.begin());
+ fs::path::const_iterator toIter(to.begin());
+
+ // TODO the following equivalent test is probably quite expensive as new
+ // paths are created for each iteration but the case sensitivity depends on
+ // the fs
+ while ((fromIter != from.end()) && (toIter != to.end()) &&
+ (boost::iequals(fromIter->string(), toIter->string()))) {
+ ++fromIter;
+ ++toIter;
+ }
+
+ // Navigate backwards in directory to reach previously found base
+ fs::path result;
+ for (; fromIter != from.end(); ++fromIter) {
+ if (*fromIter != ".") {
+ result /= "..";
+ }
+ }
+
+ // Now navigate down the directory branch
+ for (; toIter != to.end(); ++toIter) {
+ result /= *toIter;
+ }
+ return result;
+}
+
+std::string to_hex(void* bufferIn, size_t bufferSize)
+{
+ unsigned char* buffer = static_cast<unsigned char*>(bufferIn);
+ std::ostringstream temp;
+ temp << std::hex;
+ for (size_t i = 0; i < bufferSize; ++i) {
+ temp << std::setfill('0') << std::setw(2) << (unsigned int)buffer[i];
+ if ((i % 16) == 15) {
+ temp << "\n";
+ } else {
+ temp << " ";
+ }
+ }
+ return temp.str();
+}
+
+std::wstring to_upper(const std::wstring& input)
+{
+ std::wstring result;
+ result.resize(input.size());
+ ::LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, input.c_str(),
+ static_cast<int>(input.size()), &result[0],
+ static_cast<int>(result.size()));
+ return result;
+}
+
+std::string byte_string(std::size_t n)
+{
+ auto s = std::to_string(n);
+ auto p = s.size();
+
+ while (p > 3) {
+ p -= 3;
+ s.insert(p, 1, ',');
+ }
+
+ return s + " B";
+}
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/stringutils.h b/libs/usvfs/src/shared/stringutils.h
new file mode 100644
index 0000000..64d0af7
--- /dev/null
+++ b/libs/usvfs/src/shared/stringutils.h
@@ -0,0 +1,57 @@
+/*
+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::shared
+{
+
+void strncpy_sz(char* dest, const char* src, size_t destSize);
+void wcsncpy_sz(wchar_t* dest, const wchar_t* src, size_t destSize);
+
+bool startswith(const wchar_t* string, const wchar_t* subString);
+
+// Return path when appended to a_From will resolve to same as a_To
+fs::path make_relative(const fs::path& from, const fs::path& to);
+
+std::string to_hex(void* bufferIn, size_t bufferSize);
+
+// convert unicode string to upper-case (locale invariant)
+std::wstring to_upper(const std::wstring& input);
+
+// formats a number with thousand separators and B at the end
+//
+std::string byte_string(std::size_t n);
+
+class FormatGuard
+{
+ std::ostream& m_Stream;
+ std::ios::fmtflags m_Flags;
+
+public:
+ FormatGuard(std::ostream& stream) : m_Stream(stream), m_Flags(stream.flags()) {}
+
+ ~FormatGuard() { m_Stream.flags(m_Flags); }
+
+ FormatGuard(const FormatGuard&) = delete;
+ FormatGuard& operator=(FormatGuard&) = delete;
+};
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/tree_container.h b/libs/usvfs/src/shared/tree_container.h
new file mode 100644
index 0000000..a2f61fa
--- /dev/null
+++ b/libs/usvfs/src/shared/tree_container.h
@@ -0,0 +1,630 @@
+#pragma once
+
+#include "directory_tree.h"
+#include "shared_memory.h"
+
+namespace usvfs::shared
+{
+
+// smart pointer to DirectoryTrees (only intended for top-level nodes). This
+// will transparently switch to new shared memory regions in case they get
+// reallocated
+//
+//
+// reassign() is called from a variety of places when the current chunk of
+// shared memory is full or is marked as being outdated; its job is to either
+// find another chunk that may have been created by another process or to create
+// a brand new one
+//
+//
+// if there is only a single process hooked, it will slowly fill up the shared
+// memory when adding files, eventually throw a bi::bad_alloc and end up in
+// reassign(); a new block will be allocated, the data copied over, and the old
+// block will be deallocated
+//
+//
+// when multiple processes are involved, things are more complicated
+//
+// two processes A and B will start by using the same shared memory, but they
+// have their own pointer to it that's local to the process (the `m_TreeMeta`
+// member variable)
+//
+// so when process A fills up the shared memory and reallocates it, process B
+// is still pointing to the old shared memory; only when process B does some
+// operation that accesses the file tree will the pointer be checked and
+// adjusted to point to the new shared memory
+//
+// in this example, when process A ran out of memory, it set `outdated` to
+// `true` in the shared memory block, allocated a new one and copied the data
+// over, but it did not deallocate the block because process B is still
+// pointing to it
+//
+// when process B tries to access the block, it checks `outdated` (see get());
+// if it's true, it means that it's pointing to an outdated shared memory block
+// and must find the new one that process A created
+//
+// the new block is not necessarily the very next name, it is possible for
+// process A to burn through a series of blocks quickly when adding a bunch of
+// new files, and these blocks will all have been deallocated by the time
+// process B tries to find the newest one
+//
+// so when a process sees its block as outdated, it will try to open a bunch of
+// names until one exists that is not outdated; if it can't find the block
+// (shouldn't happen), it will just create a new one
+//
+template <typename TreeT>
+class TreeContainer
+{
+public:
+ /**
+ * @brief Constructor
+ * @param SHMName name of the shared memory holding the tree. This should contain the
+ * running number
+ * @param size initial size in bytes of the container. since the tree is resized by
+ * doubling this should be a power of two. 64k is supposed to be the page size on
+ * windows so smaller allocations make little sense
+ * @note size can't be too small. If initial allocations fail automatic growing won't
+ * work
+ */
+ TreeContainer(const std::string& SHMName, size_t size = 64 * 1024)
+ : m_TreeMeta(nullptr), m_SHMName(SHMName)
+ {
+ std::locale global_loc = std::locale();
+ std::locale loc(global_loc, new fs::detail::utf8_codecvt_facet);
+ fs::path::imbue(loc);
+
+ // append _1 to the name if it doesn't end with _N already
+ std::regex pattern(R"exp((.*_)(\d+))exp");
+ std::smatch match;
+ std::string shmName(m_SHMName.c_str());
+ regex_match(shmName, match, pattern);
+
+ if (match.size() != 3) {
+ m_SHMName += "_1";
+ }
+
+ // creates a new memory block if this is the first process to run or attach
+ // to an already existing one
+ createOrOpen(m_SHMName, size);
+
+ spdlog::get("usvfs")->info("attached to {0} with {1} nodes, size {2}", m_SHMName,
+ m_TreeMeta->tree->numNodesRecursive(),
+ byte_string(m_SHM->get_size()));
+ }
+
+ TreeContainer(const TreeContainer&) = delete;
+ TreeContainer& operator=(const TreeContainer&) = delete;
+
+ ~TreeContainer()
+ {
+ if (unassign(m_SHM, m_TreeMeta)) {
+ bi::shared_memory_object::remove(m_SHMName.c_str());
+ }
+ }
+
+ /**
+ * @return retrieve an allocator that can be used to create objects in this tree
+ */
+ VoidAllocatorT allocator() { return VoidAllocatorT(m_SHM->get_segment_manager()); }
+
+ template <typename... Arguments>
+ typename TreeT::DataT create(Arguments&&... args)
+ {
+ return TreeT::DataT(std::forward<Arguments>(args)..., allocator());
+ }
+
+ TreeT* operator->() { return get(); }
+
+ /**
+ * @return raw pointer to the managed tree
+ */
+ TreeT* get()
+ {
+ if (m_TreeMeta->outdated) {
+ reassign();
+ }
+
+ return m_TreeMeta->tree.get();
+ }
+
+ /**
+ * @return raw const pointer to the managed tree
+ */
+ const TreeT* get() const
+ {
+ if (m_TreeMeta->outdated) {
+ // safe const_cast, TreeContainer are never created const
+ const_cast<TreeContainer<TreeT>*>(this)->reassign();
+ }
+
+ return m_TreeMeta->tree.get();
+ }
+
+ const TreeT* operator->() const { return get(); }
+
+ /**
+ * @return current name of the managed shared memory
+ */
+ std::string shmName() const { return m_SHMName; }
+
+ void clear() { m_TreeMeta->tree->clear(); }
+
+ /**
+ * @brief add a new file to the tree
+ *
+ * @param name name of the file, expected to be relative to this directory
+ * @param data the file data to attach
+ * @param flags flags for this files
+ * @param overwrite if true, the new leaf will overwrite an existing one that compares
+ *as "equal"
+ * @return pointer to the new node or a null ptr
+ **/
+ template <typename T>
+ typename TreeT::NodePtrT addFile(const fs::path& name, const T& data,
+ TreeFlags flags = 0, bool overwrite = true)
+ {
+ for (;;) {
+ DecomposablePath dp(name.string());
+
+ try {
+ return addNode(m_TreeMeta->tree.get(), dp, data, overwrite, flags, allocator());
+ } catch (const bi::bad_alloc&) {
+ }
+
+ reassign();
+ }
+ }
+
+ /**
+ * @brief add a new directory to the tree
+ *
+ * @param name name of the file, expected to be relative to this directory
+ * @param data the file data to attach
+ * @param flags flags for this files
+ * @param overwrite if true, the new leaf will overwrite an existing one that compares
+ *as "equal"
+ * @return pointer to the new node or a null ptr
+ **/
+ template <typename T>
+ typename TreeT::NodePtrT addDirectory(const fs::path& name, const T& data,
+ TreeFlags flags = 0, bool overwrite = true)
+ {
+ for (;;) {
+ DecomposablePath dp(name.string());
+
+ try {
+ return addNode(m_TreeMeta->tree.get(), dp, data, overwrite,
+ flags | FLAG_DIRECTORY, allocator());
+ } catch (const bi::bad_alloc&) {
+ }
+
+ reassign();
+ }
+ }
+
+ void getBuffer(void*& buffer, size_t& bufferSize) const
+ {
+ buffer = m_SHM->get_address();
+ bufferSize = m_SHM->get_size();
+ }
+
+private:
+ struct TreeMeta
+ {
+ TreeMeta(const typename TreeT::DataT& data, SegmentManagerT* segmentManager)
+ : tree(segmentManager->construct<TreeT>(bi::anonymous_instance)(
+ "", true, TreeT::NodePtrT(), data, VoidAllocatorT(segmentManager))),
+ referenceCount(0), // reference count only set on top level node
+ outdated(false)
+ {}
+
+ OffsetPtrT<TreeT> tree;
+ long referenceCount;
+ bool outdated;
+ bi::interprocess_mutex mutex;
+ };
+
+ std::string m_SHMName;
+ std::shared_ptr<SharedMemoryT> m_SHM;
+ TreeMeta* m_TreeMeta;
+
+ typename TreeT::DataT createEmpty()
+ {
+ return createDataEmpty<typename TreeT::DataT>(allocator());
+ }
+
+ template <typename T>
+ TreeT* createSubNode(const VoidAllocatorT& allocator, std::string_view name,
+ unsigned long flags, const T& data)
+ {
+ auto* manager = allocator.get_segment_manager();
+
+ return manager->construct<TreeT>(bi::anonymous_instance)(
+ name, flags, TreeT::NodePtrT(),
+ createData<typename TreeT::DataT, T>(data, allocator), manager);
+ }
+
+ typename TreeT::NodePtrT createSubPtr(TreeT* subNode)
+ {
+ SharedMemoryT::segment_manager* manager = m_SHM->get_segment_manager();
+ return TreeT::NodePtrT(subNode, allocator(), TreeT::DeleterT(manager));
+ }
+
+ template <typename T>
+ typename TreeT::NodePtrT addNode(TreeT* base, DecomposablePath& path, const T& data,
+ bool overwrite, unsigned int flags,
+ const VoidAllocatorT& allocator)
+ {
+ if (!path.peekNext()) {
+ typename TreeT::NodePtrT newNode = base->node(path.current());
+
+ if (!newNode) {
+ // last name component, should be the filename
+ TreeT* node = createSubNode(allocator, path.current(), flags, data);
+ newNode = createSubPtr(node);
+ newNode->m_Self = TreeT::WeakPtrT(newNode);
+ newNode->m_Parent = base->m_Self;
+ base->set(StringT(path.current(), allocator), newNode);
+ return newNode;
+ } else if (overwrite) {
+ newNode->m_Data = createData<typename TreeT::DataT, T>(data, allocator);
+ newNode->m_Flags = static_cast<usvfs::shared::TreeFlags>(flags);
+ return newNode;
+ } else {
+ // the node is already in the tree, overwrite is false, nothing to do
+ return {};
+ }
+ } else {
+ // not last component, continue search in child node
+ auto subNode = base->m_Nodes.find(path.current());
+
+ if (subNode == base->m_Nodes.end()) {
+ typename TreeT::NodePtrT newNode = createSubPtr(createSubNode(
+ allocator, path.current(), FLAG_DIRECTORY | FLAG_DUMMY, createEmpty()));
+
+ subNode =
+ base->m_Nodes.emplace(StringT(path.current(), allocator), newNode).first;
+ subNode->second->m_Self = TreeT::WeakPtrT(subNode->second);
+ subNode->second->m_Parent = base->m_Self;
+ }
+
+ path.next();
+
+ return addNode(subNode->second.get().get(), path, data, overwrite, flags,
+ allocator);
+ }
+ }
+
+ /**
+ * @brief copy content of one tree to a different tree (in a different shared memory
+ * segment
+ * @param destination
+ * @param reference
+ * @note at the time this is called, destination needs to refer to the shm of
+ * "destination" so that objects can be allocated in the new tree
+ */
+ void copyTree(TreeT* destination, const TreeT* reference)
+ {
+ VoidAllocatorT allocator = VoidAllocatorT(m_SHM->get_segment_manager());
+ destination->m_Flags = reference->m_Flags;
+ dataAssign(destination->m_Data, reference->m_Data);
+ destination->m_Name.assign(reference->m_Name.c_str());
+
+ for (const auto& kv : reference->m_Nodes) {
+ TreeT* newNode = createSubNode(allocator, "", true, createEmpty());
+ typename TreeT::NodePtrT newNodePtr = createSubPtr(newNode);
+
+ // need to set self BEFORE recursively copying the subtree, otherwise
+ // how would we assign parent pointers?
+ newNode->m_Self = newNodePtr;
+
+ TreeT* source = reinterpret_cast<TreeT*>(kv.second.get().get());
+ copyTree(newNode, source);
+ destination->set(newNode->m_Name, newNodePtr);
+ newNode->m_Parent = destination->m_Self;
+ }
+ }
+
+ int increaseRefCount(TreeMeta* treeMeta)
+ {
+ bi::scoped_lock<bi::interprocess_mutex> lock(treeMeta->mutex);
+ return ++treeMeta->referenceCount;
+ }
+
+ int decreaseRefCount(TreeMeta* treeMeta)
+ {
+ bi::scoped_lock<bi::interprocess_mutex> lock(treeMeta->mutex);
+ return --treeMeta->referenceCount;
+ }
+
+ // see activateSHM() for return value
+ //
+ std::optional<std::string> createOrOpen(const std::string& SHMName, size_t size)
+ {
+ SharedMemoryT* newSHM = openSHM(SHMName);
+
+ if (newSHM) {
+ spdlog::get("usvfs")->info("{} opened in process {}", SHMName,
+ ::GetCurrentProcessId());
+ } else {
+ newSHM = createSHM(SHMName, size);
+
+ if (newSHM) {
+ spdlog::get("usvfs")->info("{} created in process {}", SHMName,
+ ::GetCurrentProcessId());
+ }
+ }
+
+ if (!newSHM) {
+ spdlog::get("usvfs")->error("failed to create or open {} in process {}", SHMName,
+ ::GetCurrentProcessId());
+
+ throw std::exception("no shm instance");
+ }
+
+ return activateSHM(newSHM, SHMName);
+ }
+
+ // see activateSHM() for return value
+ //
+ SharedMemoryT* createSHM(const std::string& SHMName, size_t size)
+ {
+ try {
+ return new SharedMemoryT(bi::create_only, SHMName.c_str(),
+ static_cast<unsigned int>(size));
+ } catch (const bi::interprocess_exception&) {
+ }
+
+ return nullptr;
+ }
+
+ // see activateSHM() for return value
+ //
+ SharedMemoryT* openSHM(const std::string& SHMName)
+ {
+ try {
+ return new SharedMemoryT(bi::open_only, SHMName.c_str());
+ } catch (const bi::interprocess_exception&) {
+ }
+
+ return nullptr;
+ }
+
+ // makes the given shm current, returns the name of the previous shm block
+ // if it is now unused and must be destroyed; if the block is still used by
+ // another process, returns empty
+ //
+ // see reassign()
+ //
+ std::optional<std::string> activateSHM(SharedMemoryT* shm, const std::string& SHMName)
+ {
+ std::shared_ptr<SharedMemoryT> oldSHM = m_SHM;
+
+ m_SHM.reset(shm);
+ std::pair<TreeMeta*, SharedMemoryT::size_type> res = m_SHM->find<TreeMeta>("Meta");
+
+ if (res.first == nullptr) {
+ res.first = m_SHM->construct<TreeMeta>("Meta")(createEmpty(),
+ m_SHM->get_segment_manager());
+ if (res.first == nullptr) {
+ USVFS_THROW_EXCEPTION(bi::bad_alloc());
+ }
+ if (m_TreeMeta != nullptr) {
+ copyTree(res.first->tree.get(), m_TreeMeta->tree.get());
+ }
+ }
+
+ increaseRefCount(res.first);
+
+ std::optional<std::string> deadSHMName;
+
+ if (oldSHM.get() != nullptr) {
+ const bool lastUser = unassign(oldSHM, m_TreeMeta);
+ if (lastUser) {
+ deadSHMName = m_SHMName;
+ }
+ }
+
+ m_TreeMeta = res.first;
+ m_SHMName = SHMName;
+
+ return deadSHMName;
+ }
+
+ static std::string followupName(const std::string& currentName)
+ {
+ std::regex pattern(R"exp((.*_)(\d+))exp");
+ std::smatch match;
+ regex_match(currentName, match, pattern);
+
+ if (match.size() != 3) {
+ USVFS_THROW_EXCEPTION(usage_error() << ex_msg("shared memory name invalid"));
+ }
+
+ const int count = boost::lexical_cast<int>(match[2]);
+ return match[1].str() + std::to_string(count + 1);
+ }
+
+ bool unassign(const std::shared_ptr<SharedMemoryT>& shm, TreeMeta* tree)
+ {
+ if (tree == nullptr) {
+ return true;
+ }
+
+ if (decreaseRefCount(tree) == 0) {
+ shm->get_segment_manager()->destroy_ptr(tree);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // re-entrancy: every time a process switches to a new shared memory block, it
+ // will know whether it was the last process to have a handle to it; when that
+ // happens, the block will be destroyed to avoid leaking it
+ //
+ // destroying these blocks is somewhat dangerous: it ends up in boost, which
+ // will try to access the filesystem to see if the name of the shared memory
+ // corresponds to a file on the drive, which can call hooked functions and end
+ // up right back here
+ //
+ // (note that in usvfs, only the shared memory for the log file uses a real
+ // file on the filesystem, see shmlogger.cpp; all the tree stuff uses
+ // anonymous, memory mapped files that live in the Windows pagefile)
+ //
+ // so the old blocks can be destroyed, but only after all the shenanigans with
+ // finding the correct shared memory block are over and `m_TreeMeta` points to
+ // a valid block, so all the names of the dead shared memory blocks are kept
+ // in a vector and deallocated at the very end
+ //
+ void reassign()
+ {
+ // list of all the shared memory blocks that are now unused and can be
+ // destroyed
+ std::vector<std::string> deadSHMNames;
+
+ if (m_TreeMeta->outdated) {
+ // this block was marked as outdated, which should only happen when
+ // another process has ran out of memory and started allocating blocks
+ // with higher numbers
+
+ spdlog::get("usvfs")->info("tree {0} is outdated, looking for another one",
+ m_SHMName);
+
+ if (findNewerBlock(deadSHMNames)) {
+ // the new block was found and activated
+ return;
+ }
+ } else {
+ // this block isn't outdated, so reassign() was called because a bad_alloc
+ // exception was thrown
+ spdlog::get("usvfs")->info(
+ "ran out of memory in tree {0}, will create another one", m_SHMName);
+ }
+
+ // either the block is full or it's outdated, but no higher block was found;
+ // just create a new one
+
+ createNewBlock(deadSHMNames);
+
+ // remove the old shared memory blocks; this can be recursive and call
+ // reassign() again, but it's safe at this point
+ for (const std::string& name : deadSHMNames) {
+ spdlog::get("usvfs")->info("destroying {0}", name);
+ bi::shared_memory_object::remove(name.c_str());
+ }
+ }
+
+ // tries a series of shm names above the current one in the hope of finding
+ // the most recent one
+ //
+ // if the new block was found and activated correctly, returns true
+ //
+ // when findNewerBlock() returns false, this container will either still be
+ // pointing to the same, outdated block as it started with, or it might have
+ // moved up to another outdated block, but it will always be pointing to
+ // a valid block in memory
+ //
+ bool findNewerBlock(std::vector<std::string>& deadSHMNames)
+ {
+ // how many blocks are checked above the current one; it's unlikely there
+ // will ever be more than 15 to 20 blocks allocated, but this is high
+ // because it's really the only way to keep processes connected to the same
+ // shm block, so processes must not fail to find the next one
+ constexpr int Tries = 100;
+
+ std::string nextName = m_SHMName;
+
+ for (int i = 0; i < Tries; ++i) {
+ // the shm name is something like "mod_organizer_3", which becomes
+ // "mod_organizer_4"
+ nextName = followupName(nextName);
+ spdlog::get("usvfs")->info("opening {0}", nextName);
+
+ // open the shm, see if it exists
+ SharedMemoryT* shm = openSHM(nextName);
+
+ if (!shm) {
+ // this is not necessarily an error, another process might have created
+ // a bunch of blocks and destroyed them before this process had a chance
+ // to see them, so just keep going
+ spdlog::get("usvfs")->info("{0} doesn't exist", nextName);
+ continue;
+ }
+
+ spdlog::get("usvfs")->info("{0} exists, activating", nextName);
+ const auto deadSHMName = activateSHM(shm, nextName);
+
+ // if this process was the last user of the previous block, it must be
+ // deallocated, but only after this whole thing is finished, because it
+ // can end up calling reassign() again
+ if (deadSHMName) {
+ spdlog::get("usvfs")->info("will destroy {0}", *deadSHMName);
+ deadSHMNames.push_back(*deadSHMName);
+ }
+
+ // another process might have already created this block, run out of
+ // memory and created more, so make sure to only stop when finding a block
+ // that's not outdated
+ if (m_TreeMeta->outdated) {
+ spdlog::get("usvfs")->info("{0} is also outdated", nextName);
+ } else {
+ // shm opened correctly, activated and not outdated, done
+ spdlog::get("usvfs")->info("{0} not outdated, taking it, size now {1}",
+ nextName, byte_string(m_SHM->get_size()));
+
+ return true;
+ }
+ }
+
+ // this block is outdated, but no other valid block was found above; this
+ // shouldn't happen, but just create a new block to make sure programs can
+ // still run
+
+ spdlog::get("usvfs")->error(
+ "found no existing tree above {0}, will create a new one", m_SHMName);
+
+ return false;
+ }
+
+ // creates a new block and activates it, throws on failure
+ //
+ void createNewBlock(std::vector<std::string>& deadSHMNames)
+ {
+ // the current block is now considered stale, so make sure other processes
+ // are aware of it and try to find the new block
+ //
+ // todo: there really should be some synchronization here, another process
+ // can pick up this flag before the next block has been created
+ m_TreeMeta->outdated = true;
+
+ // the shm name is something like "mod_organizer_3", which becomes
+ // "mod_organizer_4"
+ const std::string nextName = followupName(m_SHMName);
+ spdlog::get("usvfs")->info("creating {0}", nextName);
+
+ SharedMemoryT* shm = createSHM(nextName, m_SHM->get_size() * 2);
+
+ if (!shm) {
+ // this shouldn't happen
+ spdlog::get("usvfs")->error("failed to create {0}", nextName);
+ throw std::exception("cannot create block");
+ }
+
+ spdlog::get("usvfs")->info("{0} created, activating", nextName);
+ const auto deadSHMName = activateSHM(shm, nextName);
+
+ // if this process was the last user of the previous block, it must be
+ // deallocated, but only after this whole thing is finished, because it
+ // can end up calling reassign() again
+ if (deadSHMName) {
+ spdlog::get("usvfs")->info("will destroy {0}", *deadSHMName);
+ deadSHMNames.push_back(*deadSHMName);
+ }
+
+ spdlog::get("usvfs")->info("tree {0} size now {1}", m_SHMName,
+ byte_string(m_SHM->get_size()));
+ }
+};
+
+} // namespace usvfs::shared
diff --git a/libs/usvfs/src/shared/unicodestring.cpp b/libs/usvfs/src/shared/unicodestring.cpp
new file mode 100644
index 0000000..f997e7f
--- /dev/null
+++ b/libs/usvfs/src/shared/unicodestring.cpp
@@ -0,0 +1,77 @@
+/*
+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 "unicodestring.h"
+#include "logging.h"
+#include "stringcast.h"
+#include "stringutils.h"
+
+namespace usvfs
+{
+
+UnicodeString::UnicodeString(LPCWSTR string, size_t length)
+{
+ if (length == std::string::npos) {
+ length = wcslen(string);
+ }
+ m_Buffer.resize(length + 1);
+ memcpy(m_Buffer.data(), string, length * sizeof(wchar_t));
+ update();
+}
+
+UnicodeString::UnicodeString(const std::wstring& string)
+{
+ m_Buffer.resize(string.length() + 1);
+ memcpy(m_Buffer.data(), string.data(), string.length() * sizeof(wchar_t));
+ update();
+}
+
+UnicodeString& UnicodeString::operator=(const std::wstring& string)
+{
+ m_Buffer.resize(string.length() + 1);
+ memcpy(m_Buffer.data(), string.data(), string.length() * sizeof(wchar_t));
+ update();
+ return *this;
+}
+
+UnicodeString& UnicodeString::appendPath(PUNICODE_STRING path)
+{
+ if (path != nullptr && path->Buffer && path->Length) {
+ auto appendAt = size();
+ if (appendAt) {
+ m_Buffer.resize(m_Buffer.size() + path->Length / sizeof(WCHAR) + 1);
+ m_Buffer[appendAt++] = L'\\';
+ } else
+ m_Buffer.resize(path->Length / sizeof(WCHAR) + 1);
+ memcpy(&m_Buffer[appendAt], path->Buffer, path->Length);
+ update();
+ }
+
+ return *this;
+}
+
+void UnicodeString::update()
+{
+ m_Data.Length = static_cast<USHORT>(size() * sizeof(WCHAR));
+ m_Data.MaximumLength = static_cast<USHORT>((m_Buffer.capacity() - 1) * sizeof(WCHAR));
+ m_Data.Buffer = m_Buffer.data();
+}
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/shared/unicodestring.h b/libs/usvfs/src/shared/unicodestring.h
new file mode 100644
index 0000000..77071ad
--- /dev/null
+++ b/libs/usvfs/src/shared/unicodestring.h
@@ -0,0 +1,110 @@
+/*
+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 "ntdll_declarations.h"
+#include "windows_sane.h"
+
+#include "formatters.h"
+
+namespace usvfs
+{
+
+/**
+ * @brief C++ wrapper for the windows UNICODE_STRING structure
+ */
+class UnicodeString
+{
+public:
+ UnicodeString() : m_Buffer(1) { update(); }
+
+ UnicodeString(const UnicodeString& other) : m_Buffer(other.m_Buffer) { update(); }
+
+ UnicodeString(UnicodeString&& other) : m_Buffer(std::move(other.m_Buffer))
+ {
+ update();
+ }
+
+ UnicodeString(const std::wstring& string);
+ UnicodeString(LPCWSTR string, size_t length = std::string::npos);
+
+ UnicodeString& operator=(const std::wstring& string);
+
+ UnicodeString& operator=(const UnicodeString& other)
+ {
+ m_Buffer = other.m_Buffer;
+ update();
+ return *this;
+ }
+
+ UnicodeString& operator=(UnicodeString&& other)
+ {
+ m_Buffer = std::move(other.m_Buffer);
+ update();
+ return *this;
+ }
+
+ /**
+ * @brief convert to a WinNt Api-style unicode string. This is only valid as long
+ * as the string isn't modified
+ */
+ explicit operator PUNICODE_STRING() { return &m_Data; }
+
+ /**
+ * @brief convert to a Win32 Api-style unicode string. This is only valid as long
+ * as the string isn't modified
+ */
+ explicit operator LPCWSTR() const { return m_Data.Buffer; }
+
+ /**
+ * @return length of the string in 16-bit words (not including zero termination)
+ */
+ size_t size() const { return m_Buffer.size() - 1; }
+
+ wchar_t operator[](size_t pos) const { return m_Buffer[pos]; }
+
+ UnicodeString& appendPath(PUNICODE_STRING path);
+
+private:
+ friend struct ::std::formatter<UnicodeString, char>;
+
+ void update();
+
+ UNICODE_STRING m_Data;
+ std::vector<wchar_t> m_Buffer;
+};
+
+} // namespace usvfs
+
+template <>
+struct std::formatter<usvfs::UnicodeString, char>
+ : std::formatter<PCUNICODE_STRING, char>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(const usvfs::UnicodeString& v, FmtContext& ctx) const
+ {
+ if (v.size() == 0) {
+ return std::format_to(ctx.out(), "<empty string>");
+ } else {
+ return std::formatter<PCUNICODE_STRING, char>::format(&v.m_Data, ctx);
+ }
+ }
+};
diff --git a/libs/usvfs/src/shared/wildcard.cpp b/libs/usvfs/src/shared/wildcard.cpp
new file mode 100644
index 0000000..a2efe6d
--- /dev/null
+++ b/libs/usvfs/src/shared/wildcard.cpp
@@ -0,0 +1,194 @@
+/*
+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 "wildcard.h"
+#include "logging.h"
+#include "windows_sane.h"
+
+static bool IsInnerMatch(LPCWSTR pszString, LPCWSTR pszMatch)
+{
+ while (*pszMatch != L'\0') {
+ if ((*pszMatch == L'?') || (*pszMatch == L'>')) {
+ if (!*pszString) {
+ // ? must match exactly one character
+ return false;
+ }
+
+ ++pszString;
+ ++pszMatch;
+ } else if ((*pszMatch == L'*') || (*pszMatch == L'<')) {
+ if (IsInnerMatch(pszString, pszMatch + 1)) {
+ // * may match empty string or we may have matched something already
+ return true;
+ }
+
+ return *pszString && IsInnerMatch(pszString + 1, pszMatch);
+
+ // the rest of the string can't be matched
+ } else {
+ if (CharUpperW(MAKEINTRESOURCEW(MAKELONG(*pszString++, 0))) !=
+ CharUpperW(MAKEINTRESOURCEW(MAKELONG(*pszMatch++, 0)))) {
+ // regular chars compare
+ return false;
+ }
+ }
+ }
+
+ return !*pszString && !*pszMatch;
+}
+
+static LPCSTR InnerMatch(LPCSTR pszString, LPCSTR pszMatch)
+{
+ // We have a special case where string is empty ("") and the mask is "*".
+ // We need to handle this too. So we can't test on !*pszString here.
+ // The loop breaks when the match string is exhausted.
+ while (*pszString != '\0') {
+ // Single wildcard character
+ if ((*pszMatch == '?') || (*pszMatch == '>')) {
+ // can't match directory separator
+ if ((*pszString == '\\') || (*pszString == '/'))
+ return nullptr;
+
+ // Matches any character except empty string
+ if (*pszString == '\0') {
+ // string consumed, part of the pattern is left
+ return pszMatch;
+ }
+
+ // OK next
+ ++pszString;
+ ++pszMatch;
+ } else if ((*pszMatch == '*') || (*pszMatch == '<')) {
+ // * doesn't match directory separators
+ if ((*pszString == '\\') || (*pszString == '/')) {
+ ++pszMatch;
+ continue;
+ }
+
+ // Need to do some tricks.
+
+ // 1. The wildcard * is ignored.
+ // So just an empty string matches. This is done by recursion.
+ // Because we eat one character from the match string, the
+ // recursion will stop.
+ {
+ LPCSTR remainder = InnerMatch(pszString, pszMatch + 1);
+ if (remainder != nullptr) {
+ // we have a match and the * replaces no other character
+ return remainder;
+ }
+ }
+
+ // 2. Chance we eat the next character and try it again, with a
+ // wildcard * match. This is done by recursion. Because we eat
+ // one character from the string, the recursion will stop.
+ if (*pszString != '\0') {
+ LPCSTR remainder = InnerMatch(pszString + 1, pszMatch);
+ if (remainder != nullptr) {
+ return remainder;
+ }
+ }
+
+ // Nothing worked with this wildcard.
+ return nullptr;
+ } else {
+ // Standard compare of 2 chars. Note that *pszSring might be 0
+ // here, but then we never get a match on *pszMask that has always
+ // a value while inside this loop.
+ if (CharUpperA(MAKEINTRESOURCEA(MAKELONG(*pszString++, 0))) !=
+ CharUpperA(MAKEINTRESOURCEA(MAKELONG(*pszMatch++, 0))))
+ return nullptr;
+ }
+ }
+
+ // successful match if the input string was completely consumed
+ if (*pszString == '\0') {
+ while ((*pszMatch == '*') || (*pszMatch == '<')) {
+ ++pszMatch;
+ }
+ return pszMatch;
+ } else {
+ return nullptr;
+ }
+}
+
+namespace usvfs::shared::wildcard
+{
+
+bool Match(LPCWSTR pszString, LPCWSTR pszMatch)
+{
+ if (*pszString == L'.') {
+ // cmd.exe seems to ignore
+ return Match(pszString + 1, pszMatch);
+ } else {
+ size_t len = wcslen(pszMatch);
+ if ((len > 2) && (wcscmp(pszMatch + len - 2, L".*") == 0)) {
+ // cmd.exe seems to completely ignore .* at the end.
+ std::wstring temp(pszMatch, pszMatch + len - 2);
+ return IsInnerMatch(pszString, temp.c_str());
+ }
+ return IsInnerMatch(pszString, pszMatch);
+ }
+}
+
+bool Match(LPCSTR pszString, LPCSTR pszMatch)
+{
+ if (*pszString == '.') {
+ // cmd.exe seems to ignore
+ return Match(pszString + 1, pszMatch);
+ } else {
+ size_t len = strlen(pszMatch);
+ LPCSTR res = nullptr;
+ if ((len > 2) && (strcmp(pszMatch + len - 2, ".*") == 0)) {
+ // cmd.exe seems to completely ignore .* at the end.
+ std::string temp(pszMatch, pszMatch + len - 2);
+ res = InnerMatch(pszString, temp.c_str());
+ } else {
+ res = InnerMatch(pszString, pszMatch);
+ }
+ return ((res != nullptr) && (*res == '\0'));
+ }
+}
+
+LPCSTR PartialMatch(LPCSTR pszString, LPCSTR pszMatch)
+{
+ if (*pszString == '.') {
+ // cmd.exe seems to ignore dots at the start
+ return PartialMatch(pszString + 1, pszMatch);
+ } else {
+ size_t len = strlen(pszMatch);
+ if ((len > 2) && (strcmp(pszMatch + len - 2, ".*") == 0)) {
+ // in cmd.exe there seems to be no difference between <something>* and
+ // <something>*.*
+ std::string temp(pszMatch, pszMatch + len - 2);
+ LPCSTR pos = InnerMatch(pszString, temp.c_str());
+ if (pos != nullptr) {
+ if (*pos == '\0') {
+ return pszMatch + strlen(pszMatch);
+ } else {
+ return pszMatch + (pos - temp.c_str());
+ }
+ }
+ }
+ return InnerMatch(pszString, pszMatch);
+ }
+}
+
+} // namespace usvfs::shared::wildcard
diff --git a/libs/usvfs/src/shared/wildcard.h b/libs/usvfs/src/shared/wildcard.h
new file mode 100644
index 0000000..bf5b4a3
--- /dev/null
+++ b/libs/usvfs/src/shared/wildcard.h
@@ -0,0 +1,67 @@
+/*
+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/>.
+*/
+/// Wildcard matching code
+/// by Martin Richter
+/// licensed under The Code Project Open License (CPOL)
+
+#pragma once
+
+#include "windows_sane.h"
+
+namespace usvfs::shared::wildcard
+{
+
+/**
+ * @brief match string to wildcard windows-style
+ * @param pszString Input string to match
+ * @param pszMatch Match mask that may contain wildcards like ? and *
+ * @note A ? sign matches any character, except an empty string.
+ * @note A * sign matches any string inclusive an empty string.
+ * @note Characters are compared caseless.
+ * @return true if the string matches the pattern
+ */
+bool Match(LPCWSTR pszString, LPCWSTR pszMatch);
+
+/**
+ * @brief match string to wildcard windows-style
+ * @param pszString Input string to match
+ * @param pszMatch Match mask that may contain wildcards like ? and *
+ * @note A ? sign matches any character, except an empty string.
+ * @note A * sign matches any string inclusive an empty string.
+ * @note Characters are compared caseless.
+ * @return true if the string matches the pattern
+ */
+bool Match(LPCSTR pszString, LPCSTR pszMatch);
+
+/**
+ * @brief match string to wildcard windows-style
+ * @param pszString Input string to match
+ * @param pszMatch Match mask that may contain wildcards like ? and *
+ * @note A ? sign matches any character, except an empty string.
+ * @note A * sign matches any string inclusive an empty string.
+ * @note Characters are compared caseless.
+ * @return the "not-consumed" remainder of the pattern. If this points to a
+ * zero terminator, this was a full match.
+ * Returns nullptr if no match is possible
+ */
+LPCSTR PartialMatch(LPCSTR pszString, LPCSTR pszMatch);
+
+} // namespace usvfs::shared::wildcard
diff --git a/libs/usvfs/src/shared/winapi.cpp b/libs/usvfs/src/shared/winapi.cpp
new file mode 100644
index 0000000..e28a13c
--- /dev/null
+++ b/libs/usvfs/src/shared/winapi.cpp
@@ -0,0 +1,490 @@
+/*
+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 "winapi.h"
+#include "exceptionex.h"
+#include "logging.h"
+#include "ntdll_declarations.h"
+#include "stringcast.h"
+#include "stringutils.h"
+#include "unicodestring.h"
+
+namespace winapi::ansi
+{
+
+std::string getModuleFileName(HMODULE module, HANDLE process)
+{
+ std::wstring result = wide::getModuleFileName(module, process);
+ return usvfs::shared::string_cast<std::string>(result);
+}
+
+std::string getCurrentDirectory()
+{
+ std::string result;
+ DWORD required = GetCurrentDirectoryA(0, nullptr);
+ if (required == 0UL) {
+ throw usvfs::shared::windows_error("failed to determine current working directory");
+ }
+ result.resize(required);
+ GetCurrentDirectoryA(required, &result[0]);
+ result.resize(required - 1);
+ return result;
+}
+
+std::pair<std::string, std::string> getFullPathName(LPCSTR fileName)
+{
+ static const int INIT_SIZE = 128;
+ std::string result;
+ result.resize(INIT_SIZE);
+ LPSTR filePart = nullptr;
+ DWORD requiredSize = GetFullPathNameA(fileName, INIT_SIZE, &result[0], &filePart);
+ if (requiredSize >= INIT_SIZE) {
+ result.resize(requiredSize);
+ GetFullPathNameA(fileName, requiredSize, &result[0], &filePart);
+ }
+ if (requiredSize != 0UL) {
+ return std::make_pair(result, std::string(filePart != nullptr ? filePart : ""));
+ } else {
+ return make_pair(result, std::string());
+ }
+}
+
+} // namespace winapi::ansi
+
+namespace winapi::wide
+{
+
+std::wstring getModuleFileName(HMODULE module, HANDLE process)
+{
+ std::wstring result;
+ result.resize(64);
+ DWORD rc = 0UL;
+
+ while ((rc = (process == INVALID_HANDLE_VALUE)
+ ? ::GetModuleFileNameW(module, &result[0],
+ static_cast<DWORD>(result.size()))
+ : ::GetModuleFileNameExW(process, module, &result[0],
+ static_cast<DWORD>(result.size()))) ==
+ result.size()) {
+ result.resize(result.size() * 2);
+ }
+
+ if (rc == 0UL) {
+ if (::GetLastError() == ERROR_PARTIAL_COPY) {
+#if BOOST_ARCH_X86_64
+ return L"unknown (32-bit process)";
+#else
+ return L"unknown (64-bit process)";
+#endif
+ } else {
+ throw usvfs::shared::windows_error("failed to retrieve module file name");
+ }
+ }
+
+ result.resize(rc);
+
+ return result;
+}
+
+std::pair<std::wstring, std::wstring> getFullPathName(LPCWSTR fileName)
+{
+ wchar_t buf1[MAX_PATH];
+ std::vector<wchar_t> buf2;
+ wchar_t* result = buf1;
+ LPWSTR filePart = nullptr;
+ DWORD requiredSize = GetFullPathNameW(fileName, MAX_PATH, result, &filePart);
+ if (requiredSize >= MAX_PATH) {
+ buf2.resize(requiredSize);
+ result = &buf2[0];
+ requiredSize = GetFullPathNameW(fileName, requiredSize, result, &filePart);
+ }
+ return make_pair(std::wstring(result, requiredSize),
+ std::wstring((requiredSize && filePart) ? filePart : L""));
+}
+
+std::wstring getCurrentDirectory()
+{
+ // really great api this (::GetCurrentDirectoryW)
+ // - if it succeeds, returns size in characters WITHOUT zero termination
+ // - if it fails due to buffer too small, returns size in characters WITH zero
+ // termination
+ // - if it fails for other reasons, returns 0
+ std::wstring result;
+ DWORD required = GetCurrentDirectoryW(0, nullptr);
+ if (required == 0UL) {
+ throw usvfs::shared::windows_error("failed to determine current working directory");
+ }
+ result.resize(required);
+ GetCurrentDirectoryW(required, &result[0]);
+ result.resize(required - 1);
+ return result;
+}
+
+std::wstring getKnownFolderPath(REFKNOWNFOLDERID folderID)
+{
+ PWSTR writablePath;
+
+ ::SHGetKnownFolderPath(folderID, 0, nullptr, &writablePath);
+
+ ON_BLOCK_EXIT([writablePath]() {
+ ::CoTaskMemFree(writablePath);
+ });
+
+ return std::wstring(writablePath);
+}
+
+} // namespace winapi::wide
+
+namespace winapi::ex
+{
+
+std::pair<uintptr_t, uintptr_t> getSectionRange(HANDLE moduleHandle)
+{
+ std::pair<uintptr_t, uintptr_t> result;
+ bool found = false;
+ uintptr_t exeModule = reinterpret_cast<uintptr_t>(moduleHandle);
+ if (exeModule == 0) {
+ throw std::runtime_error("failed to determine address range of executable");
+ }
+
+ std::pair<uintptr_t, uintptr_t> totalRange{UINT_MAX, 0};
+
+ PIMAGE_DOS_HEADER dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(exeModule);
+ PIMAGE_NT_HEADERS ntHeader =
+ reinterpret_cast<PIMAGE_NT_HEADERS>(exeModule + dosHeader->e_lfanew);
+ PIMAGE_SECTION_HEADER sectionHeader =
+ reinterpret_cast<PIMAGE_SECTION_HEADER>(ntHeader + 1);
+ for (int i = 0; i < ntHeader->FileHeader.NumberOfSections && !found; ++i) {
+ if (memcmp(sectionHeader->Name, ".text", 5) == 0) {
+ result.first = exeModule + sectionHeader->VirtualAddress;
+ result.second = result.first + sectionHeader->Misc.VirtualSize;
+ found = true;
+ } else {
+ uintptr_t start = exeModule + sectionHeader->VirtualAddress;
+ totalRange.first = std::min(totalRange.first, start);
+ totalRange.second = std::max<uintptr_t>(totalRange.second,
+ start + sectionHeader->Misc.VirtualSize);
+ }
+ ++sectionHeader;
+ }
+
+ if (!found) {
+ return totalRange;
+ }
+
+ return result;
+}
+
+OSVersion getOSVersion()
+{
+ RTL_OSVERSIONINFOEXW versionInfo;
+ ZeroMemory(&versionInfo, sizeof(RTL_OSVERSIONINFOEXW));
+ versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);
+ RtlGetVersion((PRTL_OSVERSIONINFOW)&versionInfo);
+
+ OSVersion result;
+ result.major = versionInfo.dwMajorVersion;
+ result.minor = versionInfo.dwMinorVersion;
+ result.build = versionInfo.dwBuildNumber;
+ result.platformid = versionInfo.dwPlatformId;
+ result.servicpack =
+ versionInfo.wServicePackMajor << 16 | versionInfo.wServicePackMinor;
+ return result;
+}
+
+} // namespace winapi::ex
+
+namespace winapi::ex::ansi
+{
+
+std::string errorString(DWORD errorCode)
+{
+ std::ostringstream finalMessage;
+
+ LPSTR buffer = nullptr;
+
+ DWORD currentErrorCode = GetLastError();
+
+ errorCode =
+ errorCode != std::numeric_limits<DWORD>::max() ? errorCode : currentErrorCode;
+
+ // TODO: the message is not english?
+ if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
+ nullptr, errorCode,
+ 0 //, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)
+ ,
+ (LPSTR)&buffer, 0, nullptr) == 0) {
+ finalMessage << "(unknown error [" << errorCode << "])";
+ } else {
+ if (buffer != nullptr) {
+ size_t end = strlen(buffer) - 1;
+ while ((buffer[end] == '\n') || (buffer[end] == '\r')) {
+ buffer[end--] = '\0';
+ }
+ finalMessage << "(" << buffer << " [" << errorCode << "])";
+ LocalFree(buffer); // allocated by FormatMessage
+ }
+ }
+
+ SetLastError(currentErrorCode); // restore error code because FormatMessage might
+ // have modified it
+ return finalMessage.str();
+}
+
+std::string toString(const FILETIME& time)
+{
+ SYSTEMTIME temp;
+ FileTimeToSystemTime(&time, &temp);
+ std::ostringstream stream;
+ stream << temp.wYear << "-" << temp.wMonth << "-" << temp.wDay << temp.wHour << ":"
+ << temp.wMinute << ":" << temp.wSecond;
+ return stream.str();
+}
+
+LPCSTR GetBaseName(LPCSTR string)
+{
+ LPCSTR result = string + strlen(string) - 1;
+ while (result > string) {
+ if ((*result == '\\') || (*result == '/')) {
+ ++result;
+ break;
+ } else {
+ --result;
+ }
+ }
+ return result;
+}
+
+} // namespace winapi::ex::ansi
+
+namespace winapi::ex::wide
+{
+
+bool fileExists(LPCWSTR fileName, bool* isDirectory)
+{
+ DWORD attrib = GetFileAttributesW(fileName);
+
+ if (attrib == INVALID_FILE_ATTRIBUTES) {
+ return false;
+ } else {
+ if (isDirectory != nullptr) {
+ *isDirectory = (attrib & FILE_ATTRIBUTE_DIRECTORY) != 0;
+ }
+ return true;
+ }
+}
+
+std::wstring errorString(DWORD errorCode)
+{
+ std::wostringstream finalMessage;
+
+ LPWSTR buffer = nullptr;
+
+ DWORD currentErrorCode = GetLastError();
+
+ errorCode =
+ errorCode != std::numeric_limits<DWORD>::max() ? errorCode : currentErrorCode;
+
+ // TODO: the message is not english?
+ if (FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
+ nullptr, errorCode,
+ 0 //, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)
+ ,
+ (LPWSTR)&buffer, 0, nullptr) == 0) {
+ finalMessage << L"(unknown error [" << errorCode << "])";
+ } else {
+ if (buffer != nullptr) {
+ size_t end = wcslen(buffer) - 1;
+ while ((buffer[end] == L'\n') || (buffer[end] == L'\r')) {
+ buffer[end--] = L'\0';
+ }
+ finalMessage << L"(" << buffer << L" [" << errorCode << L"])";
+ LocalFree(buffer); // allocated by FormatMessage
+ }
+ }
+
+ SetLastError(currentErrorCode); // restore error code because FormatMessage might
+ // have modified it
+ return finalMessage.str();
+}
+
+std::wstring toString(const FILETIME& time)
+{
+ SYSTEMTIME temp;
+ FileTimeToSystemTime(&time, &temp);
+ std::wostringstream stream;
+ stream << temp.wYear << "-" << temp.wMonth << "-" << temp.wDay << temp.wHour << ":"
+ << temp.wMinute << ":" << temp.wSecond;
+ return stream.str();
+}
+
+LPCWSTR GetBaseName(LPCWSTR string)
+{
+ LPCWSTR result;
+ if ((string == nullptr) || (string[0] == L'\0')) {
+ result = string;
+ } else {
+ result = string + wcslen(string) - 1;
+ }
+
+ while (result > string) {
+ if ((*result == L'\\') || (*result == L'/')) {
+ ++result;
+ break;
+ } else {
+ --result;
+ }
+ }
+ return result;
+}
+
+LPWSTR GetBaseName(LPWSTR path)
+{
+ LPCWSTR result = GetBaseName(static_cast<LPCWSTR>(path));
+ return const_cast<LPWSTR>(result);
+}
+
+std::wstring getSectionName(PVOID addressIn, HANDLE process)
+{
+ if (process == nullptr) {
+ process = GetCurrentProcess();
+ }
+ HMODULE modules[1024];
+ intptr_t address = reinterpret_cast<intptr_t>(addressIn);
+ DWORD required;
+ if (::EnumProcessModules(process, modules, sizeof(modules), &required)) {
+ for (DWORD i = 0; i < (std::min<DWORD>(1024UL, required) / sizeof(HMODULE)); ++i) {
+ std::pair<intptr_t, intptr_t> range = getSectionRange(modules[i]);
+ if ((address > range.first) && (address < range.second)) {
+ try {
+ return winapi::wide::getModuleFileName(modules[i], process);
+ } catch (const std::exception&) {
+ return std::wstring(L"unknown");
+ }
+ }
+ }
+ }
+ return std::wstring(L"unknown");
+}
+
+std::vector<FileResult> quickFindFiles(LPCWSTR directoryName, LPCWSTR pattern)
+{
+ std::vector<FileResult> result;
+
+ static const unsigned int BUFFER_SIZE = 1024;
+
+ HANDLE hdl = CreateFileW(directoryName, GENERIC_READ,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
+
+ ON_BLOCK_EXIT([hdl]() {
+ CloseHandle(hdl);
+ });
+
+ uint8_t buffer[BUFFER_SIZE];
+
+ NTSTATUS res = STATUS_SUCCESS; // status success
+ while (res == STATUS_SUCCESS) {
+ IO_STATUS_BLOCK status;
+
+ res = NtQueryDirectoryFile(
+ hdl, nullptr, nullptr, nullptr, &status, buffer, BUFFER_SIZE,
+ FileFullDirectoryInformation, FALSE,
+ static_cast<PUNICODE_STRING>(usvfs::UnicodeString(pattern)), FALSE);
+ if (res == STATUS_SUCCESS) {
+ FILE_FULL_DIR_INFORMATION* info =
+ reinterpret_cast<FILE_FULL_DIR_INFORMATION*>(buffer);
+ void* endPos = buffer + status.Information;
+ while (info < endPos) {
+ FileResult file;
+ file.fileName =
+ std::wstring(info->FileName, info->FileNameLength / sizeof(wchar_t));
+ file.attributes = info->FileAttributes;
+
+ result.push_back(file);
+ if (info->NextEntryOffset == 0) {
+ break;
+ } else {
+ info = reinterpret_cast<FILE_FULL_DIR_INFORMATION*>(
+ reinterpret_cast<uint8_t*>(info) + info->NextEntryOffset);
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+bool createPath(boost::filesystem::path path, LPSECURITY_ATTRIBUTES securityAttributes)
+{
+ // sanity and guaranteed recursion end:
+ if (!path.has_relative_path())
+ throw usvfs::shared::windows_error(
+ "createPath() 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 usvfs::shared::windows_error("createPath() called on a file: " +
+ path.string());
+ }
+ if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND)
+ throw usvfs::shared::windows_error(
+ "createPath() GetFileAttributesW failed on: " + path.string(), err);
+
+ if (err != ERROR_FILE_NOT_FOUND) // ERROR_FILE_NOT_FOUND means parent directory
+ // already exists
+ createPath(path.parent_path(),
+ securityAttributes); // otherwise create parent directory (recursively)
+
+ BOOL res = CreateDirectoryW(path.c_str(), securityAttributes);
+ if (!res) {
+ err = GetLastError();
+ throw usvfs::shared::windows_error(
+ "createPath() CreateDirectoryW failed on: " + path.string(), err);
+ }
+ return true;
+}
+
+std::wstring getWindowsBuildLab(bool ex)
+{
+ HKEY hKey = nullptr;
+ auto res = RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ LR"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", 0,
+ KEY_READ, &hKey);
+ if (res != ERROR_SUCCESS || !hKey)
+ return L"Opening HKLM Windows NT\\CurrentVersion failed?!";
+ WCHAR buf[200];
+ DWORD size = static_cast<DWORD>(sizeof(buf));
+ res = RegQueryValueExW(hKey, ex ? L"BuildLabEx" : L"BuildLab", NULL, NULL,
+ reinterpret_cast<LPBYTE>(buf), &size);
+ if (res != ERROR_SUCCESS || size > sizeof(buf))
+ return ex ? L"BuildLabEx reg value not found?!" : L"BuildLab reg value not found?!";
+ size /= sizeof(buf[0]);
+ if (size && !buf[size - 1])
+ --size;
+ return std::wstring(buf, size);
+}
+
+} // namespace winapi::ex::wide
diff --git a/libs/usvfs/src/shared/winapi.h b/libs/usvfs/src/shared/winapi.h
new file mode 100644
index 0000000..f94eff6
--- /dev/null
+++ b/libs/usvfs/src/shared/winapi.h
@@ -0,0 +1,610 @@
+/*
+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 "logging.h"
+#include "stringcast.h"
+#include "windows_sane.h"
+#include <ShlObj.h>
+
+#define ALIAS(alias, original) \
+ template <typename... Args> \
+ auto alias(Args&&... args) -> decltype(original(std::forward<Args>(args)...)) \
+ { \
+ return original(std::forward<Args>(args)...); \
+ }
+
+#define ALIAST(alias, original) \
+ template <typename T, typename... Args> \
+ auto alias<T>(Args && ... args) \
+ -> decltype(original<T>(std::forward<Args>(args)...)) \
+ { \
+ return original<T>(std::forward<Args>(args)...); \
+ }
+
+namespace winapi
+{
+
+struct parameter_error : public std::runtime_error
+{
+ parameter_error(const std::string& msg) : runtime_error(msg) {}
+};
+
+} // namespace winapi
+
+namespace winapi::process
+{
+
+/**
+ * @brief result of process creation
+ */
+struct Result
+{
+ Result()
+ {
+ ::ZeroMemory(&processInfo, sizeof(PROCESS_INFORMATION));
+ ::ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
+ startupInfo.cb = sizeof(STARTUPINFO);
+ }
+
+ Result(Result&& reference)
+ : valid(reference.valid), startupInfo(reference.startupInfo),
+ processInfo(reference.processInfo), errorCode(reference.errorCode)
+ {
+ reference.valid = false;
+ }
+
+ ~Result()
+ {
+ if (valid) {
+ CloseHandle(processInfo.hProcess);
+ CloseHandle(processInfo.hThread);
+ }
+
+ if (stdoutPipe != INVALID_HANDLE_VALUE) {
+ CloseHandle(stdoutPipe);
+ }
+ }
+
+ Result(const Result&) = delete;
+
+ size_t readStdout(std::vector<uint8_t>& buffer, bool& eof)
+ {
+ if (stdoutPipe != INVALID_HANDLE_VALUE) {
+ DWORD read;
+ BOOL res = ReadFile(stdoutPipe, &buffer[0], static_cast<DWORD>(buffer.size()),
+ &read, nullptr);
+ eof = (res == TRUE) && (read == 0);
+ return static_cast<size_t>(read);
+ } else {
+ eof = true;
+ return 0;
+ }
+ }
+
+ bool valid{false};
+ STARTUPINFO startupInfo;
+ PROCESS_INFORMATION processInfo;
+ DWORD errorCode{0UL};
+
+ HANDLE stdoutPipe{INVALID_HANDLE_VALUE};
+};
+
+/**
+ * @brief internal class to handle process creation with named parameters.
+ */
+template <typename CharT>
+class _Create
+{
+public:
+ _Create(const std::basic_string<CharT>& binaryName);
+ _Create(const _Create<CharT>& reference) = delete;
+ _Create<CharT>& operator=(const _Create<CharT>& reference) = delete;
+
+ _Create(_Create<CharT>&& reference)
+ : m_CurrentDirectory(std::move(reference.m_CurrentDirectory)),
+ m_ProcessAttributes(reference.m_ProcessAttributes),
+ m_ThreadAttributes(reference.m_ThreadAttributes),
+ m_InheritHandles(reference.m_InheritHandles),
+ m_CreationFlags(std::move(reference.m_CreationFlags)),
+ m_Executed(reference.m_Executed)
+ {
+ // stringstream should be moveable but it seems it isn't on mingw
+ m_CommandLine << reference.m_CommandLine.rdbuf();
+ }
+
+ /// named parameter "argument". May be called repeatedly. This is
+ /// directly appended to the command line with a separating space. No
+ /// quoting happens
+ template <typename ArgT>
+ _Create& argument(const ArgT& argin)
+ {
+ m_CommandLine << " " << argin;
+ return *this;
+ }
+
+ template <typename ArgT>
+ _Create& arg(const ArgT& argin)
+ {
+ return this->argument(argin);
+ }
+
+ template <typename IterT>
+ _Create& arguments(IterT begin, IterT end)
+ {
+ for (; begin != end; ++begin) {
+ m_CommandLine << " " << *begin;
+ }
+
+ return *this;
+ }
+
+ /// @brief set the working directory for the process
+ _Create& workingDirectory(const std::basic_string<CharT>& path);
+
+ /// @brief set process attributes
+ _Create& processAttributes(SECURITY_ATTRIBUTES* attributes);
+
+ /// @brief set thread attributes
+ _Create& threadAttributes(SECURITY_ATTRIBUTES* attributes);
+
+ /// @brief activate inheriting handles
+ _Create& inheritHandles();
+
+ /// @brief have the process start suspended
+ _Create& suspended();
+
+ /// @brief set the process up to output stout to a pipe which can be
+ /// retrieved through the result object
+ _Create& stdoutPipe();
+
+ /// @brief end the named parameter cascade and create the process
+ Result operator()()
+ {
+ m_CommandLine.seekp(0, std::ios::end);
+ unsigned int length = static_cast<unsigned int>(m_CommandLine.tellp());
+ std::unique_ptr<CharT[]> clBuffer(new CharT[length + 1]);
+ memset(clBuffer.get(), 0, (length + 1) * sizeof(CharT));
+ memcpy(clBuffer.get(), m_CommandLine.str().c_str(), length * sizeof(CharT));
+ Result result;
+
+ if (m_StdoutPipe) {
+ result.stdoutPipe = setupPipe(result.startupInfo.hStdOutput);
+ result.startupInfo.dwFlags |= STARTF_USESTDHANDLES;
+ }
+
+ result.valid =
+ createProcessInt(nullptr, clBuffer.get(), m_ProcessAttributes,
+ m_ThreadAttributes, m_InheritHandles, m_CreationFlags, nullptr,
+ m_CurrentDirectory.length() > 0 ? m_CurrentDirectory.c_str()
+ : nullptr,
+ &result.startupInfo, &result.processInfo) == TRUE;
+
+ if (m_Stdout != INVALID_HANDLE_VALUE) {
+ // got to close the write end of pipes
+ CloseHandle(result.startupInfo.hStdOutput);
+ }
+
+ if (result.valid) {
+ result.errorCode = NOERROR;
+ } else {
+ result.errorCode = GetLastError();
+ }
+ return result;
+ }
+
+private:
+ static BOOL createProcessInt(LPCWSTR lpApplicationName, LPWSTR lpCommandLine,
+ SECURITY_ATTRIBUTES* lpProcessAttributes,
+ SECURITY_ATTRIBUTES* lpThreadAttributes,
+ BOOL bInheritHandles, DWORD dwCreationFlags,
+ LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory,
+ LPSTARTUPINFOW lpStartupInfo,
+ LPPROCESS_INFORMATION lpProcessInformation)
+ {
+ return ::CreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes,
+ lpThreadAttributes, bInheritHandles, dwCreationFlags,
+ lpEnvironment, lpCurrentDirectory, lpStartupInfo,
+ lpProcessInformation);
+ }
+
+ static BOOL createProcessInt(LPCSTR lpApplicationName, LPSTR lpCommandLine,
+ SECURITY_ATTRIBUTES* lpProcessAttributes,
+ SECURITY_ATTRIBUTES* lpThreadAttributes,
+ BOOL bInheritHandles, DWORD dwCreationFlags,
+ LPVOID lpEnvironment, LPCSTR lpCurrentDirectory,
+ LPSTARTUPINFOW lpStartupInfo,
+ LPPROCESS_INFORMATION lpProcessInformation)
+ {
+ std::wstring executable;
+ if (lpApplicationName != nullptr) {
+ executable = usvfs::shared::string_cast<std::wstring>(lpApplicationName);
+ }
+
+ std::wstring cmdline;
+ if (lpCommandLine != nullptr) {
+ cmdline = usvfs::shared::string_cast<std::wstring>(lpCommandLine);
+ }
+
+ std::wstring cwd;
+ if (lpCurrentDirectory != nullptr) {
+ cwd = usvfs::shared::string_cast<std::wstring>(lpCurrentDirectory);
+ }
+
+ return ::CreateProcessW(lpApplicationName != nullptr ? executable.c_str() : nullptr,
+ lpCommandLine != nullptr ? &cmdline[0] : nullptr,
+ lpProcessAttributes, lpThreadAttributes, bInheritHandles,
+ dwCreationFlags, lpEnvironment,
+ lpCurrentDirectory != nullptr ? cwd.c_str() : nullptr,
+ lpStartupInfo, lpProcessInformation);
+ }
+
+ HANDLE setupPipe(HANDLE& childHandle)
+ {
+ SECURITY_ATTRIBUTES attr;
+ attr.nLength = sizeof(SECURITY_ATTRIBUTES);
+ attr.bInheritHandle = TRUE;
+ attr.lpSecurityDescriptor = nullptr;
+
+ HANDLE pipe[2];
+
+ CreatePipe(&pipe[0], &pipe[1], &attr, 0);
+ SetHandleInformation(pipe[0], HANDLE_FLAG_INHERIT, 0);
+
+ childHandle = pipe[1];
+
+ return pipe[0];
+ }
+
+private:
+ std::basic_stringstream<CharT> m_CommandLine;
+ std::basic_string<CharT> m_CurrentDirectory{};
+ SECURITY_ATTRIBUTES* m_ProcessAttributes{nullptr};
+ SECURITY_ATTRIBUTES* m_ThreadAttributes{nullptr};
+ BOOL m_InheritHandles{false};
+ DWORD m_CreationFlags{0UL};
+ bool m_Executed{false};
+ bool m_StdoutPipe{false};
+
+ HANDLE m_Stdout{INVALID_HANDLE_VALUE};
+};
+
+} // namespace winapi::process
+
+namespace winapi::file
+{
+/**
+ * @brief internal class to handle file creation (opening) with named
+ * parameters.
+ */
+template <typename CharT, DWORD DefaultDisposition>
+class _Create
+{
+public:
+ _Create(const std::basic_string<CharT>& fileName) : m_FileName(fileName) {}
+
+ _Create& access(DWORD desiredAccess)
+ {
+ m_DesiredAccess = desiredAccess;
+ return *this;
+ }
+
+ _Create& share(DWORD shareMode)
+ {
+ m_ShareMode = shareMode;
+ return *this;
+ }
+
+ _Create& createAlways()
+ {
+ m_CreationDisposition = CREATE_ALWAYS;
+ return *this;
+ }
+
+ _Create& openAlways()
+ {
+ m_CreationDisposition = OPEN_ALWAYS;
+ return *this;
+ }
+
+ _Create& security(SECURITY_ATTRIBUTES* attributes)
+ {
+ m_SecurityAttributes = attributes;
+ return *this;
+ }
+
+ _Create& templateFile(HANDLE templateFile)
+ {
+ m_Template = templateFile;
+ return *this;
+ }
+
+ /// @brief end the named parameter cascade and open the file
+ HANDLE operator()()
+ {
+ return callDelegate(
+ std::integral_constant<bool, sizeof(CharT) == sizeof(wchar_t)>());
+ }
+
+private:
+ HANDLE callDelegate(std::true_type)
+ {
+ return ::CreateFileW(m_FileName.c_str(), m_DesiredAccess, m_ShareMode,
+ m_SecurityAttributes, m_CreationDisposition, m_Flags,
+ m_Template);
+ }
+
+ HANDLE callDelegate(std::false_type)
+ {
+ return ::CreateFileA(m_FileName.c_str(), m_DesiredAccess, m_ShareMode,
+ m_SecurityAttributes, m_CreationDisposition, m_Flags,
+ m_Template);
+ }
+
+private:
+ std::basic_string<CharT> m_FileName;
+ DWORD m_DesiredAccess{GENERIC_ALL};
+ DWORD m_ShareMode{0UL};
+ DWORD m_CreationDisposition{DefaultDisposition};
+ DWORD m_Flags{FILE_ATTRIBUTE_NORMAL};
+ HANDLE m_Template{nullptr};
+ SECURITY_ATTRIBUTES* m_SecurityAttributes{nullptr};
+};
+
+} // namespace winapi::file
+
+namespace winapi::ansi
+{
+
+std::string getModuleFileName(HMODULE module, HANDLE process = INVALID_HANDLE_VALUE);
+std::pair<std::string, std::string> getFullPathName(LPCSTR fileName);
+std::string getCurrentDirectory();
+typedef process::_Create<char> createProcess;
+typedef file::_Create<char, CREATE_NEW> createFile;
+typedef file::_Create<char, OPEN_EXISTING> openFile;
+
+} // namespace winapi::ansi
+
+namespace winapi::wide
+{
+
+std::wstring getModuleFileName(HMODULE module, HANDLE process = INVALID_HANDLE_VALUE);
+std::pair<std::wstring, std::wstring> getFullPathName(LPCWSTR fileName);
+std::wstring getCurrentDirectory();
+std::wstring getKnownFolderPath(REFKNOWNFOLDERID folderID);
+
+typedef process::_Create<wchar_t> createProcess;
+typedef file::_Create<wchar_t, CREATE_NEW> createFile;
+typedef file::_Create<wchar_t, OPEN_EXISTING> openFile;
+
+} // namespace winapi::wide
+
+/**
+ * useful convenience functions close to the api
+ */
+namespace winapi::ex
+{
+
+/**
+ * @brief retrieve the address range covering the code section of a module
+ * @param moduleHandle handle to the module
+ * @return start and end address of the code section
+ * @note the code section can only be identified if it has the standardized section name
+ * ".text" Otherwise the whole address range of all sections in the module is returned.
+ * This happens for compressed exectuables for example
+ */
+std::pair<uintptr_t, uintptr_t> getSectionRange(HANDLE moduleHandle);
+
+struct OSVersion
+{
+ DWORD major;
+ DWORD minor;
+ DWORD build;
+ DWORD platformid;
+ DWORD servicpack;
+};
+
+OSVersion getOSVersion();
+
+} // namespace winapi::ex
+
+namespace winapi::ex::ansi
+{
+
+/**
+ * @brief retrieve an error string for a windows error message
+ * @param errorCode the error code to look up. If this is left at the default,
+ * ::GetLastError is used
+ * @return string representation of the error. Currently this is localized
+ */
+std::string errorString(DWORD errorCode = std::numeric_limits<DWORD>::max());
+
+/**
+ * @brief convert filetime to string
+ * @param time time to convert
+ * @return a string representation (currently only supports utc and iso format with
+ * second precision)
+ */
+std::string toString(const FILETIME& time);
+
+/**
+ * @brief find file name in a windows file path
+ * @param path the path to search in
+ * @return the file name of the path or an empty string if the path ends on
+ * a slash
+ * @note this function doesn't access the file system so it doesn't depend
+ * on whether the file actually exists. This also means it can't
+ * determine if a path that doesn't end on a slash refers to a file or
+ * directory
+ * @note the return value is a pointer into the same buffer, no copy is
+ * created
+ */
+LPCSTR GetBaseName(LPCSTR string);
+
+} // namespace winapi::ex::ansi
+
+namespace winapi::ex::wide
+{
+/**
+ * retrieve the name of the binary section containing the specified address
+ * @param address the address to test
+ * @param process the process for which to retrieve the section. If this is
+ * nullptr, the current process is analized.
+ * @return name of the section or "unknown" if no matching section was found
+ */
+std::wstring getSectionName(PVOID address, HANDLE process = nullptr);
+
+/**
+ * @brief test if a file exists
+ * @param path path to check
+ * @param isDirectory (optional) if this isn't null, it will be set to true if the path
+ * specifies a directory, false otherwise
+ * @return true if the file (or directory) exists.
+ */
+bool fileExists(LPCWSTR fileName, bool* isDirectory = nullptr);
+
+/**
+ * @brief retrieve an error string for a windows error message
+ * @param errorCode the error code to look up. If this is left at the default,
+ * ::GetLastError is used
+ * @return string representation of the error. Currently this is localized
+ */
+std::wstring errorString(DWORD errorCode = std::numeric_limits<DWORD>::max());
+
+/**
+ * @brief convert filetime to string
+ * @param time time to convert
+ * @return a string representation (currently only supports utc and iso format with
+ * second precision)
+ */
+std::wstring toString(const FILETIME& time);
+
+/**
+ * @brief find file name in a windows file path
+ * @param path the path to search in
+ * @return the file name of the path or an empty string if the path ends on
+ * a slash
+ * @note this function doesn't access the file system so it doesn't depend
+ * on whether the file actually exists. This also means it can't
+ * determine if a path that doesn't end on a slash refers to a file or
+ * directory
+ * @note the return value is a pointer into the same buffer, no copy is
+ * created
+ */
+LPCWSTR GetBaseName(LPCWSTR path);
+
+/**
+ * @see const-variant of this function
+ */
+LPWSTR GetBaseName(LPWSTR path);
+
+struct FileResult
+{
+ std::wstring fileName;
+ ULONG attributes;
+};
+
+/**
+ * @brief a quick function to find all files in a directory or files following a
+ * pattern. This uses NtQueryDirectoryFile api internally so it should be faster than
+ * the usual FindFirstFile/FindNextFile pattern
+ * @param directoryName name of the directory to search in
+ * @param pattern name pattern that needs to match
+ * @return the list of files found
+ */
+std::vector<FileResult> quickFindFiles(LPCWSTR directoryName, LPCWSTR pattern);
+
+/**
+ * @brief create the specified directory including all intermediate
+ * directories
+ * @param path the path to create
+ * @param securityAttributes the security attributes to use for all created
+ * directories. if this is null (default), the standard attributes
+ * are used
+ * @return true if the directory (and possibly parent directories) were actually created
+ * and false if the directory already existed. Throws exceptions on failure.
+ */
+bool createPath(boost::filesystem::path path,
+ LPSECURITY_ATTRIBUTES securityAttributes = nullptr);
+inline bool createPath(LPCWSTR path, LPSECURITY_ATTRIBUTES securityAttributes = nullptr)
+{
+ return createPath(boost::filesystem::path(path), securityAttributes);
+}
+
+std::wstring getWindowsBuildLab(bool ex = false);
+
+} // namespace winapi::ex::wide
+
+namespace winapi::process
+{
+
+template <typename CharT>
+_Create<CharT>::_Create(const std::basic_string<CharT>& binaryName)
+{
+ if (binaryName.length() > MAX_PATH) {
+ throw parameter_error("executable filename can't be longer than 260 characters");
+ }
+ m_CommandLine << "\"" << binaryName << "\"";
+}
+
+template <typename CharT>
+_Create<CharT>& _Create<CharT>::workingDirectory(const std::basic_string<CharT>& path)
+{
+ m_CurrentDirectory = path;
+ return *this;
+}
+
+template <typename CharT>
+_Create<CharT>& _Create<CharT>::processAttributes(SECURITY_ATTRIBUTES* attributes)
+{
+ m_ProcessAttributes = attributes;
+ return *this;
+}
+
+template <typename CharT>
+_Create<CharT>& _Create<CharT>::threadAttributes(SECURITY_ATTRIBUTES* attributes)
+{
+ m_ThreadAttributes = attributes;
+ return *this;
+}
+
+template <typename CharT>
+_Create<CharT>& _Create<CharT>::inheritHandles()
+{
+ m_InheritHandles = true;
+ return *this;
+}
+
+template <typename CharT>
+_Create<CharT>& _Create<CharT>::suspended()
+{
+ m_CreationFlags |= CREATE_SUSPENDED;
+ return *this;
+}
+
+template <typename CharT>
+_Create<CharT>& _Create<CharT>::stdoutPipe()
+{
+ m_StdoutPipe = true;
+ return *this;
+}
+
+} // namespace winapi::process
diff --git a/libs/usvfs/src/shared/windows_sane.h b/libs/usvfs/src/shared/windows_sane.h
new file mode 100644
index 0000000..7bb3726
--- /dev/null
+++ b/libs/usvfs/src/shared/windows_sane.h
@@ -0,0 +1,28 @@
+/*
+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
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
diff --git a/libs/usvfs/src/thooklib/CMakeLists.txt b/libs/usvfs/src/thooklib/CMakeLists.txt
new file mode 100644
index 0000000..48426db
--- /dev/null
+++ b/libs/usvfs/src/thooklib/CMakeLists.txt
@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(asmjit CONFIG REQUIRED)
+find_library(LIBUDIS86_LIBRARY libudis86)
+find_package(spdlog CONFIG REQUIRED)
+
+file(GLOB sources CONFIGURE_DEPENDS "*.cpp" "*.h")
+source_group("" FILES ${sources})
+
+add_library(thooklib STATIC ${sources})
+target_link_libraries(thooklib PRIVATE shared asmjit::asmjit ${LIBUDIS86_LIBRARY} spdlog::spdlog_header_only)
+target_include_directories(thooklib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+set_target_properties(thooklib PROPERTIES FOLDER injection)
+target_precompile_headers(shared PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../shared/pch.h)
diff --git a/libs/usvfs/src/thooklib/asmjit_sane.h b/libs/usvfs/src/thooklib/asmjit_sane.h
new file mode 100644
index 0000000..365622a
--- /dev/null
+++ b/libs/usvfs/src/thooklib/asmjit_sane.h
@@ -0,0 +1,29 @@
+/*
+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
+
+#pragma warning(push)
+#pragma warning(disable : 4201)
+#pragma warning(disable : 4244)
+#pragma warning(disable : 4245)
+#include <asmjit/asmjit.h>
+#include <asmjit/x86.h>
+#pragma warning(pop)
diff --git a/libs/usvfs/src/thooklib/hooklib.cpp b/libs/usvfs/src/thooklib/hooklib.cpp
new file mode 100644
index 0000000..9dbbafa
--- /dev/null
+++ b/libs/usvfs/src/thooklib/hooklib.cpp
@@ -0,0 +1,740 @@
+/*
+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 "udis86wrapper.h"
+#include <boost/format.hpp>
+#include <boost/predef.h>
+#include <map>
+#pragma warning(push, 3)
+#include <asmjit/asmjit.h>
+#pragma warning(pop)
+#include "hooklib.h"
+#include "ttrampolinepool.h"
+#include "utility.h"
+#include <addrtools.h>
+#include <formatters.h>
+#include <shmlogger.h>
+#include <winapi.h>
+
+#if BOOST_ARCH_X86_64
+#pragma message("64bit build")
+#define JUMP_SIZE 13
+#elif BOOST_ARCH_X86_32
+#define JUMP_SIZE 5
+#else
+#error "unsupported architecture"
+#endif
+
+using namespace asmjit;
+// from here on out I'll only test for 64 or "other"
+
+using namespace HookLib;
+using namespace asmjit;
+
+using namespace usvfs;
+
+struct THookInfo
+{
+ LPVOID originalFunction;
+ LPVOID replacementFunction;
+ LPVOID detour; // detour to call the original function after hook was installed.
+ LPVOID trampoline; // code fragment that decides whether the replacement function or
+ // detour is executed (preventing endless loops)
+ std::vector<uint8_t>
+ preamble; // part of the detour that needs to be re-inserted into the original
+ // function to return it to vanilla state
+ bool stub; // if this is true, the trampoline calls the "replacement"-function that
+ // before the original function, not instead of it
+ enum
+ {
+ TYPE_HOTPATCH, // official hot-patch variant as used on 32-bit windows
+ TYPE_WIN64PATCH, // custom patch variant used on 64-bit windows
+ TYPE_CHAINPATCH, // the hook is part of the hook chain (and not the first)
+ TYPE_OVERWRITE, // full jump overwrite used if none of the above work
+ TYPE_RIPINDIRECT // the function already started on a rip-relative jump so we only
+ // modified that variable
+ } type;
+};
+
+UDis86Wrapper& disasm()
+{
+ static UDis86Wrapper instance;
+ return instance;
+}
+
+void PauseOtherThreads()
+{
+ // TODO: implement me!
+}
+
+void ResumePausedThreads()
+{
+ // TODO: implement me! should resume only the threads paused by PauseOtherThreads
+}
+
+// not using the disassembler because this is simpler
+LPBYTE ShortJumpTarget(LPBYTE address)
+{
+ int8_t off = *(address + 1);
+ return address + 2 + off;
+}
+
+size_t GetJumpSize(LPBYTE, LPVOID)
+{
+ // TODO: it would be neater to use asmjit to generate this jump and ask asmjit for
+ // the size of this jump but with asmjit I can only generate absolute jumps, which
+ // take too much space.
+
+ // Since trampoline buffers is always allocated within 32-bit
+ // address range of jump, we can say with confidence that a 5-byte jump is possible
+ return 5;
+}
+
+void WriteLongJump(LPBYTE jumpAddr, LPVOID destination)
+{
+ // TODO: not using asmjit here because I couldn't figure out how to generate
+ // a working, space-optimized, relative jump to outside the generated code and
+ // we do want to optimize this jump
+#if BOOST_ARCH_X86_64
+ intptr_t dist = reinterpret_cast<intptr_t>(destination) -
+ (reinterpret_cast<intptr_t>(jumpAddr) + 5);
+ int32_t distShort = static_cast<int32_t>(dist);
+#else
+ int32_t distShort = reinterpret_cast<intptr_t>(destination) -
+ (reinterpret_cast<intptr_t>(jumpAddr) + 5);
+#endif
+ *jumpAddr = 0xE9;
+ *reinterpret_cast<int32_t*>(jumpAddr + 1) = distShort;
+}
+
+void WriteSingleJump(THookInfo& hookInfo, HookError* error)
+{
+ DWORD oldprotect, ignore;
+ // Set the target function to copy on write, so we don't modify code for other
+ // processes
+ if (!VirtualProtect(hookInfo.originalFunction, JUMP_SIZE, PAGE_EXECUTE_WRITECOPY,
+ &oldprotect)) {
+ throw std::runtime_error("failed to change virtual protection");
+ }
+
+ WriteLongJump(reinterpret_cast<LPBYTE>(hookInfo.originalFunction),
+ hookInfo.trampoline);
+
+ // restore old memory protection
+ if (!VirtualProtect(hookInfo.originalFunction, JUMP_SIZE, oldprotect, &ignore)) {
+ throw std::runtime_error("failed to change virtual protection");
+ }
+
+ if (error != nullptr) {
+ *error = ERR_NONE;
+ }
+}
+
+void WriteShortJump(LPBYTE jumpAddr, const signed char offset)
+{
+ *jumpAddr = 0xEB;
+ *(jumpAddr + 1) = offset;
+}
+
+void WriteIndirectJump(THookInfo& hookInfo, size_t jumpSize, HookError* error)
+{
+ DWORD oldProtect = 0;
+ LPBYTE jumpAddr = reinterpret_cast<LPBYTE>(hookInfo.originalFunction) - jumpSize;
+ // allow write to jump address + the short jump inside the function
+ if (!VirtualProtect(jumpAddr, jumpSize + 2, PAGE_EXECUTE_WRITECOPY, &oldProtect)) {
+ throw std::runtime_error("failed to change virtual protection");
+ }
+
+ // insert the long jump first, then the short jump to the long jump, thus
+ // activating the reroute
+ WriteLongJump(jumpAddr, hookInfo.trampoline);
+
+ PauseOtherThreads();
+ WriteShortJump(jumpAddr + jumpSize, -(static_cast<int8_t>(jumpSize) + 2));
+ ResumePausedThreads();
+
+ // restore access protection
+ if (!VirtualProtect(jumpAddr, jumpSize + 2, oldProtect, &oldProtect)) {
+ throw std::runtime_error("failed to change virtual protection");
+ }
+ if (error != nullptr) {
+ *error = ERR_NONE;
+ }
+}
+
+/// implements function hooking using the mechanism intended for hot patching
+/// Explanation: the visual studio compiler offers an option to prepare functions
+/// for hot patching. In this case the compiler leaves room for one far jump before
+/// the actual function and a 2-byte nop inside the function.
+/// to hook such a function we write a jump to our replacement function to the
+/// space before the function and short jump to that jump to where the 2-byte nop
+/// was.
+/// On 32-bit windows, MS seems to have compiled all relevant functions in the
+/// windows API for hot patching starting with Windows XP SP3
+/// On 64-bit windows a lot of functions have the space for a jump before the function
+/// but they don't have the 2-byte nop so this function doesn't work
+/// \param hookInfo info about the hook to be installed
+/// \return true on success, false on error
+BOOL HookHotPatch(THookInfo& hookInfo, HookError* error)
+{
+ LPVOID original = reinterpret_cast<LPVOID>(hookInfo.originalFunction);
+
+ if (hookInfo.stub) {
+ hookInfo.trampoline = TrampolinePool::instance().storeStub(
+ hookInfo.replacementFunction,
+ reinterpret_cast<LPVOID>(hookInfo.originalFunction),
+ shared::AddrAdd(original, 2));
+ } else {
+ hookInfo.trampoline = TrampolinePool::instance().storeTrampoline(
+ hookInfo.replacementFunction,
+ reinterpret_cast<LPVOID>(hookInfo.originalFunction),
+ shared::AddrAdd(original, 2));
+ }
+
+ WriteIndirectJump(hookInfo, JUMP_SIZE, error);
+ hookInfo.type = THookInfo::TYPE_HOTPATCH;
+ // in this case we don't need a separate detour, we simply jump past the 2-byte nop
+ hookInfo.detour = reinterpret_cast<LPBYTE>(hookInfo.originalFunction) + 2;
+
+ return TRUE;
+}
+
+uintptr_t followJumps(THookInfo& hookInfo)
+{
+ LPBYTE original = reinterpret_cast<LPBYTE>(hookInfo.originalFunction);
+ LPBYTE shortTarget = ShortJumpTarget(original);
+
+ // disassemble the long jump
+ disasm().setInputBuffer(shortTarget, JUMP_SIZE);
+
+ ud_disassemble(disasm());
+ if (ud_insn_mnemonic(disasm()) != UD_Ijmp) {
+ // this shouldn't happen, we only call this if the jump was discovered before
+ throw std::runtime_error("failed to find jump in patch");
+ }
+
+ uint64_t res = ud_insn_off(disasm()) + ud_insn_len(disasm());
+ res += disasm().jumpOffset();
+
+ return static_cast<uintptr_t>(res);
+}
+
+///
+/// \brief hook a call that is implemented as a short-jump to a long jump using a
+/// rip-relative address variable
+/// \param hookInfo info about the hook to be installed
+/// \param error if the return code is false and this is not null, the referred-to
+/// variable is set to an error code
+/// \return true on success, false on error
+///
+BOOL HookRIPIndirection(THookInfo& hookInfo, HookError* error)
+{
+ uintptr_t res = followJumps(hookInfo);
+
+ const ud_operand_t* op = ud_insn_opr(disasm(), 0);
+
+ if (op->base != UD_R_RIP) {
+ throw std::runtime_error("expected rip-relative addressing");
+ }
+
+ auto chainNext = disasm().jumpTarget();
+ if (hookInfo.stub) {
+ hookInfo.trampoline = TrampolinePool::instance().storeStub(
+ hookInfo.replacementFunction,
+ reinterpret_cast<LPVOID>(hookInfo.originalFunction),
+ reinterpret_cast<LPVOID>(chainNext));
+ } else {
+ hookInfo.trampoline = TrampolinePool::instance().storeTrampoline(
+ hookInfo.replacementFunction,
+ reinterpret_cast<LPVOID>(hookInfo.originalFunction),
+ reinterpret_cast<LPVOID>(chainNext));
+ }
+
+ DWORD oldProtect = 0;
+ if (!VirtualProtect(reinterpret_cast<LPVOID>(res), 2, PAGE_EXECUTE_WRITECOPY,
+ &oldProtect)) {
+ throw std::runtime_error("failed to change virtual protection");
+ }
+
+ *reinterpret_cast<uintptr_t*>(res) = reinterpret_cast<uintptr_t>(hookInfo.trampoline);
+
+ if (!VirtualProtect(reinterpret_cast<LPVOID>(res), 2, oldProtect, &oldProtect)) {
+ throw std::runtime_error("failed to change virtual protection");
+ }
+
+ hookInfo.type = THookInfo::TYPE_RIPINDIRECT;
+ hookInfo.detour = reinterpret_cast<LPVOID>(chainNext);
+
+ if (error != nullptr) {
+ *error = ERR_NONE;
+ }
+
+ return TRUE;
+}
+
+BOOL HookChainHook(THookInfo& hookInfo, LPBYTE jumpPos, HookError* error)
+{
+ // disassemble the long jump
+ disasm().setInputBuffer(jumpPos, JUMP_SIZE);
+
+ ud_disassemble(disasm());
+ if (ud_insn_mnemonic(disasm()) != UD_Ijmp) {
+ // this shouldn't happen, we only call this if the jump was discovered before
+ throw std::runtime_error("failed to find jump in patch");
+ }
+
+ auto chainTarget = disasm().jumpTarget();
+
+ size_t size = ud_insn_len(disasm());
+
+ // save the original code for the preamble so we can restore it later
+ hookInfo.preamble.resize(size);
+ memcpy(&hookInfo.preamble[0], jumpPos, size);
+
+ spdlog::get("usvfs")->info("existing hook to {0:x} in {1}", chainTarget,
+ shared::string_cast<std::string>(
+ winapi::ex::wide::getSectionName((void*)chainTarget)));
+
+ if (hookInfo.stub) {
+ hookInfo.trampoline = TrampolinePool::instance().storeStub(
+ hookInfo.replacementFunction,
+ reinterpret_cast<LPVOID>(hookInfo.originalFunction),
+ reinterpret_cast<LPVOID>(chainTarget));
+ } else {
+ hookInfo.trampoline = TrampolinePool::instance().storeTrampoline(
+ hookInfo.replacementFunction,
+ reinterpret_cast<LPVOID>(hookInfo.originalFunction),
+ reinterpret_cast<LPVOID>(chainTarget));
+ }
+
+ DWORD oldProtect = 0;
+ if (!VirtualProtect(jumpPos, size, PAGE_EXECUTE_WRITECOPY, &oldProtect)) {
+ throw std::runtime_error("failed to change virtual protection");
+ }
+
+ WriteLongJump(jumpPos, hookInfo.trampoline);
+
+ if (!VirtualProtect(jumpPos, size, oldProtect, &oldProtect)) {
+ throw std::runtime_error("failed to change virtual protection");
+ }
+
+ hookInfo.type = THookInfo::TYPE_CHAINPATCH;
+ hookInfo.detour = reinterpret_cast<LPVOID>(chainTarget);
+
+ if (error != nullptr) {
+ *error = ERR_NONE;
+ }
+
+ return TRUE;
+}
+
+///
+/// implements hooking by chaining to an existing hook
+/// \param hookInfo info about the hook to be installed
+/// \param error if the return code is false and this is not null,
+/// the referred-to variable is set to an error code
+/// \return true on success, false on error
+///
+BOOL HookChainHook(THookInfo& hookInfo, HookError* error)
+{
+ return HookChainHook(hookInfo, reinterpret_cast<LPBYTE>(hookInfo.originalFunction),
+ error);
+}
+
+///
+/// implements hooking by chaining to an existing hot patch
+/// \param hookInfo info about the hook to be installed
+/// \param error if the return code is false and this is not null,
+/// the referred-to variable is set to an error code
+/// \return true on success, false on error
+///
+BOOL HookChainPatch(THookInfo& hookInfo, HookError* error)
+{
+ LPBYTE original = reinterpret_cast<LPBYTE>(hookInfo.originalFunction);
+ LPBYTE shortTarget = ShortJumpTarget(original);
+
+ return HookChainHook(hookInfo, shortTarget, error);
+}
+
+/// implements function hooking by overwriting the first n bytes of the function
+/// with a jump to the replacement function. Since this is destructive to the original
+/// function code the first n bytes of the function need to be copied somewhere else
+/// and that code needs to be called via a detour. This is a lot more complex than
+/// the hotpatch mechanism.
+/// \param hookInfo info about the hook to be installed
+/// \param error if the return code is false and this is not null, the referred-to
+/// variable is set to an error code
+/// \return true on success, false on error
+BOOL HookDisasm(THookInfo& hookInfo, HookError* error)
+{
+ LPBYTE address = reinterpret_cast<LPBYTE>(hookInfo.originalFunction);
+ ud_set_input_buffer(disasm(), address, 40);
+
+ size_t jumpSize = GetJumpSize(
+ static_cast<LPBYTE>(hookInfo.originalFunction),
+ TrampolinePool::instance().currentBufferAddress(hookInfo.originalFunction));
+
+ // test if we have room for a jump before the function
+ bool jumpspace = true;
+ for (size_t i = 0; i < jumpSize; ++i) {
+ if (*(address - i - 1) != 0x90) {
+ jumpspace = false;
+ break;
+ }
+ }
+
+ size_t minSize = jumpspace ? 2 : jumpSize;
+
+ // iterate over all instructions that overlap with the jump instructions
+ // we want to write.
+ // TODO right now, this does not test if the function is smaller than the jump.
+ size_t size = 0;
+ while (size < minSize) {
+ if (ud_disassemble(disasm()) == 0) {
+ throw std::runtime_error("premature end of file in disassembly");
+ }
+
+ if ((size == 0) && (ud_insn_mnemonic(disasm()) == UD_Ijmp)) {
+ if (error != nullptr) {
+ *error = ERR_JUMP;
+ }
+ return FALSE;
+ }
+
+ size += ud_insn_len(disasm());
+
+ // ret instruction or int3 smells like function end
+ if ((ud_insn_mnemonic(disasm()) == UD_Iret) ||
+ (ud_insn_mnemonic(disasm()) == UD_Iint3)) {
+ if (error != nullptr) {
+ *error = ERR_FUNCEND;
+ }
+ return FALSE;
+ }
+
+ // check the operands for relative addressing (not supported)
+ for (int i = 0; i < 3; ++i) {
+ const ud_operand* op = ud_insn_opr(disasm(), i);
+ if (op) {
+ if (
+ // rip-relative are not handled, usually relative jumps
+ op->base == UD_R_RIP
+
+ // rsp-relative call are not handled (there are valid rsp-relative
+ // instructions, e.g. sub)
+ || (ud_insn_mnemonic(disasm()) == UD_Icall && op->base == UD_R_RSP)) {
+ if (error != nullptr) {
+ *error = ERR_RIP;
+ }
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ // save the original code for the preamble so we can restore it later
+ hookInfo.preamble.resize(size);
+ memcpy(&hookInfo.preamble[0], hookInfo.originalFunction, size);
+
+ size_t rerouteOffset = 0;
+ if (hookInfo.stub) {
+ hookInfo.trampoline = TrampolinePool::instance().storeStub(
+ hookInfo.replacementFunction, hookInfo.originalFunction, size, &rerouteOffset);
+ } else {
+ hookInfo.trampoline = TrampolinePool::instance().storeTrampoline(
+ hookInfo.replacementFunction, hookInfo.originalFunction, size, &rerouteOffset);
+ }
+
+ if (jumpspace) {
+ WriteIndirectJump(hookInfo, jumpSize, error);
+ hookInfo.type = THookInfo::TYPE_WIN64PATCH;
+ } else {
+ WriteSingleJump(hookInfo, error);
+ hookInfo.type = THookInfo::TYPE_OVERWRITE;
+ }
+ hookInfo.detour = reinterpret_cast<LPBYTE>(hookInfo.trampoline) + rerouteOffset;
+
+ return TRUE;
+}
+
+enum EPreamble
+{
+ PRE_PATCHFREE,
+ PRE_PATCHUSED,
+ PRE_RIPINDIRECT,
+ PRE_FOREIGNHOOK,
+ PRE_UNKNOWN
+};
+
+EPreamble DeterminePreamble(LPBYTE address)
+{
+ ud_set_input_buffer(disasm(), address, JUMP_SIZE);
+ ud_disassemble(disasm());
+
+ if ((ud_insn_mnemonic(disasm()) == UD_Imov) &&
+ (ud_insn_opr(disasm(), 0) == ud_insn_opr(disasm(), 1)) &&
+ (ud_insn_opr(disasm(), 0)->type == UD_OP_REG)) {
+ // mov edi, edi
+ return PRE_PATCHFREE;
+ } else if ((ud_insn_mnemonic(disasm()) == UD_Ijmp) && (ud_insn_len(disasm()) == 2)) {
+ // determine target of the short jump
+ LPBYTE shortTarget = ShortJumpTarget(address);
+
+ // test if that short jump leads to a long jump
+ ud_set_input_buffer(disasm(), shortTarget, JUMP_SIZE);
+ ud_disassemble(disasm());
+ if (ud_insn_mnemonic(disasm()) == UD_Ijmp) {
+ const ud_operand* op = ud_insn_opr(disasm(), 0);
+ if (op->base == UD_R_RIP) {
+ return PRE_RIPINDIRECT;
+ } else {
+ return PRE_PATCHUSED;
+ }
+ } else {
+ return PRE_UNKNOWN;
+ }
+ } else if (ud_insn_mnemonic(disasm()) == UD_Ijmp) {
+ return PRE_FOREIGNHOOK;
+ } else {
+ return PRE_UNKNOWN;
+ }
+}
+
+static std::map<HOOKHANDLE, THookInfo> s_Hooks;
+
+static HOOKHANDLE GenerateHandle()
+{
+ static ULONG NextHandle = 1;
+ return NextHandle++;
+}
+
+HOOKHANDLE applyHook(THookInfo info, HookError* error)
+{
+ // apply the correct hook function depending on how the function start looks
+ EPreamble preamble = DeterminePreamble((LPBYTE)info.originalFunction);
+
+ BOOL success = FALSE;
+ switch (preamble) {
+ case PRE_PATCHUSED: {
+ success = HookChainPatch(info, error);
+ } break;
+ case PRE_PATCHFREE: {
+ success = HookHotPatch(info, error);
+ } break;
+ case PRE_RIPINDIRECT: {
+ success = HookRIPIndirection(info, error);
+ } break;
+ case PRE_FOREIGNHOOK: {
+ success = HookChainHook(info, error);
+ } break;
+ default: {
+ success = HookDisasm(info, error);
+ } break;
+ }
+
+ if (success == TRUE) {
+ HOOKHANDLE handle = GenerateHandle();
+ s_Hooks[handle] = info;
+ return handle;
+ } else {
+ return INVALID_HOOK;
+ }
+}
+
+HOOKHANDLE HookLib::InstallStub(LPVOID functionAddress, LPVOID stubAddress,
+ HookError* error)
+{
+ if (functionAddress == nullptr) {
+ if (error != nullptr)
+ *error = ERR_INVALIDPARAMETERS;
+ return INVALID_HOOK;
+ }
+
+ THookInfo info;
+ info.originalFunction = functionAddress;
+ info.replacementFunction = stubAddress;
+ info.stub = true;
+ info.detour = nullptr;
+ info.trampoline = nullptr;
+ info.type = THookInfo::TYPE_OVERWRITE;
+
+ return applyHook(info, error);
+}
+
+HOOKHANDLE HookLib::InstallStub(HMODULE module, LPCSTR functionName, LPVOID stubAddress,
+ HookError* error)
+{
+ LPVOID funcAddr = MyGetProcAddress(module, functionName);
+ return InstallStub(funcAddr, stubAddress, error);
+}
+
+HOOKHANDLE HookLib::InstallHook(LPVOID functionAddress, LPVOID hookAddress,
+ HookError* error)
+{
+ if (functionAddress == nullptr) {
+ if (error != nullptr)
+ *error = ERR_INVALIDPARAMETERS;
+ return INVALID_HOOK;
+ }
+ THookInfo info;
+ info.originalFunction = functionAddress;
+ info.replacementFunction = hookAddress;
+ info.stub = false;
+ info.detour = nullptr;
+ info.trampoline = nullptr;
+ info.type = THookInfo::TYPE_OVERWRITE;
+
+ return applyHook(info, error);
+}
+
+HOOKHANDLE HookLib::InstallHook(HMODULE module, LPCSTR functionName, LPVOID hookAddress,
+ HookError* error)
+{
+ LPVOID funcAddr = MyGetProcAddress(module, functionName);
+ return InstallHook(funcAddr, hookAddress, error);
+}
+
+void HookLib::RemoveHook(HOOKHANDLE handle)
+{
+ auto iter = s_Hooks.find(handle);
+ if (iter != s_Hooks.end()) {
+ THookInfo info = iter->second;
+ PauseOtherThreads();
+ LPBYTE address = reinterpret_cast<LPBYTE>(info.originalFunction);
+ if (info.type == THookInfo::TYPE_HOTPATCH) {
+ // return the short jump to 2-byte nop
+ // TODO: This doesn't take into account if we chain-loaded another hook
+
+ DWORD oldProtect = 0;
+ if (!VirtualProtect(address, 2, PAGE_EXECUTE_WRITECOPY, &oldProtect)) {
+ throw shared::windows_error("failed to gain write access to remove hook");
+ }
+ *address = 0x8b;
+ *(address + 1) = 0xff;
+ VirtualProtect(address, 2, oldProtect, &oldProtect);
+ } else if ((info.type == THookInfo::TYPE_OVERWRITE) ||
+ (info.type == THookInfo::TYPE_WIN64PATCH)) {
+ DWORD oldProtect = 0;
+ if (!VirtualProtect(address, info.preamble.size(), PAGE_EXECUTE_WRITECOPY,
+ &oldProtect)) {
+ throw shared::windows_error("failed to gain write access to remove hook");
+ }
+ // TODO: remove hook by restoring the original function. This only works if we
+ // have the exact code available somewhere
+ memcpy(address, &info.preamble[0], info.preamble.size());
+ VirtualProtect(address, info.preamble.size(), oldProtect, &oldProtect);
+ } else if (info.type == THookInfo::TYPE_CHAINPATCH) {
+ // we could attempt to restore the original function preamble but I'm not
+ // sure we can reliably write the jump with same (or lower) size.
+ // Instead overwrite our own trampoline
+ disasm().setInputBuffer(static_cast<uint8_t*>(info.originalFunction), JUMP_SIZE);
+ ud_disassemble(disasm());
+ if (ud_insn_mnemonic(disasm()) != UD_Ijmp) {
+ // this shouldn't happen, we only call this if the jump was discovered before
+ throw std::runtime_error("failed to find jump in patch");
+ }
+
+ LPBYTE jumpPos = static_cast<LPBYTE>(info.originalFunction);
+ if (ud_insn_len(disasm()) == 2) {
+ jumpPos = reinterpret_cast<LPBYTE>(disasm().jumpTarget());
+ }
+
+ DWORD oldProtect = 0;
+ if (!VirtualProtect(jumpPos, info.preamble.size(), PAGE_EXECUTE_WRITECOPY,
+ &oldProtect)) {
+ throw shared::windows_error("failed to gain write access to remove hook");
+ }
+ memcpy(jumpPos, info.preamble.data(), info.preamble.size());
+ VirtualProtect(jumpPos, info.preamble.size(), oldProtect, &oldProtect);
+ } else if (info.type == THookInfo::TYPE_RIPINDIRECT) {
+ uintptr_t res = followJumps(info);
+ DWORD oldProtect = 0;
+ if (!VirtualProtect(reinterpret_cast<LPVOID>(res), JUMP_SIZE,
+ PAGE_EXECUTE_WRITECOPY, &oldProtect)) {
+ throw shared::windows_error("failed to gain write access to remove hook");
+ }
+ *reinterpret_cast<uintptr_t*>(res) =
+ reinterpret_cast<uintptr_t>(info.originalFunction);
+ VirtualProtect(reinterpret_cast<LPVOID>(res), JUMP_SIZE, oldProtect, &oldProtect);
+ } else {
+ spdlog::get("usvfs")->critical("can't remove hook, unknown hook type!");
+ }
+
+ s_Hooks.erase(iter);
+ ResumePausedThreads();
+ } else {
+ spdlog::get("usvfs")->info("handle unknown: {0:x}", handle);
+ }
+}
+
+const char* HookLib::GetErrorString(HookError err)
+{
+ switch (err) {
+ case ERR_NONE:
+ return "No Error";
+ case ERR_INVALIDPARAMETERS:
+ return "Invalid parameters";
+ case ERR_FUNCEND:
+ return "Function too short";
+ case ERR_JUMP:
+ return "Function starts on a jump";
+ case ERR_RIP:
+ return "RIP-relative addressing can't be relocated.";
+ case ERR_RELJUMP:
+ return "Relative Jump can't be relocated.";
+ default:
+ return "Unkown error code";
+ }
+}
+
+const char* HookLib::GetHookType(HOOKHANDLE handle)
+{
+ auto iter = s_Hooks.find(handle);
+ if (iter != s_Hooks.end()) {
+ THookInfo info = iter->second;
+ switch (info.type) {
+ case THookInfo::TYPE_HOTPATCH:
+ return "hot patch";
+ case THookInfo::TYPE_WIN64PATCH:
+ return "64-bit hot patch";
+ case THookInfo::TYPE_CHAINPATCH:
+ return "chained patch";
+ case THookInfo::TYPE_OVERWRITE:
+ return "overwrite";
+ case THookInfo::TYPE_RIPINDIRECT:
+ return "rip indirection modified";
+ default: {
+ spdlog::get("usvfs")->error("invalid hook type {0}", info.type);
+ return "invalid hook type";
+ }
+ }
+ }
+ return "invalid handle";
+}
+
+LPVOID HookLib::GetDetour(HOOKHANDLE handle)
+{
+ auto iter = s_Hooks.find(handle);
+ if (iter != s_Hooks.end()) {
+ THookInfo info = iter->second;
+ return info.detour;
+ }
+ return nullptr;
+}
diff --git a/libs/usvfs/src/thooklib/hooklib.h b/libs/usvfs/src/thooklib/hooklib.h
new file mode 100644
index 0000000..a549c03
--- /dev/null
+++ b/libs/usvfs/src/thooklib/hooklib.h
@@ -0,0 +1,125 @@
+/*
+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 <map>
+#include <windows_sane.h>
+
+namespace HookLib
+{
+
+enum HookError
+{
+ ERR_NONE,
+ ERR_INVALIDPARAMETERS, // parameters are invalid
+ ERR_FUNCEND, // function is too short to be hooked
+ ERR_JUMP, // function consists only of an unconditional jump. Maybe it has already
+ // been hooked?
+ ERR_RIP, // segment of the function to be overwritten contains a instruction-relative
+ // operation
+ ERR_RELJUMP // segment of the function to be overwritten contains a relative jump we
+ // can't relocated
+};
+
+typedef ULONG HOOKHANDLE;
+static const HOOKHANDLE INVALID_HOOK = (HOOKHANDLE)-1;
+
+///
+/// \brief install a stub (function to be called before the target function)
+/// \param functionAddress address of the function to stub
+/// \param stubAddress address of the stub function. This function has to have the
+/// signature of void foobar(LPVOID address).
+/// address receives the address of the function.
+/// \param error (optional) if set, the referenced variable will receive an error code
+/// describing the problem (if any)
+/// \return a handle to reference the hook in later operations or INVALID_HOOK on error
+///
+HOOKHANDLE InstallStub(LPVOID functionAddress, LPVOID stubAddress,
+ HookError* error = nullptr);
+
+///
+/// \brief install a stub (function to be called before the target function)
+/// \param module the module containing the function to hook
+/// \param functionName name of the function to stub (as exported by the library)
+/// \param stubAddress address of the stub function. This function has to have the
+/// signature of void foobar(LPVOID address).
+/// address receives the address of the function.
+/// \param error (optional) if set, the referenced variable will receive an error code
+/// describing the problem (if any)
+/// \return a handle to reference the hook in later operations or INVALID_HOOK on error
+///
+HOOKHANDLE InstallStub(HMODULE module, LPCSTR functionName, LPVOID stubAddress,
+ HookError* error = nullptr);
+
+///
+/// \brief install a hook (function replacing the existing functionality of the
+/// function)
+/// \param functionAddress address of the function to hook
+/// \param hookAddress address of the replacement function. This function has to have
+/// the exact same signature as the replaced function
+/// \param error (optional) if set, the referenced variable will receive an error code
+/// describing the problem (if any)
+/// \return a handle to reference the hook in later operations or INVALID_HOOK on error
+///
+HOOKHANDLE InstallHook(LPVOID functionAddress, LPVOID hookAddress,
+ HookError* error = nullptr);
+
+///
+/// \brief install a hook (function replacing the existing functionality of the
+/// function)
+/// \param functionName name of the function to hook (as exported by the library)
+/// \param hookAddress address of the replacement function. This function has to have
+/// the exact same signature as the replaced function
+/// \param error (optional) if set, the referenced variable will receive an error code
+/// describing the problem (if any)
+/// \return a handle to reference the hook in later operations or INVALID_HOOK on error
+///
+HOOKHANDLE InstallHook(HMODULE module, LPCSTR functionName, LPVOID hookAddress,
+ HookError* error = nullptr);
+
+///
+/// \brief remove a hook
+/// \param handle handle returned in InstallStub or InstallHook
+///
+void RemoveHook(HOOKHANDLE handle);
+
+///
+/// \brief determine the type of a hook
+/// \param handle the handle to look up
+/// \return a string describing the used hooking mechanism
+///
+const char* GetHookType(HOOKHANDLE handle);
+
+///
+/// \brief retrieve the address that can be used to directly call a detour
+/// \param handle handle for the hook
+/// \return function address
+///
+LPVOID GetDetour(HOOKHANDLE handle);
+
+///
+/// \brief resolve an error code to a descriptive string
+/// \param err the error code to resolve
+/// \return the error string
+///
+const char* GetErrorString(HookError err);
+
+} // namespace HookLib
diff --git a/libs/usvfs/src/thooklib/pch.cpp b/libs/usvfs/src/thooklib/pch.cpp
new file mode 100644
index 0000000..1d9f38c
--- /dev/null
+++ b/libs/usvfs/src/thooklib/pch.cpp
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/libs/usvfs/src/thooklib/ttrampolinepool.cpp b/libs/usvfs/src/thooklib/ttrampolinepool.cpp
new file mode 100644
index 0000000..3b6a51f
--- /dev/null
+++ b/libs/usvfs/src/thooklib/ttrampolinepool.cpp
@@ -0,0 +1,601 @@
+/*
+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 "ttrampolinepool.h"
+#include <addrtools.h>
+#include <shmlogger.h>
+// #include <boost/thread/lock_guard.hpp>
+#include "udis86wrapper.h"
+
+using namespace asmjit;
+#if BOOST_ARCH_X86_64
+using namespace x86;
+#elif BOOST_ARCH_X86_32
+using namespace asmjit::x86;
+#endif
+
+using namespace usvfs::shared;
+
+namespace HookLib
+{
+
+TrampolinePool* TrampolinePool::s_Instance = nullptr;
+
+TrampolinePool::TrampolinePool() : m_MaxTrampolineSize(sizeof(LPVOID))
+{
+ m_BarrierAddr = &TrampolinePool::barrier;
+ m_ReleaseAddr = &TrampolinePool::release;
+
+ SYSTEM_INFO sysInfo;
+ ::ZeroMemory(&sysInfo, sizeof(SYSTEM_INFO));
+ GetSystemInfo(&sysInfo);
+ m_BufferSize = sysInfo.dwPageSize;
+
+ // if search range = ffffff then addressmask = ffffffffff000000
+ // => all jumps between xxxxxxxxxx000000 and xxxxxxxxxxffffff will use the same buffer
+ // for trampolines which is guaranteed to be in that range
+ // TODO it should be valid to use 2 ^ 32 as the search range to increase our chances
+ // of finding a memory block we can reserve but then there is a problem with
+ // converting negative jump distances to 32 bit I didn't understand. Everything
+ // up to 2 ^ 31 seems to be fine though
+ m_SearchRange = static_cast<size_t>(pow(2, 30)) - 1;
+ m_AddressMask = std::numeric_limits<uint64_t>::max() - m_SearchRange;
+}
+
+// static
+void TrampolinePool::initialize()
+{
+ if (!s_Instance)
+ s_Instance = new TrampolinePool();
+}
+
+void TrampolinePool::setBlock(bool block)
+{
+ m_FullBlock = block;
+ if (m_ThreadGuards.get() == nullptr) {
+ m_ThreadGuards.reset(new TThreadMap());
+ }
+}
+
+#if BOOST_ARCH_X86_64
+// push all registers (except rax) and flags to the stack
+static void pushAll(X86Assembler& assembler)
+{
+ assembler.pushf();
+ assembler.push(rcx);
+ assembler.push(rdx);
+ assembler.push(rbx);
+ assembler.push(rbp);
+ assembler.push(rsi);
+ assembler.push(rdi);
+ assembler.push(r8);
+ assembler.push(r9);
+ assembler.push(r10);
+ assembler.push(r11);
+ assembler.push(r12);
+ assembler.push(r13);
+ assembler.push(r14);
+ assembler.push(r15);
+}
+
+// pop all registers (except rax) and flags from stack
+static void popAll(X86Assembler& assembler)
+{
+ assembler.pop(r15);
+ assembler.pop(r14);
+ assembler.pop(r13);
+ assembler.pop(r12);
+ assembler.pop(r11);
+ assembler.pop(r10);
+ assembler.pop(r9);
+ assembler.pop(r8);
+ assembler.pop(rdi);
+ assembler.pop(rsi);
+ assembler.pop(rbp);
+ assembler.pop(rbx);
+ assembler.pop(rdx);
+ assembler.pop(rcx);
+ assembler.popf();
+}
+#endif // BOOST_ARCH_X86_64
+
+void TrampolinePool::addBarrier(LPVOID rerouteAddr, LPVOID original,
+ X86Assembler& assembler)
+{
+ Label skipLabel = assembler.newLabel();
+
+#if BOOST_ARCH_X86_64
+ pushAll(assembler);
+ assembler.mov(rcx,
+ imm(reinterpret_cast<int64_t>(
+ original))); // set call parameter for call to barrier function
+ assembler.mov(rax, imm((intptr_t)(void*)barrier));
+ assembler.sub(rsp, 32);
+ assembler.call(rax);
+ assembler.add(rsp, 32);
+ popAll(assembler);
+ // test barrier
+ assembler.cmp(rax, 0); // test if the barrier is locked
+ assembler.jz(skipLabel); // skip if barrier was locked
+
+ // call replacement function
+ // for this call no registers are saved. the called function is a compiled function so
+ // it should correctly save non-volatile registers, and the caller can't expect the
+ // volatile ones to remain valid
+ assembler.pop(r10);
+ assembler.mov(dword_ptr(rax), r10); // store that return address to the variable
+ // supplied by the barrier function
+ assembler.mov(rax, imm((intptr_t)(LPVOID)rerouteAddr));
+ assembler.call(rax);
+ assembler.push(rax); // save away result
+
+ // open the barrier again
+ pushAll(assembler);
+ assembler.mov(rcx, imm(reinterpret_cast<int64_t>(original)));
+ assembler.mov(rax, imm((intptr_t)(void*)release));
+ assembler.sub(rsp, 32);
+ assembler.call(rax);
+ assembler.add(rsp, 32);
+ popAll(assembler);
+ assembler.pop(r10); // get the result from the replacement function to a register
+ assembler.push(rax); // push the original return address back on the stack
+ assembler.mov(rax, r10); // move result of actual call to rax
+ assembler.ret(); // return, using the original return address
+#else // BOOST_ARCH_X86_64
+ assembler.push(imm(void_ptr_cast<int32_t>(
+ original))); // push original function, as parameter to barrier
+ assembler.mov(ecx, (Ptr) static_cast<void*>(TrampolinePool::barrier));
+ assembler.call(ecx); // call barrier function
+ assembler.cmp(eax, 0);
+ assembler.jz(skipLabel); // if barrier is locked, jump to end of function
+
+ // case a: we got through the barrier
+ assembler.pop(ecx); // pop the return address into ecx
+ assembler.mov(dword_ptr(eax), ecx); // store that return address in the variable
+ // supplied by the barrier function
+
+ assembler.mov(eax, (Ptr) static_cast<void*>(rerouteAddr));
+ assembler.call(eax); // call replacement function (pointer was stored in front of the
+ // trampoline) (this function gets the parameters that were on
+ // the stack already and cleans
+ // them up itself (stdcall convention))
+ assembler.push(eax); // save away result
+ assembler.push(imm(void_ptr_cast<int32_t>(original))); // open the barrier again
+ assembler.mov(eax, (Ptr) static_cast<void*>(TrampolinePool::release));
+ assembler.call(eax);
+ assembler.pop(ecx); // pop the result from the actual call to ecx
+ assembler.push(eax); // push the original return address (returned by
+ // TTrampolinePool::release) back on the stack
+ assembler.mov(eax, ecx); // move result of actual call to eax
+ assembler.ret(); // return, using the original return address
+#endif // BOOST_ARCH_X86_64
+
+ assembler.bind(skipLabel);
+}
+
+LPVOID TrampolinePool::roundAddress(LPVOID address) const
+{
+ return reinterpret_cast<LPVOID>(reinterpret_cast<intptr_t>(address) & m_AddressMask);
+}
+
+TrampolinePool::BufferList& TrampolinePool::getBufferList(LPVOID address)
+{
+ LPVOID rounded = roundAddress(address);
+ auto iter = m_Buffers.find(rounded);
+ if (iter == m_Buffers.end()) {
+ BufferList newBufList = {0, std::vector<LPVOID>()};
+ m_Buffers[rounded] = newBufList;
+ iter = allocateBuffer(address);
+ }
+ return iter->second;
+}
+
+LPVOID TrampolinePool::storeStub(LPVOID reroute, LPVOID original, LPVOID returnAddress)
+{
+ BufferList& bufferList = getBufferList(original);
+ // first test to increase likelyhood we don't have to reallocate later
+ if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) {
+ allocateBuffer(original);
+ }
+
+ LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset);
+
+ // ??? write address of reroute to trampoline and move past the address
+ *reinterpret_cast<LPVOID*>(spot) = reroute;
+ // coverity[suspicious_sizeof]
+ spot = AddrAdd(spot, sizeof(LPVOID));
+ bufferList.offset += sizeof(LPVOID);
+
+ JitRuntime runtime;
+#if BOOST_ARCH_X86_64
+ X86Assembler assembler(&runtime);
+#else
+ X86Assembler assembler(&runtime);
+#endif
+ addCallToStub(assembler, original, reroute);
+ addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(returnAddress));
+
+ size_t codeSize = assembler.getCodeSize();
+
+ m_MaxTrampolineSize =
+ std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID)));
+
+ // final test to see if we can store the trampoline in the buffer
+ if ((bufferList.offset + codeSize) > m_BufferSize) {
+ // can't place function in buffer, allocate another and try again
+ allocateBuffer(original);
+ // we could relocate the code and the data but this is simpler
+ return storeStub(reroute, original, returnAddress);
+ }
+
+ // adjust relative jumps for move to buffer
+ codeSize = assembler.relocCode(spot);
+
+ uint8_t* code = assembler.getBuffer();
+ memcpy(spot, code, codeSize);
+
+ bufferList.offset += codeSize;
+
+ return spot;
+}
+
+LPVOID TrampolinePool::storeTrampoline(LPVOID reroute, LPVOID original,
+ LPVOID returnAddress)
+{
+ BufferList& bufferList = getBufferList(original);
+ // first test to increase likelyhood we don't have to reallocate later
+ if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) {
+ allocateBuffer(original);
+ }
+
+ LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset);
+
+ *reinterpret_cast<LPVOID*>(spot) = reroute;
+ // coverity[suspicious_sizeof]
+ spot = AddrAdd(spot, sizeof(LPVOID));
+ bufferList.offset += sizeof(LPVOID);
+
+ JitRuntime runtime;
+ X86Assembler assembler(&runtime);
+ addBarrier(reroute, original, assembler);
+#if BOOST_ARCH_X86_64
+ assembler.mov(rax, imm((intptr_t)(void*)(returnAddress)));
+ assembler.jmp(rax);
+#else
+ assembler.mov(eax, imm((intptr_t)(void*)(returnAddress)));
+ assembler.jmp(eax);
+#endif
+ size_t codeSize = assembler.getCodeSize();
+
+ m_MaxTrampolineSize =
+ std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID)));
+
+ // final test to see if we can store the trampoline in the buffer
+ if ((bufferList.offset + codeSize) > m_BufferSize) {
+ // can't place function in buffer, allocate another and try again
+ allocateBuffer(original);
+ // we could relocate the code and the data but this is simpler
+ return storeTrampoline(reroute, original, returnAddress);
+ }
+
+ // adjust relative jumps for move to buffer
+ codeSize = assembler.relocCode(spot);
+
+ // copy code to buffer
+ uint8_t* code = assembler.getBuffer();
+ memcpy(spot, code, codeSize);
+
+ bufferList.offset += codeSize;
+ return spot;
+}
+
+#if BOOST_ARCH_X86_64
+void TrampolinePool::copyCode(X86Assembler& assembler, LPVOID source, size_t numBytes)
+{
+ static UDis86Wrapper disasm;
+
+ disasm.setInputBuffer(static_cast<const uint8_t*>(source), numBytes);
+
+ size_t offset = 0;
+
+ while (ud_disassemble(disasm) != 0) {
+ // rewrite relative jumps, blind copy everything else
+
+ offset += ud_insn_len(disasm);
+
+ // WARNING: doesn't support conditional jumps
+ if ((ud_insn_mnemonic(disasm) == UD_Ijmp) &&
+ (ud_insn_opr(disasm, 0)->type == UD_OP_JIMM)) {
+ uintptr_t dest = disasm.jumpTarget();
+ assembler.mov(rax, imm(static_cast<uint64_t>(dest)));
+ assembler.jmp(rax);
+ } else {
+ assembler.embed(ud_insn_ptr(&disasm.obj()), ud_insn_len(&disasm.obj()));
+ // assembler.data();
+ }
+ }
+}
+#endif
+
+void TrampolinePool::addCallToStub(X86Assembler& assembler, LPVOID original,
+ LPVOID reroute)
+{
+#if BOOST_ARCH_X86_64
+ pushAll(assembler);
+ assembler.mov(rcx, imm(reinterpret_cast<int64_t>(original)));
+ assembler.mov(rax, imm((intptr_t)(LPVOID)reroute));
+ assembler.sub(rsp, 32);
+ assembler.call(rax);
+ assembler.add(rsp, 32);
+ popAll(assembler);
+#else // BOOST_ARCH_X86_64
+ assembler.push(reinterpret_cast<int64_t>(original));
+ assembler.mov(ecx, imm((intptr_t)(LPVOID)reroute));
+ assembler.call(ecx);
+ assembler.pop(ecx); // remove argument from stack
+#endif // BOOST_ARCH_X86_64
+}
+
+void TrampolinePool::addAbsoluteJump(X86Assembler& assembler, uint64_t destination)
+{
+#if BOOST_ARCH_X86_64
+ assembler.push(rax);
+ assembler.push(rax);
+ assembler.mov(rax, imm(destination));
+ assembler.mov(ptr(rsp, 8), rax);
+ assembler.pop(rax);
+ assembler.ret();
+#else // BOOST_ARCH_X86_64
+ assembler.push(imm(destination));
+ assembler.ret();
+#endif // BOOST_ARCH_X86_64
+}
+
+LPVOID TrampolinePool::storeStub(LPVOID reroute, LPVOID original, size_t preambleSize,
+ size_t* rerouteOffset)
+{
+ BufferList& bufferList = getBufferList(original);
+ // first test to increase likelyhood we don't have to reallocate later
+ if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) {
+ allocateBuffer(original);
+ }
+
+ LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset);
+
+ *reinterpret_cast<LPVOID*>(spot) = reroute;
+ // coverity[suspicious_sizeof]
+ spot = AddrAdd(spot, sizeof(LPVOID));
+ bufferList.offset += sizeof(LPVOID);
+
+ JitRuntime runtime;
+ X86Assembler assembler(&runtime);
+ addCallToStub(assembler, original, reroute);
+#if BOOST_ARCH_X86_64
+ // insert backup code
+ *rerouteOffset = assembler.getCodeSize();
+ copyCode(assembler, original, preambleSize);
+#else // BOOST_ARCH_X86_64
+ assembler.embed(original, preambleSize);
+#endif // BOOST_ARCH_X86_64
+ addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(original) + preambleSize);
+
+ // adjust relative jumps for move to buffer
+ size_t codeSize = assembler.getCodeSize();
+
+ m_MaxTrampolineSize =
+ std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID)));
+
+ // final test to see if we can store the trampoline in the buffer
+ if ((bufferList.offset + codeSize) > m_BufferSize) {
+ // can't place function in buffer, allocate another and try again
+ allocateBuffer(original);
+ // we could relocate the code and the data but this is simpler
+ return storeStub(reroute, original, preambleSize, rerouteOffset);
+ }
+
+ // copy code to buffer
+ codeSize = assembler.relocCode(spot);
+
+ bufferList.offset += preambleSize + codeSize;
+ return spot;
+}
+
+LPVOID TrampolinePool::storeTrampoline(LPVOID reroute, LPVOID original,
+ size_t preambleSize, size_t* rerouteOffset)
+{
+ BufferList& bufferList = getBufferList(original);
+ // first test to increase likelyhood we don't have to reallocate later
+ if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) {
+ allocateBuffer(original);
+ }
+
+ LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset);
+
+ *reinterpret_cast<LPVOID*>(spot) = reroute;
+ // coverity[suspicious_sizeof]
+ spot = AddrAdd(spot, sizeof(LPVOID));
+ bufferList.offset += sizeof(LPVOID);
+
+ JitRuntime runtime;
+ X86Assembler assembler(&runtime);
+ addBarrier(reroute, original, assembler);
+ // insert backup code
+ *rerouteOffset = assembler.getCodeSize();
+ assembler.embed(original, static_cast<uint32_t>(preambleSize));
+ addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(original) + preambleSize);
+
+ // adjust relative jumps for move to buffer
+ size_t codeSize = assembler.getCodeSize();
+
+ m_MaxTrampolineSize =
+ std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID)));
+
+ // TODO this does not take into account that the code size may technically change
+ // after relocation in which case the following test may determine the code fits into
+ // the buffer when it really doesnt't. asmjit doesn't seem to provide a way to adjust
+ // jumps without actually moving the code though
+
+ // final test to see if we can store the trampoline in the buffer
+ if ((bufferList.offset + codeSize) > m_BufferSize) {
+ // can't place function in buffer, allocate another and try again
+ allocateBuffer(original);
+ // we could relocate the code and the data but this is simpler
+ return storeTrampoline(reroute, original, preambleSize, rerouteOffset);
+ }
+
+ // copy code to buffer
+ codeSize = static_cast<size_t>(assembler.relocCode(spot));
+
+ bufferList.offset += preambleSize + codeSize;
+
+ return spot;
+}
+
+LPVOID TrampolinePool::currentBufferAddress(LPVOID addressNear)
+{
+ LPVOID rounded = roundAddress(addressNear);
+ auto lookupAddress = m_Buffers.find(rounded);
+
+ if (lookupAddress == m_Buffers.end()) {
+ lookupAddress = m_Buffers.insert(std::make_pair(rounded, BufferList())).first;
+ }
+ if (lookupAddress->second.buffers.size() == 0) {
+ allocateBuffer(addressNear);
+ }
+
+ LPVOID res = *(lookupAddress->second.buffers.rbegin());
+ return res;
+}
+
+void TrampolinePool::forceUnlockBarrier()
+{
+ if (m_ThreadGuards.get() != nullptr) {
+ for (auto funcId : *m_ThreadGuards) {
+ (*m_ThreadGuards)[funcId.first] = nullptr;
+ }
+ } // else no barriers to unlock
+}
+
+TrampolinePool::BufferMap::iterator TrampolinePool::allocateBuffer(LPVOID addressNear)
+{
+ // allocate a buffer that we can write to and that is executable
+ SYSTEM_INFO sysInfo;
+ ::ZeroMemory(&sysInfo, sizeof(SYSTEM_INFO));
+ GetSystemInfo(&sysInfo);
+
+ LPVOID rounded = roundAddress(addressNear);
+ auto iter = m_Buffers.find(rounded);
+ uintptr_t lowerEnd = reinterpret_cast<uintptr_t>(rounded);
+ if (iter->second.buffers.size() > 0) {
+ // start searching were we last found a buffer
+ lowerEnd = reinterpret_cast<uintptr_t>(*iter->second.buffers.rbegin()) +
+ sysInfo.dwPageSize;
+ }
+
+ uintptr_t start =
+ std::max(std::max(lowerEnd, MIN_ALLOC_ADDR),
+ reinterpret_cast<uintptr_t>(sysInfo.lpMinimumApplicationAddress));
+ uintptr_t upperEnd = reinterpret_cast<uintptr_t>(rounded) + m_SearchRange;
+ uintptr_t end = std::min(
+ upperEnd, reinterpret_cast<uintptr_t>(sysInfo.lpMaximumApplicationAddress));
+
+ LPVOID buffer = nullptr;
+ for (uintptr_t cur = start; cur < end; cur += sysInfo.dwPageSize) {
+ buffer = VirtualAlloc(reinterpret_cast<LPVOID>(cur), m_BufferSize,
+ MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
+ if (buffer != nullptr) {
+ break;
+ }
+ }
+ if (buffer == nullptr) {
+ throw std::runtime_error("failed to allocate buffer in range");
+ }
+
+ // the caller must have looked up the bufferlist in order to determine that a
+ // buffer has to be allocated
+ assert(iter != m_Buffers.end());
+
+ iter->second.offset = 0;
+ iter->second.buffers.push_back(buffer);
+ spdlog::get("usvfs")->debug(
+ "allocated trampoline buffer for jumps between {0:p} and {1:x} at {2:p}"
+ "(size {3})",
+ rounded, (reinterpret_cast<uintptr_t>(rounded) + m_SearchRange), buffer,
+ m_BufferSize);
+ return iter;
+}
+
+LPVOID TrampolinePool::barrier(LPVOID function)
+{
+ DWORD err = GetLastError();
+ LPVOID res = instance().barrierInt(function);
+ SetLastError(err);
+ return res;
+}
+
+LPVOID TrampolinePool::release(LPVOID function)
+{
+ DWORD err = GetLastError();
+ LPVOID res = instance().releaseInt(function);
+ SetLastError(err);
+ return res;
+}
+
+LPVOID TrampolinePool::barrierInt(LPVOID func)
+{
+ if (m_FullBlock) {
+ return nullptr;
+ }
+
+ if (m_ThreadGuards.get() == nullptr) {
+ m_ThreadGuards.reset(new TThreadMap());
+ }
+
+ auto iter = m_ThreadGuards->find(func);
+ if ((iter == m_ThreadGuards->end()) || (iter->second == nullptr)) {
+ (*m_ThreadGuards)[func] = reinterpret_cast<LPVOID>(1);
+ return &(*m_ThreadGuards)[func];
+ } else {
+ return nullptr;
+ }
+}
+
+LPVOID TrampolinePool::releaseInt(LPVOID func)
+{
+ DWORD lastError = GetLastError();
+ if (m_ThreadGuards.get() == nullptr) {
+ m_ThreadGuards.reset(new TThreadMap());
+ }
+
+ auto iter = m_ThreadGuards->find(func);
+ if (iter == m_ThreadGuards->end()) {
+ spdlog::get("hooks")->error("failed to release barrier for func {}", func);
+ ::SetLastError(lastError);
+ return nullptr;
+ }
+
+ LPVOID res = (*m_ThreadGuards)[func];
+ (*m_ThreadGuards)[func] = nullptr;
+
+ ::SetLastError(lastError);
+ return res;
+}
+
+} // namespace HookLib
diff --git a/libs/usvfs/src/thooklib/ttrampolinepool.h b/libs/usvfs/src/thooklib/ttrampolinepool.h
new file mode 100644
index 0000000..5b44c51
--- /dev/null
+++ b/libs/usvfs/src/thooklib/ttrampolinepool.h
@@ -0,0 +1,222 @@
+/*
+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 "asmjit_sane.h"
+#include <logging.h>
+#include <map>
+#include <vector>
+#include <windows_sane.h>
+
+#ifdef _MSC_VER
+#pragma warning(disable : 4714)
+#include <boost/config/compiler/visualc.hpp>
+#else
+#include <boost/config/compiler/gcc.hpp>
+#endif
+#include <boost/predef.h>
+#include <boost/thread.hpp>
+#include <mutex>
+
+// #include <boost/thread/mutex.hpp>
+
+namespace HookLib
+{
+
+///
+/// trampolines are runtime-generated mini-functions that are used to call the
+/// original code of a function
+///
+class TrampolinePool
+{
+public:
+ /// Call initialize before you use the TrampolinePool
+ static void initialize();
+
+ static TrampolinePool& instance()
+ {
+ // This is a very very sensitive place so we want to keep this function as simple as
+ // possible and having it inlined probably cann't hurt
+ return *s_Instance;
+ }
+
+ void setBlock(bool block);
+
+ ///
+ /// store a stub without moving code from the original function. This is used in cases
+ /// where the hook can be placed without overwriting logic (i.e. hot-patchable
+ /// functions and when chaining hooks)
+ /// \param reroute the stub function to call before the regular function (on x86 this
+ /// needs to be cdecl calling convention!)
+ /// \param original the original function
+ /// \param returnAddress address under which the original functionality can be
+ /// reached.
+ /// for the first hook this should be (original + 2), otherwise
+ /// the address of the next hook in the chain
+ /// \return address of the created trampoline function
+ ///
+ LPVOID storeStub(LPVOID reroute, LPVOID original, LPVOID returnAddress);
+
+ ///
+ /// store a stub, moving part of the original function to the trampoline
+ /// \param reroute the stub function to call before the regular function (on x86 this
+ /// needs to be cdecl calling convention!)
+ /// \param original the original function
+ /// \param preambleSize number of bytes from the original function to backup. Needs to
+ /// correspond to complete instructions
+ /// \param rerouteOffset offset in bytes from the created trampoline to the preamble
+ /// that leads back to the original code
+ /// \return address of the created trampoline function
+ ///
+ LPVOID storeStub(LPVOID reroute, LPVOID original, size_t preambleSize,
+ size_t* rerouteOffset);
+
+ ///
+ /// store a trampoline for hot-patchable functions, where the original function
+ /// is unharmed.
+ /// \param reroute the reroute function
+ /// \param original original function
+ /// \param returnAddress address under which the original functionality can be
+ /// reached.
+ /// \return address of the trampoline function
+ ///
+ LPVOID storeTrampoline(LPVOID reroute, LPVOID original, LPVOID returnAddress);
+
+ ///
+ /// store a trampoline, copying a part of the original function to the trampoline.
+ /// This is used for the case where the hooking mechanism needs to overwrite part of
+ /// the function
+ /// \param reroute the reroute function
+ /// \param original original function
+ /// \param preambleSize number of bytes from the original function to backup. Needs to
+ /// correspond to complete instructions
+ /// \param rerouteOffset offset in bytes from the created trampoline to the preamble
+ /// that leads us back to the original code
+ /// \return address of the trampoline function
+ ///
+ LPVOID storeTrampoline(LPVOID reroute, LPVOID original, size_t preambleSize,
+ size_t* rerouteOffset);
+
+ ///
+ /// \param addressNear used to find a trampoline buffer near the jump instruction
+ /// \return retrieve address of current trampoline buffer
+ ///
+ LPVOID currentBufferAddress(LPVOID addressNear);
+
+ ///
+ /// \brief forces the barrier(s) for the current thread to be released
+ ///
+ void forceUnlockBarrier();
+
+private:
+ struct BufferList
+ {
+ size_t offset;
+ std::vector<LPVOID> buffers;
+ };
+
+ typedef std::map<LPVOID, BufferList> BufferMap;
+ static const intptr_t ADDRESS_MASK =
+ 0xFFFFFFFFFF000000LL; // mask to "round" addresses to consolidate near
+ // trampolines
+
+private:
+ TrampolinePool();
+
+ TrampolinePool& operator=(const TrampolinePool& reference); // not implemented
+
+ /**
+ * @brief allocates a buffer with read, write and execute rights near the
+ * specified adress. The purpose is that we want to be able to jump from
+ * adressNear to generated code with a 5-byte jump, even on x64 systems.
+ * @param addressNear the reference adress
+ * @note the resulting buffer is stored in the m_Buffers map
+ */
+ BufferMap::iterator allocateBuffer(LPVOID addressNear);
+
+ void addBarrier(LPVOID rerouteAddr, LPVOID original, asmjit::X86Assembler& assembler);
+
+#if BOOST_ARCH_X86_64
+ void copyCode(asmjit::X86Assembler& assembler, LPVOID source, size_t numBytes);
+#endif // BOOST_ARCH_X86_64
+
+ BufferList& getBufferList(LPVOID address);
+
+ LPVOID roundAddress(LPVOID address) const;
+
+public:
+ static LPVOID __stdcall barrier(LPVOID function);
+ static LPVOID __stdcall release(LPVOID function);
+
+ LPVOID barrierInt(LPVOID function);
+ LPVOID releaseInt(LPVOID function);
+
+ void addCallToStub(asmjit::X86Assembler& assembler, LPVOID original, LPVOID reroute);
+
+private:
+ ///
+ /// \brief add a jump to an address outside the custom generated asm code (without
+ /// modifying registers)
+ /// \param assembler the assembler generator to write to
+ /// \param destination destination address
+ /// \note this currently generates a lot code on x64, may be overly complicated
+ ///
+ void addAbsoluteJump(asmjit::X86Assembler& assembler, uint64_t destination);
+
+ DWORD determinePageSize();
+
+private:
+#if BOOST_ARCH_X86_64
+ static const int SIZE_OF_JUMP = 13;
+#elif BOOST_ARCH_X86_32
+ static const int SIZE_OF_JUMP = 5;
+#endif
+
+ // the hook lib tries to allocate buffer close to the hooked functions,
+ // for 32-bits application, this often falls on 0x40000000, which is the
+ // address at which DLLs can be loaded on Windows
+ //
+ // some old-style DRM (e.g. Dragon Age 2) check that address and prevent
+ // the game from running if it's not 0x40000000, so we give them a bit of
+ // spaces before allocating our buffers
+ //
+ static constexpr uintptr_t MIN_ALLOC_ADDR = 0x40000000 + 0x200000;
+
+ static TrampolinePool* s_Instance;
+
+ bool m_FullBlock{false};
+
+ BufferMap m_Buffers;
+
+ typedef std::map<void*, void*> TThreadMap;
+ boost::thread_specific_ptr<TThreadMap> m_ThreadGuards;
+
+ LPVOID m_BarrierAddr;
+ LPVOID m_ReleaseAddr;
+
+ DWORD m_BufferSize = {1024};
+ size_t m_SearchRange;
+ uint64_t m_AddressMask;
+
+ int m_MaxTrampolineSize;
+};
+
+} // namespace HookLib
diff --git a/libs/usvfs/src/thooklib/udis86wrapper.cpp b/libs/usvfs/src/thooklib/udis86wrapper.cpp
new file mode 100644
index 0000000..1ac7332
--- /dev/null
+++ b/libs/usvfs/src/thooklib/udis86wrapper.cpp
@@ -0,0 +1,102 @@
+/*
+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 "udis86wrapper.h"
+#include "pch.h"
+#include <boost/predef.h>
+#include <shmlogger.h>
+#include <stdexcept>
+
+namespace HookLib
+{
+
+UDis86Wrapper::UDis86Wrapper()
+{
+ ud_init(&m_Obj);
+ ud_set_syntax(&m_Obj, UD_SYN_INTEL);
+#if BOOST_ARCH_X86_64
+ ud_set_mode(&m_Obj, 64);
+#else
+ ud_set_mode(&m_Obj, 32);
+#endif
+}
+
+void UDis86Wrapper::setInputBuffer(const uint8_t* buffer, size_t size)
+{
+ m_Buffer = buffer;
+ ud_set_input_buffer(&m_Obj, buffer, size);
+ ud_set_pc(&m_Obj, reinterpret_cast<uint64_t>(m_Buffer));
+}
+
+ud_t& UDis86Wrapper::obj()
+{
+ return m_Obj;
+}
+
+bool UDis86Wrapper::isRelativeJump()
+{
+ ud_mnemonic_code code = ud_insn_mnemonic(&m_Obj);
+ // all conditional jumps and loops are relative, as are unconditional jumps with an
+ // offset operand
+ return (code == UD_Ijo) || (code == UD_Ijno) || (code == UD_Ijb) ||
+ (code == UD_Ijae) || (code == UD_Ijz) || (code == UD_Ijnz) ||
+ (code == UD_Ijbe) || (code == UD_Ija) || (code == UD_Ijs) ||
+ (code == UD_Ijns) || (code == UD_Ijp) || (code == UD_Ijnp) ||
+ (code == UD_Ijl) || (code == UD_Ijge) || (code == UD_Ijle) ||
+ (code == UD_Ijg) || (code == UD_Ijcxz) || (code == UD_Ijecxz) ||
+ (code == UD_Ijrcxz) || (code == UD_Iloop) || (code == UD_Iloope) ||
+ (code == UD_Iloopne) ||
+ ((code == UD_Icall) && (ud_insn_opr(&m_Obj, 0)->type == UD_OP_JIMM)) ||
+ ((code == UD_Ijmp) && (ud_insn_opr(&m_Obj, 0)->type == UD_OP_JIMM));
+}
+
+intptr_t UDis86Wrapper::jumpOffset()
+{
+ const ud_operand_t* op = ud_insn_opr(&m_Obj, 0);
+ switch (op->size) {
+ case 8:
+ return static_cast<intptr_t>(op->lval.sbyte);
+ case 16:
+ return static_cast<intptr_t>(op->lval.sword);
+ case 32:
+ return static_cast<intptr_t>(op->lval.sdword);
+ case 64:
+ return static_cast<intptr_t>(op->lval.sqword);
+ default:
+ throw std::runtime_error("unsupported jump size");
+ }
+}
+
+uint64_t UDis86Wrapper::jumpTarget()
+{
+ // TODO: assert we're actually on a jump
+
+ uint64_t res = ud_insn_off(&m_Obj) + ud_insn_len(&m_Obj);
+
+ res += jumpOffset();
+
+ if (ud_insn_opr(&m_Obj, 0)->base == UD_R_RIP) {
+ res = *reinterpret_cast<uintptr_t*>(res);
+ }
+
+ return res;
+}
+
+} // namespace HookLib
diff --git a/libs/usvfs/src/thooklib/udis86wrapper.h b/libs/usvfs/src/thooklib/udis86wrapper.h
new file mode 100644
index 0000000..48d8a42
--- /dev/null
+++ b/libs/usvfs/src/thooklib/udis86wrapper.h
@@ -0,0 +1,62 @@
+/*
+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 <udis86.h>
+#undef inline // libudis86/types.h defines inline to __inline which is no longer legal
+ // since vs2012
+
+namespace HookLib
+{
+
+class UDis86Wrapper
+{
+
+public:
+ UDis86Wrapper();
+
+ void setInputBuffer(const uint8_t* buffer, size_t size);
+
+ ud_t& obj();
+
+ operator ud_t*() { return &m_Obj; }
+
+ bool isRelativeJump();
+
+ intptr_t jumpOffset();
+
+ ///
+ /// determines the absolute jump target at the current instruction, taking into
+ /// account relative instructions of all sizes and RIP-relative addressing.
+ /// \return absolute address of the jump at the current disassembler instruction
+ /// \note this works correctly ONLY if the input buffer has been set with
+ /// setInputBuffer or
+ /// if ud_set_pc has been called
+ ///
+ uint64_t jumpTarget();
+
+private:
+private:
+ ud_t m_Obj;
+ const uint8_t* m_Buffer{nullptr};
+};
+
+} // namespace HookLib
diff --git a/libs/usvfs/src/thooklib/utility.cpp b/libs/usvfs/src/thooklib/utility.cpp
new file mode 100644
index 0000000..124141f
--- /dev/null
+++ b/libs/usvfs/src/thooklib/utility.cpp
@@ -0,0 +1,86 @@
+/*
+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 "utility.h"
+#include "exceptionex.h"
+#include <cstdlib>
+
+namespace HookLib
+{
+
+FARPROC MyGetProcAddress(HMODULE module, LPCSTR functionName)
+{
+ // determine position of the exports of the module
+ PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)module;
+ if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
+ return nullptr;
+ }
+
+ PIMAGE_NT_HEADERS ntHeaders =
+ (PIMAGE_NT_HEADERS)(((LPBYTE)dosHeader) + dosHeader->e_lfanew);
+ if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
+ return nullptr;
+ }
+
+ PIMAGE_OPTIONAL_HEADER optionalHeader = &ntHeaders->OptionalHeader;
+ if (optionalHeader->NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT) {
+ return nullptr;
+ }
+ PIMAGE_DATA_DIRECTORY dataDirectory =
+ &optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
+ PIMAGE_EXPORT_DIRECTORY exportDirectory =
+ (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)dosHeader + dataDirectory->VirtualAddress);
+
+ ULONG* addressOfNames = (ULONG*)((LPBYTE)module + exportDirectory->AddressOfNames);
+ ULONG* funcAddr = (ULONG*)((LPBYTE)module + exportDirectory->AddressOfFunctions);
+
+ // search exports for the specified name
+ for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) {
+ char* curFunctionName = (char*)((LPBYTE)module + addressOfNames[i]);
+ USHORT* nameOrdinals =
+ (USHORT*)((LPBYTE)module + exportDirectory->AddressOfNameOrdinals);
+ if (strcmp(functionName, curFunctionName) == 0) {
+ if (funcAddr[nameOrdinals[i]] >= dataDirectory->VirtualAddress &&
+ funcAddr[nameOrdinals[i]] <
+ dataDirectory->VirtualAddress + dataDirectory->Size) {
+ char* forwardLibName = _strdup((LPSTR)module + funcAddr[nameOrdinals[i]]);
+ ON_BLOCK_EXIT([forwardLibName]() {
+ free(forwardLibName);
+ });
+ char* forwardFunctionName = strchr(forwardLibName, '.');
+ *forwardFunctionName = 0;
+ ++forwardFunctionName;
+
+ HMODULE forwardLib = LoadLibraryA(forwardLibName);
+ FARPROC forward = nullptr;
+ if (forwardLib != nullptr) {
+ forward = MyGetProcAddress(forwardLib, forwardFunctionName);
+ FreeLibrary(forwardLib);
+ }
+
+ return forward;
+ }
+ return (FARPROC)((LPBYTE)module + funcAddr[nameOrdinals[i]]);
+ }
+ }
+ return nullptr;
+}
+
+} // namespace HookLib
diff --git a/libs/usvfs/src/thooklib/utility.h b/libs/usvfs/src/thooklib/utility.h
new file mode 100644
index 0000000..e0be9f7
--- /dev/null
+++ b/libs/usvfs/src/thooklib/utility.h
@@ -0,0 +1,35 @@
+/*
+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 <windows_sane.h>
+
+namespace HookLib
+{
+
+/// \brief reimplementation of GetProcAddress to circumvent foreign hooks of
+/// GetProcAddress like AcLayer
+/// \param module handle to the module that contains the function or variable
+/// \param functionName function to retrieve the address of
+/// \return address of the exported function
+FARPROC MyGetProcAddress(HMODULE module, LPCSTR functionName);
+
+} // namespace HookLib
diff --git a/libs/usvfs/src/tinjectlib/CMakeLists.txt b/libs/usvfs/src/tinjectlib/CMakeLists.txt
new file mode 100644
index 0000000..096608e
--- /dev/null
+++ b/libs/usvfs/src/tinjectlib/CMakeLists.txt
@@ -0,0 +1,8 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(asmjit CONFIG REQUIRED)
+
+add_library(tinjectlib STATIC asmjit_sane.h injectlib.cpp injectlib.h)
+target_link_libraries(tinjectlib PRIVATE shared asmjit::asmjit)
+target_include_directories(tinjectlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+set_target_properties(tinjectlib PROPERTIES FOLDER injection)
diff --git a/libs/usvfs/src/tinjectlib/asmjit_sane.h b/libs/usvfs/src/tinjectlib/asmjit_sane.h
new file mode 100644
index 0000000..70ee117
--- /dev/null
+++ b/libs/usvfs/src/tinjectlib/asmjit_sane.h
@@ -0,0 +1,32 @@
+/*
+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
+
+#pragma warning(push, 3)
+#pragma warning(disable : 4201)
+#pragma warning(disable : 4244)
+#pragma warning(disable : 4245)
+// #ifndef ASMJIT_API
+// #define ASMJIT_API
+// #endif // ASMJIT_API
+#include <asmjit/asmjit.h>
+#include <asmjit/x86.h>
+#pragma warning(pop)
diff --git a/libs/usvfs/src/tinjectlib/injectlib.cpp b/libs/usvfs/src/tinjectlib/injectlib.cpp
new file mode 100644
index 0000000..2c7362a
--- /dev/null
+++ b/libs/usvfs/src/tinjectlib/injectlib.cpp
@@ -0,0 +1,482 @@
+/*
+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 <cstdio>
+#include <stdexcept>
+
+#include <boost/filesystem.hpp>
+#include <boost/predef.h>
+#include <boost/static_assert.hpp>
+#include <boost/type_traits.hpp>
+namespace fs = boost::filesystem;
+
+#include "injectlib.h"
+#include <addrtools.h>
+#include <exceptionex.h>
+#include <stringcast.h>
+#include <stringutils.h>
+// local version of asmjit with warning suppression
+#include "asmjit_sane.h"
+#include <TlHelp32.h>
+#include <spdlog/spdlog.h>
+
+using namespace asmjit;
+using namespace usvfs::shared;
+
+#if BOOST_ARCH_X86_64
+#pragma message("64bit build")
+using namespace x86;
+#elif BOOST_ARCH_X86_32
+#pragma message("32bit build")
+using namespace asmjit::x86;
+#else
+#error "unsupported architecture"
+#endif
+
+typedef HMODULE(WINAPI* TLoadLibraryType)(LPCWSTR);
+typedef FARPROC(WINAPI* TGetProcAddressType)(HMODULE, LPCSTR);
+typedef DWORD(WINAPI* TGetLastErrorType)();
+
+typedef BOOL(WINAPI* TSetXStateFeaturesMaskType)(PCONTEXT, DWORD64);
+
+static const size_t MAX_FUNCTIONAME = 20;
+
+struct TDataRemote
+{
+ TLoadLibraryType loadLibrary;
+ TGetProcAddressType getProcAddress;
+ TGetLastErrorType getLastError;
+ REGWORD returnAddress;
+
+ char initFunction[MAX_FUNCTIONAME + 1];
+ WCHAR dllName[MAX_PATH];
+};
+
+#if BOOST_ARCH_X86_64
+void pushAll(X86Assembler& assembler)
+{
+ assembler.pushf();
+ assembler.push(rax);
+ assembler.push(rcx);
+ assembler.push(rdx);
+ assembler.push(rbx);
+ assembler.push(rbp);
+ assembler.push(rsi);
+ assembler.push(rdi);
+ assembler.push(r8);
+ assembler.push(r9);
+ assembler.push(r10);
+ assembler.push(r11);
+ assembler.push(r12);
+ assembler.push(r13);
+ assembler.push(r14);
+ assembler.push(r15);
+}
+
+void popAll(X86Assembler& assembler)
+{
+ assembler.pop(r15);
+ assembler.pop(r14);
+ assembler.pop(r13);
+ assembler.pop(r12);
+ assembler.pop(r11);
+ assembler.pop(r10);
+ assembler.pop(r9);
+ assembler.pop(r8);
+ assembler.pop(rdi);
+ assembler.pop(rsi);
+ assembler.pop(rbp);
+ assembler.pop(rbx);
+ assembler.pop(rdx);
+ assembler.pop(rcx);
+ assembler.pop(rax);
+ assembler.popf();
+}
+#endif // BOOST_ARCH_X86_64
+
+void addStub(size_t userDataSize, X86Assembler& assembler, bool skipInit,
+ TDataRemote* localData, TDataRemote* remoteData, LPCSTR initFunction)
+{
+ Label Label_DLLLoaded = assembler.newLabel();
+
+#if BOOST_ARCH_X86_64
+ pushAll(assembler);
+ // call load library for the actual injection
+ assembler.mov(rcx, imm(reinterpret_cast<int64_t>(&remoteData->dllName)));
+ assembler.mov(rax, imm((intptr_t)(void*)localData->loadLibrary));
+ assembler.sub(rsp, 32);
+ assembler.call(rax);
+ assembler.add(rsp, 32);
+
+ // cancel out of here if we failed to load the dll
+ // TODO: would be great to report this error. But how?
+ assembler.test(rax, rax);
+ assembler.jnz(Label_DLLLoaded);
+ /* this commented out code may seem pointless but it is a simple way to get at the
+ error code when debugging. assembler.mov(rax,
+ imm((intptr_t)(void*)localData->getLastError)); assembler.sub(rsp, 32);
+ assembler.call(rax);
+ assembler.add(rsp, 32);
+ assembler.int3();*/
+ popAll(assembler);
+ assembler.ret();
+ assembler.bind(Label_DLLLoaded);
+
+ // determine address of the init function
+ if (initFunction != nullptr) {
+ Label Label_SkipInit = assembler.newLabel();
+ assembler.mov(rcx, rax); // handle of the dll
+ assembler.mov(rdx, imm(reinterpret_cast<int64_t>(
+ remoteData->initFunction))); // name of init function
+ assembler.mov(rax, imm((intptr_t)(void*)localData->getProcAddress));
+ assembler.sub(rsp, 32);
+ assembler.call(rax);
+ assembler.add(rsp, 32);
+
+ if (skipInit) {
+ assembler.test(rax, rax);
+ assembler.jz(Label_SkipInit);
+ }
+
+ // call the init function with user data
+ assembler.mov(rcx,
+ imm(reinterpret_cast<int64_t>(remoteData) + sizeof(TDataRemote)));
+ assembler.mov(rdx, imm(static_cast<int64_t>(userDataSize)));
+ assembler.sub(rsp, 32);
+ assembler.call(rax);
+ assembler.add(rsp, 32);
+ assembler.bind(Label_SkipInit);
+ }
+
+ // restore registers
+ popAll(assembler);
+#else
+
+ // save registers
+ assembler.push(eax);
+ assembler.pushf();
+
+ // call load library for the actual injection
+ assembler.push(imm(void_ptr_cast<int64_t>(remoteData->dllName)));
+ // assembler.call(ptr_abs(static_cast<void*>(remoteData->loadLibrary)));
+ assembler.mov(eax, imm(void_ptr_cast<int64_t>(localData->loadLibrary)));
+ assembler.call(eax);
+
+ assembler.test(eax, eax);
+ assembler.jnz(Label_DLLLoaded);
+ /* this commented out code may seem pointless but it is a simple way to get at the
+ error code when debugging. assembler.mov(eax,
+ imm((intptr_t)(void*)localData->getLastError)); assembler.call(eax);
+ assembler.int3();*/
+ assembler.popf();
+ assembler.pop(eax);
+ assembler.ret();
+ assembler.bind(Label_DLLLoaded);
+
+ // determine address of the init function
+ if (initFunction != nullptr) {
+ Label Label_SkipInit = assembler.newLabel();
+ assembler.push(imm(
+ void_ptr_cast<int64_t>(remoteData->initFunction))); // name of init function
+ assembler.push(eax); // handle of the dll
+ assembler.mov(eax, imm(void_ptr_cast<int64_t>(localData->getProcAddress)));
+ assembler.call(eax);
+ if (skipInit) {
+ assembler.cmp(eax, 0);
+ assembler.jz(Label_SkipInit);
+ } else {
+ assembler.cmp(eax, 0);
+ assembler.jnz(Label_SkipInit);
+ // heading for a crash! give an attached debugger a chance to analyse the error
+ assembler.mov(eax, imm(void_ptr_cast<int64_t>(localData->getLastError)));
+ assembler.call(eax);
+ assembler.int3();
+ assembler.bind(Label_SkipInit);
+ }
+
+ // call the init function with user data
+ assembler.push(userDataSize);
+ assembler.push(imm(void_ptr_cast<int64_t>(remoteData) + sizeof(TDataRemote)));
+ assembler.call(eax);
+ // init function is declared __cdecl so we have to remove parameters from the stack
+ assembler.pop(eax);
+ assembler.pop(eax);
+ if (skipInit) {
+ assembler.bind(Label_SkipInit);
+ }
+ }
+
+ // restore registers
+ assembler.popf();
+ assembler.pop(eax);
+
+#endif
+}
+
+REGWORD WriteInjectionStub(HANDLE processHandle, LPCWSTR dllName, LPCSTR initFunction,
+ LPCVOID userData, size_t userDataSize, bool skipInit,
+ REGWORD returnAddress)
+{
+ HMODULE k32mod = ::LoadLibrary(__TEXT("kernel32.dll"));
+ TDataRemote data = {0};
+
+ if (k32mod != nullptr) {
+ data.loadLibrary =
+ reinterpret_cast<TLoadLibraryType>(GetProcAddress(k32mod, "LoadLibraryW"));
+ data.getProcAddress =
+ reinterpret_cast<TGetProcAddressType>(GetProcAddress(k32mod, "GetProcAddress"));
+ data.getLastError =
+ reinterpret_cast<TGetLastErrorType>(GetProcAddress(k32mod, "GetLastError"));
+ if ((data.loadLibrary == nullptr) || (data.getProcAddress == nullptr) ||
+ (data.getLastError == nullptr)) {
+ throw windows_error("failed to determine address for required functions");
+ }
+ } else {
+ throw windows_error("kernel32.dll not loaded?");
+ }
+
+ data.returnAddress = returnAddress;
+
+ if (initFunction != nullptr) {
+ strncpy_s(data.initFunction, MAX_FUNCTIONAME, initFunction, MAX_FUNCTIONAME);
+ data.initFunction[MAX_FUNCTIONAME] = '\0';
+ } else {
+ data.initFunction[0] = '\0';
+ }
+
+ wcsncpy_s(data.dllName, MAX_PATH, dllName, MAX_PATH - 1);
+ data.dllName[MAX_PATH - 1] = L'\0';
+
+ size_t totalSize = sizeof(TDataRemote) + userDataSize;
+
+ // allocate memory in the target process and write the data-block there
+ LPVOID remoteMem = VirtualAllocEx(processHandle, nullptr, totalSize,
+ MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
+
+ if (remoteMem == nullptr) {
+ throw windows_error("failed to allocate memory in target process");
+ }
+
+ SIZE_T written;
+ if (!WriteProcessMemory(processHandle, remoteMem, &data, sizeof(TDataRemote),
+ &written)) {
+ throw windows_error("failed to write control data to target process");
+ }
+ if (written != sizeof(TDataRemote)) {
+ throw windows_error("failed to write whole control data to target process");
+ }
+
+ // write user data to remote memory if necessary
+ if (userData != nullptr) {
+ if (!WriteProcessMemory(processHandle, AddrAdd(remoteMem, sizeof(TDataRemote)),
+ userData, userDataSize, &written)) {
+ throw windows_error("failed to write user data to target process");
+ }
+ if (written != userDataSize) {
+ throw windows_error("failed to write whole user data to target process");
+ }
+ }
+
+ TDataRemote* remoteData = reinterpret_cast<TDataRemote*>(remoteMem);
+
+ // now for the interesting part: write a stub into the target process that is run
+ // before any code of the original binary.
+
+ JitRuntime runtime;
+#if BOOST_ARCH_X86_64
+ X86Assembler assembler(&runtime);
+ if (returnAddress != 0) {
+ // put return address on the stack
+ // (this damages rax which hopefully doesn't matter)
+ assembler.mov(rax, imm((intptr_t)(void*)data.returnAddress));
+ assembler.push(rax);
+ } // otherwise no return address was specified here. It better be on the stack
+ // already
+#else
+ X86Assembler assembler(&runtime);
+ if (returnAddress != 0) {
+ assembler.push(imm((intptr_t)(void*)data.returnAddress));
+ }
+#endif
+
+ addStub(userDataSize, assembler, skipInit, &data, remoteData, initFunction);
+ assembler.ret(0);
+
+ size_t stubSize = assembler.getCodeSize();
+
+ // reserve memory for the stub
+ PBYTE stubRemote = reinterpret_cast<PBYTE>(
+ VirtualAllocEx(processHandle, nullptr, stubSize, MEM_COMMIT | MEM_RESERVE,
+ PAGE_EXECUTE_READWRITE));
+ if (stubRemote == nullptr) {
+ throw windows_error("failed to allocate memory for stub");
+ }
+
+ // almost there. copy stub to target process
+ if (!WriteProcessMemory(processHandle, stubRemote, assembler.getBuffer(), stubSize,
+ &written) ||
+ (written != stubSize)) {
+ throw windows_error("failed to write stub to target process");
+ }
+
+ return reinterpret_cast<REGWORD>(stubRemote);
+}
+
+void InjectDLLEIP(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName,
+ LPCSTR initFunction, LPCVOID userData, size_t userDataSize,
+ bool skipInit)
+{
+ threadHandle =
+ OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME),
+ FALSE, GetThreadId(threadHandle));
+
+ if (threadHandle == nullptr) {
+ throw windows_error("failed to open thread");
+ }
+
+ CONTEXT threadContext;
+ threadContext.ContextFlags = CONTEXT_CONTROL;
+
+ // documentation says starting with Win7 SP1 you HAVE to call SetXStateFeaturesMask
+ HMODULE k32mod = ::LoadLibrary(__TEXT("kernel32.dll"));
+ if (k32mod == nullptr) {
+ throw windows_error("failed to load kernel32.dll");
+ }
+ TSetXStateFeaturesMaskType sxsfm = reinterpret_cast<TSetXStateFeaturesMaskType>(
+ GetProcAddress(k32mod, "SetXStateFeaturesMask"));
+ if (sxsfm != nullptr) {
+ sxsfm(&threadContext, 0);
+ }
+ ::FreeLibrary(k32mod);
+
+ if (GetThreadContext(threadHandle, &threadContext) == 0) {
+ throw windows_error("failed to access thread context.");
+ }
+
+#if BOOST_ARCH_X86_64
+ REGWORD returnAddress = threadContext.Rip;
+#else
+ REGWORD returnAddress = threadContext.Eip;
+#endif
+
+ REGWORD stubAddress =
+ WriteInjectionStub(processHandle, dllName, initFunction, userData, userDataSize,
+ skipInit, returnAddress);
+
+ // make the stub the new next thing for the thread to execute
+#if BOOST_ARCH_X86_64
+ threadContext.Rip = stubAddress;
+#else
+ threadContext.Eip = stubAddress;
+#endif
+
+ if (SetThreadContext(threadHandle, &threadContext) == 0) {
+ throw windows_error("failed to overwrite thread context");
+ }
+}
+
+void InjectDLLRemoteThread(HANDLE processHandle, LPCWSTR dllName, LPCSTR initFunction,
+ LPCVOID userData, size_t userDataSize, bool skipInit)
+{
+ REGWORD stubAddress = WriteInjectionStub(processHandle, dllName, initFunction,
+ userData, userDataSize, skipInit, 0);
+
+ DWORD threadId = 0UL;
+ HANDLE threadHandle = CreateRemoteThread(
+ processHandle, nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(stubAddress),
+ nullptr, 0, &threadId);
+ if (threadHandle == nullptr) {
+ throw windows_error("failed to start remote thread");
+ }
+ ResumeThread(threadHandle);
+ spdlog::get("usvfs")->info("waiting for {0:x} to complete",
+ GetThreadId(threadHandle));
+ ::WaitForSingleObject(threadHandle, 100);
+ ::CloseHandle(threadHandle);
+}
+
+void InjectLib::InjectDLL(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName,
+ LPCSTR initFunction, LPCVOID userData, size_t userDataSize,
+ bool skipInit)
+{
+ namespace bfs = boost::filesystem;
+ if (!exists(bfs::path(dllName))) {
+ USVFS_THROW_EXCEPTION(file_not_found_error()
+ << ex_msg(string_cast<std::string>(dllName)));
+ }
+ if (threadHandle == INVALID_HANDLE_VALUE) {
+#pragma message( \
+ "doesn't seem to work as usvfs causes an exception in the first static initialization or pretty much on any function call. Because process is in different session? CRT related?")
+ /*
+ InjectDLLRemoteThread(processHandle, dllName,
+ initFunction, userData, userDataSize, skipInit);
+ */
+
+ DWORD pid = GetProcessId(processHandle);
+ HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
+ THREADENTRY32 threadInfo;
+ threadInfo.dwSize = sizeof(THREADENTRY32);
+ BOOL moreThreads = Thread32First(snapshot, &threadInfo);
+ std::vector<HANDLE> threadHandles;
+ HANDLE injectThread = INVALID_HANDLE_VALUE;
+ FILETIME injectThreadTime;
+ spdlog::get("usvfs")->info("inject dll to process {0}", pid);
+ while (moreThreads) {
+ if (threadInfo.th32OwnerProcessID == pid) {
+ HANDLE thread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, threadInfo.th32ThreadID);
+
+ if (thread != nullptr) {
+ DWORD suspCount = SuspendThread(thread);
+ if (suspCount == 0) {
+ FILETIME creationTime, exitTime, kernelTime, userTime;
+ ::GetThreadTimes(thread, &creationTime, &exitTime, &kernelTime, &userTime);
+
+ if ((injectThread == INVALID_HANDLE_VALUE) ||
+ (CompareFileTime(&creationTime, &injectThreadTime) < 0)) {
+ spdlog::get("usvfs")->info("candidate for oldest thread: {0}",
+ threadInfo.th32ThreadID);
+ injectThread = thread;
+ injectThreadTime = creationTime;
+ }
+ }
+ threadHandles.push_back(thread);
+ }
+ }
+ moreThreads = Thread32Next(snapshot, &threadInfo);
+ }
+ if (injectThread != INVALID_HANDLE_VALUE) {
+ spdlog::get("usvfs")->debug("going to inject dll");
+ InjectDLLEIP(processHandle, injectThread, dllName, initFunction, userData,
+ userDataSize, skipInit);
+ } else {
+ spdlog::get("usvfs")->critical("found no thread to use for injecting");
+ }
+
+ for (HANDLE hdl : threadHandles) {
+ spdlog::get("usvfs")->info("resuming thread {0}", ::GetThreadId(hdl));
+ ResumeThread(hdl);
+ CloseHandle(hdl);
+ }
+ CloseHandle(snapshot);
+ } else {
+ InjectDLLEIP(processHandle, threadHandle, dllName, initFunction, userData,
+ userDataSize, skipInit);
+ }
+}
diff --git a/libs/usvfs/src/tinjectlib/injectlib.h b/libs/usvfs/src/tinjectlib/injectlib.h
new file mode 100644
index 0000000..f165667
--- /dev/null
+++ b/libs/usvfs/src/tinjectlib/injectlib.h
@@ -0,0 +1,50 @@
+/*
+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 <windows_sane.h>
+
+namespace InjectLib
+{
+
+/**
+ * @brief inject a dll into the target process
+ * @param processHandle handle of the process to inject to
+ * @param threadHandle handle of the main thread in the process
+ * @param dllname name/path of the dll to inject. The path can't be longer than MAX_PATH
+ * characters
+ * @param initFunction name of the initialization function. Can't be longer than 20
+ * characters. If this is null, no function is called. Important: the init function has
+ * to exist, be exported, take the two userdata parameters and must be __cdecl calling
+ * convention on 32bit windows! Failing any of these the target process will crash or
+ * the dll isn't loaded Why __cdecl? Because otherwise we would need .def files to
+ * export the init function with a GetProcAddress-able function name.
+ * @param userData data passed to the init function
+ * @param userDataSize size of the data to be passed
+ * @param skipInit skip the call to the init function if the named function wasn't found
+ * in the dll. If false, the target process will crash if the function isn't exported in
+ * the dll
+ */
+void InjectDLL(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName,
+ LPCSTR initFunction = nullptr, LPCVOID userData = nullptr,
+ size_t userDataSize = 0, bool skipInit = false);
+
+} // namespace InjectLib
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
diff --git a/libs/usvfs/src/usvfs_helper/CMakeLists.txt b/libs/usvfs/src/usvfs_helper/CMakeLists.txt
new file mode 100644
index 0000000..5e083d9
--- /dev/null
+++ b/libs/usvfs/src/usvfs_helper/CMakeLists.txt
@@ -0,0 +1,6 @@
+cmake_minimum_required(VERSION 3.16)
+
+add_library(usvfs_helper STATIC inject.h inject.cpp)
+target_link_libraries(usvfs_helper PUBLIC shared PRIVATE tinjectlib)
+target_include_directories(usvfs_helper PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+set_target_properties(usvfs_helper PROPERTIES FOLDER injection)
diff --git a/libs/usvfs/src/usvfs_helper/inject.cpp b/libs/usvfs/src/usvfs_helper/inject.cpp
new file mode 100644
index 0000000..bb1cb58
--- /dev/null
+++ b/libs/usvfs/src/usvfs_helper/inject.cpp
@@ -0,0 +1,193 @@
+/*
+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 <string>
+#include <utility>
+
+#include <spdlog/spdlog.h>
+
+#include <boost/filesystem.hpp>
+
+#include "inject.h"
+#include <exceptionex.h>
+#include <formatters.h>
+#include <injectlib.h>
+#include <loghelpers.h>
+#include <pch.h>
+#include <stringcast.h>
+#include <stringutils.h>
+#include <usvfsparametersprivate.h>
+#include <winapi.h>
+
+namespace ush = usvfs::shared;
+
+using namespace winapi;
+
+void usvfs::injectProcess(const std::wstring& applicationPath,
+ const usvfsParameters& parameters,
+ const PROCESS_INFORMATION& processInfo)
+{
+ injectProcess(applicationPath, parameters, processInfo.hProcess, processInfo.hThread);
+}
+
+void usvfs::injectProcess(const std::wstring& applicationPath,
+ const usvfsParameters& parameters, HANDLE processHandle,
+ HANDLE threadHandle)
+{
+ bool proc64 = false;
+ bool sameBitness = false;
+ {
+ SYSTEM_INFO info;
+ GetSystemInfo(&info);
+ BOOL wow64;
+ IsWow64Process(processHandle, &wow64);
+ if (wow64) {
+ // process is running under wow64 so it has to be a 32bit process running on 64bit
+ // windows
+ proc64 = false;
+ BOOL temp;
+ IsWow64Process(GetCurrentProcess(), &temp);
+ sameBitness = temp == TRUE;
+ } else {
+ BOOL selfWow64;
+ IsWow64Process(GetCurrentProcess(), &selfWow64);
+ if (selfWow64) {
+ // WE are a 32 bit process running on 64bit windows. the other process isn't, so
+ // its 64bit
+ proc64 = true;
+ } else {
+ sameBitness = true;
+ // we have the same bitness as that other process, but which is it?
+#ifdef _WIN64
+ proc64 = true;
+#else
+ proc64 = false;
+#endif
+ }
+ }
+ }
+ boost::filesystem::path binPath = boost::filesystem::path(applicationPath);
+ spdlog::get("usvfs")->info("injecting to process {} with {} bitness",
+ ::GetProcessId(processHandle),
+ sameBitness ? "same" : "different");
+
+ if (sameBitness) {
+ static constexpr auto USVFS_DLL =
+#ifdef _WIN64
+ L"usvfs_x64.dll";
+#else
+ L"usvfs_x86.dll";
+#endif
+ const auto& preferedDll = binPath / USVFS_DLL;
+ boost::filesystem::path dllPath = preferedDll;
+ bool dllFound = boost::filesystem::exists(dllPath);
+ // support for runing tests using a usvfs dll in lib folder (and proxy under bin):
+ if (!dllFound && binPath.filename() == L"bin") {
+ dllPath = binPath.parent_path() / L"lib" / USVFS_DLL;
+ dllFound = boost::filesystem::exists(dllPath);
+ }
+ if (!dllFound) {
+ USVFS_THROW_EXCEPTION(
+ file_not_found_error()
+ << ex_msg(std::string("dll missing: ") +
+ ush::string_cast<std::string>(preferedDll.wstring()).c_str()));
+ }
+
+ spdlog::get("usvfs")->info("dll path: {}", dllPath.wstring());
+
+ InjectLib::InjectDLL(processHandle, threadHandle, dllPath.c_str(), "InitHooks",
+ &parameters, sizeof(parameters));
+
+ spdlog::get("usvfs")->info("injection to same bitness process {} successful",
+ ::GetProcessId(processHandle));
+ } else {
+ // first try platform specific proxy exe:
+ static constexpr auto USVFS_PREFERED_EXE =
+#ifdef _WIN64
+ L"usvfs_proxy_x86.exe";
+#else
+ L"usvfs_proxy_x64.exe";
+#endif
+ const auto& preferedExe = binPath / USVFS_PREFERED_EXE;
+ boost::filesystem::path exePath = preferedExe;
+ bool exeFound = boost::filesystem::exists(exePath);
+ // support for runing tests using a usvfs dll in lib folder (and proxy under bin):
+ if (!exeFound && binPath.filename() == L"lib") {
+ exePath = binPath.parent_path() / L"bin" / USVFS_PREFERED_EXE;
+ exeFound = boost::filesystem::exists(exePath);
+ }
+ // finally fallback to old proxy naming (but only for 64bit as we don't have a 64bit
+ // proxy in this case):
+#ifdef _WIN64
+ if (!exeFound) {
+ exePath = binPath / L"usvfs_proxy.exe";
+ exeFound = boost::filesystem::exists(exePath);
+ }
+#endif
+ if (!exeFound) {
+ USVFS_THROW_EXCEPTION(file_not_found_error() << ex_msg(
+ std::string("usvfs proxy not found: ") +
+ ush::string_cast<std::string>(preferedExe.wstring())));
+ } else
+ spdlog::get("usvfs")->info("using usvfs proxy: {}",
+ ush::string_cast<std::string>(preferedExe.wstring()));
+ // need to use proxy aplication to inject
+ auto proxyProcess =
+ std::move(wide::createProcess(exePath.wstring())
+ .arg(L"--instance")
+ .arg(ush::string_cast<std::wstring>(parameters.instanceName))
+ .arg(L"--pid")
+ .arg(GetProcessId(processHandle)));
+
+ if (threadHandle != INVALID_HANDLE_VALUE) {
+ proxyProcess.arg("--tid").arg(GetThreadId(threadHandle));
+ }
+ process::Result result = proxyProcess();
+ if (!result.valid) {
+ USVFS_THROW_EXCEPTION(unknown_error()
+ << ex_msg(std::string("failed to start proxy ") +
+ ush::string_cast<std::string>(exePath.wstring()))
+ << ex_win_errcode(result.errorCode));
+ } else {
+ // wait for proxy completion. this shouldn't take long, 15 seconds is very
+ // generous
+ switch (WaitForSingleObject(result.processInfo.hProcess, 15000)) {
+ case WAIT_TIMEOUT: {
+ spdlog::get("usvfs")->debug("proxy timeout");
+ TerminateProcess(result.processInfo.hProcess, 1);
+ USVFS_THROW_EXCEPTION(timeout_error()
+ << ex_msg(std::string("proxy didn't complete in time")));
+ } break;
+ case WAIT_FAILED: {
+ spdlog::get("usvfs")->debug("proxy wait failed");
+ TerminateProcess(result.processInfo.hProcess, 1);
+ USVFS_THROW_EXCEPTION(unknown_error()
+ << ex_msg(
+ std::string("failed to wait for proxy completion"))
+ << ex_win_errcode(result.errorCode));
+ } break;
+ default: {
+ spdlog::get("usvfs")->debug("proxy run successful");
+ // nop
+ } break;
+ }
+ }
+ }
+}
diff --git a/libs/usvfs/src/usvfs_helper/inject.h b/libs/usvfs/src/usvfs_helper/inject.h
new file mode 100644
index 0000000..10f179f
--- /dev/null
+++ b/libs/usvfs/src/usvfs_helper/inject.h
@@ -0,0 +1,51 @@
+/*
+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 "usvfsparameters.h"
+#include <string>
+#include <windows_sane.h>
+
+namespace usvfs
+{
+
+/**
+ * @brief inject usvfs to a process
+ * @param applicationPath
+ * @param parameters
+ * @param processInfo
+ */
+void injectProcess(const std::wstring& applicationPath,
+ const usvfsParameters& parameters,
+ const PROCESS_INFORMATION& processInfo);
+
+/**
+ * @brief inject usvfs to a process
+ * @param applicationPath path to usvfs
+ * @param parameters
+ * @param process process handle to inject to
+ * @param thread main thread inside that process. This can be set to
+ * INVALID_HANDLE_VALUE in which case a new thread is created in the process
+ */
+void injectProcess(const std::wstring& applicationPath,
+ const usvfsParameters& parameters, HANDLE process, HANDLE thread);
+
+} // namespace usvfs
diff --git a/libs/usvfs/src/usvfs_proxy/CMakeLists.txt b/libs/usvfs/src/usvfs_proxy/CMakeLists.txt
new file mode 100644
index 0000000..47614ee
--- /dev/null
+++ b/libs/usvfs/src/usvfs_proxy/CMakeLists.txt
@@ -0,0 +1,15 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Boost CONFIG REQUIRED COMPONENTS filesystem)
+
+add_executable(usvfs_proxy main.cpp version.rc)
+target_link_libraries(usvfs_proxy PRIVATE usvfs_dll shared usvfs_helper)
+set_target_properties(usvfs_proxy
+ PROPERTIES
+ RUNTIME_OUTPUT_NAME usvfs_proxy${ARCH_POSTFIX}
+ RUNTIME_OUTPUT_DIRECTORY_DEBUG ${USVFS_BINDIR}
+ RUNTIME_OUTPUT_DIRECTORY_RELEASE ${USVFS_BINDIR}
+)
+
+install(TARGETS usvfs_proxy EXPORT usvfs${ARCH_POSTFIX}Targets)
+install(FILES $<TARGET_PDB_FILE:usvfs_proxy> DESTINATION pdb OPTIONAL)
diff --git a/libs/usvfs/src/usvfs_proxy/main.cpp b/libs/usvfs/src/usvfs_proxy/main.cpp
new file mode 100644
index 0000000..88d4696
--- /dev/null
+++ b/libs/usvfs/src/usvfs_proxy/main.cpp
@@ -0,0 +1,221 @@
+/*
+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 "pch.h"
+#include <../usvfs_dll/hookcontext.h>
+#include <Psapi.h>
+#include <WinUser.h>
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/lexical_cast.hpp>
+#include <inject.h>
+#include <shared_memory.h>
+#include <sharedparameters.h>
+#include <shmlogger.h>
+#include <spdlog/spdlog.h>
+#include <usvfsparameters.h>
+#include <winapi.h>
+
+namespace bi = boost::interprocess;
+namespace bfs = boost::filesystem;
+using usvfs::SharedParameters;
+using usvfs::shared::SharedMemoryT;
+
+template <typename T>
+T getParameter(std::vector<std::string>& arguments, const std::string& key,
+ bool consume)
+{
+ auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key);
+ if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) {
+ T result = boost::lexical_cast<T>(*(iter + 1));
+ if (consume) {
+ arguments.erase(iter, iter + 2);
+ }
+ return result;
+ } else {
+ throw std::runtime_error(std::string("argument missing " + key));
+ }
+}
+
+template <typename T>
+T getParameter(std::vector<std::string>& arguments, const std::string& key,
+ const T& def, bool consume)
+{
+ auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key);
+ if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) {
+ T result = boost::lexical_cast<T>(*(iter + 1));
+ if (consume) {
+ arguments.erase(iter, iter + 2);
+ }
+ return result;
+ } else {
+ return def;
+ }
+}
+
+static void exceptionDialog(int line, int num, ...)
+{
+ va_list args;
+ va_start(args, num);
+
+ std::wstring wstr;
+ WCHAR buf[256];
+ wstr.append(L"Unhandled USVFS proxy exception (line ");
+ wsprintf(buf, L"%d): ", line);
+ wstr.append(buf);
+ for (int i = 0; i < num; i++) {
+ wsprintf(buf, L"%S", va_arg(args, const char*));
+ if (i < num - 1)
+ wsprintf(buf, L", ");
+ wstr.append(buf);
+ }
+
+ MessageBox(NULL, wstr.data(), NULL, MB_OK);
+
+ va_end(args);
+}
+
+int main(int argc, char** argv)
+{
+ std::shared_ptr<spdlog::logger> logger;
+
+ std::vector<std::string> arguments;
+ std::copy(argv + 1, argv + argc, std::back_inserter(arguments));
+
+ std::string instance;
+ try {
+ SHMLogger::open("usvfs");
+ logger = spdlog::create<usvfs::sinks::shm_sink>("usvfs", "usvfs");
+ logger->set_pattern("%H:%M:%S.%e [%L] (proxy) %v");
+
+ instance = getParameter<std::string>(arguments, "instance", true);
+ } catch (const std::exception& e) {
+ if (logger.get() == nullptr) {
+ exceptionDialog(__LINE__, 1, e.what());
+ return 1;
+ }
+ try {
+ logger->critical("{}", e.what());
+ } catch (const spdlog::spdlog_ex& e2) {
+ exceptionDialog(__LINE__, 2, e.what(), e2.what());
+ // no way to log this
+ } catch (const std::exception&) {
+ logger->critical(e.what());
+ }
+ return 1;
+ }
+
+ try {
+ std::string executable =
+ getParameter<std::string>(arguments, "executable", "", true);
+ int pid = getParameter<int>(arguments, "pid", 0, true);
+ int tid = getParameter<int>(arguments, "tid", 0, true);
+
+ logger->info("instance: {}", instance);
+ logger->info("exe: {}", executable);
+ logger->info("pid: {}", pid);
+
+ if (executable.empty() && (pid == 0)) {
+ logger->warn("not all required settings set");
+ return 1;
+ }
+
+ SharedMemoryT configurationSHM(bi::open_only, instance.c_str());
+ if (!configurationSHM.check_sanity()) {
+ logger->warn("failed to connect to vfs");
+ return 1;
+ }
+ logger->info("size: {}", configurationSHM.get_size());
+ logger->info("addr: {0:p}", configurationSHM.get_address());
+ logger->info("objs: {}", configurationSHM.get_num_named_objects());
+ std::pair<SharedParameters*, SharedMemoryT::size_type> params =
+ configurationSHM.find<SharedParameters>("parameters");
+ if (params.first == nullptr) {
+ logger->error("failed to open shared configuration for {}", instance);
+ return 1;
+ }
+
+ boost::filesystem::path p(winapi::wide::getModuleFileName(nullptr));
+
+ if (executable.empty()) {
+ HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
+ HANDLE threadHandle = INVALID_HANDLE_VALUE;
+ if (tid != 0) {
+ threadHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, tid);
+ }
+
+ BOOL blacklisted = FALSE;
+ TCHAR szModName[MAX_PATH];
+
+ if (GetModuleFileNameEx(processHandle, NULL, szModName,
+ sizeof(szModName) / sizeof(TCHAR))) {
+ const auto appName = usvfs::shared::string_cast<std::string>(szModName);
+
+ if (params.first->executableBlacklisted(appName, {})) {
+ logger->info("not injecting {} as application is blacklisted", appName);
+ blacklisted = TRUE;
+ }
+ }
+
+ if (!blacklisted) {
+ usvfs::injectProcess(p.parent_path().wstring(), params.first->makeLocal(),
+ processHandle, threadHandle);
+ }
+ } else {
+ winapi::process::Result process =
+ winapi::ansi::createProcess(executable)
+ .arguments(arguments.begin(), arguments.end())
+ .workingDirectory(bfs::path(executable).parent_path().string())
+ .suspended()();
+
+ if (!process.valid) {
+ return 1;
+ }
+
+ BOOL blacklisted = FALSE;
+
+ if (params.first->executableBlacklisted(executable, {})) {
+ logger->info("not injecting {} as application is blacklisted", executable);
+
+ blacklisted = TRUE;
+ }
+
+ if (!blacklisted) {
+ usvfs::injectProcess(p.parent_path().wstring(), params.first->makeLocal(),
+ process.processInfo);
+ }
+
+ ResumeThread(process.processInfo.hThread);
+ }
+
+ return 0;
+ } catch (const std::exception& e) {
+ try {
+ logger->critical("unhandled exception: {}", e.what());
+ logExtInfo(e);
+ } catch (const spdlog::spdlog_ex& e2) {
+ // no way to log this
+ exceptionDialog(__LINE__, 2, e.what(), e2.what());
+ } catch (const std::exception&) {
+ logger->critical(e.what());
+ }
+ }
+}
diff --git a/libs/usvfs/src/usvfs_proxy/version.rc b/libs/usvfs/src/usvfs_proxy/version.rc
new file mode 100644
index 0000000..03652c2
--- /dev/null
+++ b/libs/usvfs/src/usvfs_proxy/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 Proxy\0"
+#ifdef _WIN64
+ VALUE "OriginalFilename", "usvfs_proxy_x64.exe\0"
+#else
+ VALUE "OriginalFilename", "usvfs_proxy_x86.exe\0"
+#endif
+ VALUE "ProductName", "USVFS\0"
+ VALUE "ProductVersion", VER_PRODUCTVERSION_STR
+ END
+ END
+
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409L, 1200
+ END
+END