diff options
Diffstat (limited to 'libs/usvfs/src/usvfs_dll/maptracker.h')
| -rw-r--r-- | libs/usvfs/src/usvfs_dll/maptracker.h | 691 |
1 files changed, 691 insertions, 0 deletions
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 |
