diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2acd535..4a41974 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,9 +8,6 @@ on: pull_request: types: [opened, synchronize, reopened] -env: - VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite - jobs: build: name: Build USVFS @@ -19,44 +16,66 @@ jobs: arch: [x86, x64] config: [Debug, Release] runs-on: windows-2022 + steps: - # set VCPKG Root - - name: "Set environmental variables" + # Set vcpkg root directory + - name: "Set environment variables" shell: bash run: | echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV - - # checkout USVFS and vcpkg + + # Checkout code - uses: actions/checkout@v4 - - # configure - - run: cmake --preset vs2022-windows-${{ matrix.arch }} -B build_${{ matrix.arch }} "-DCMAKE_INSTALL_PREFIX=install/${{ matrix.config }}" - - # build - - run: cmake --build build_${{ matrix.arch }} --config ${{ matrix.config }} --target INSTALL - - # package install - - uses: actions/upload-artifact@master + + # Setup vcpkg binary caching (GitHub Actions cache) + - name: Configure vcpkg cache + uses: actions/cache@v4 + id: cache-vcpkg + with: + path: | + ${{ env.VCPKG_ROOT }}/downloads + ${{ env.VCPKG_ROOT }}/installed + ${{ env.VCPKG_ROOT }}/buildtrees + key: ${{ runner.os }}-vcpkg-${{ matrix.arch }}-${{ matrix.config }}-${{ hashFiles('**/vcpkg.json', '**/CMakeLists.txt', '**/vcpkg-configuration.json') }} + restore-keys: | + ${{ runner.os }}-vcpkg-${{ matrix.arch }}-${{ matrix.config }}- + ${{ runner.os }}-vcpkg- + + # Configure + - name: Configure CMake + run: cmake --preset vs2022-windows-${{ matrix.arch }} -B build_${{ matrix.arch }} "-DCMAKE_INSTALL_PREFIX=install/${{ matrix.config }}" + + # Build + - name: Build project + run: cmake --build build_${{ matrix.arch }} --config ${{ matrix.config }} --target INSTALL + + # Package install files + - name: Upload install files + uses: actions/upload-artifact@v4 with: name: usvfs_${{ matrix.config }}_${{ matrix.arch }} path: ./install/${{ matrix.config }} - - # package test/dlls/etc. for tests - - - uses: actions/upload-artifact@master + + # Package test files, etc. + - name: Upload library files + uses: actions/upload-artifact@v4 with: name: usvfs-libs_${{ matrix.config }}_${{ matrix.arch }} path: ./lib - - uses: actions/upload-artifact@master + + - name: Upload binary files + uses: actions/upload-artifact@v4 with: name: usvfs-bins_${{ matrix.config }}_${{ matrix.arch }} path: ./bin - - uses: actions/upload-artifact@master + + - name: Upload test files + uses: actions/upload-artifact@v4 with: name: usvfs-tests_${{ matrix.config }}_${{ matrix.arch }} path: ./test/bin - # merge x86 / x64 artifacts for tests (root bin/lib and test folder) + # Merge x86/x64 test artifacts merge-artifacts-for-tests: runs-on: ubuntu-latest name: Merge Test Artifacts @@ -65,23 +84,25 @@ jobs: matrix: config: [Debug, Release] steps: - - name: Merge USVFS libs + - name: Merge USVFS library files uses: actions/upload-artifact/merge@v4 with: name: usvfs-libs_${{ matrix.config }} pattern: usvfs-libs_${{ matrix.config }}_* - - name: Merge USVFS bins + + - name: Merge USVFS binary files uses: actions/upload-artifact/merge@v4 with: name: usvfs-bins_${{ matrix.config }} pattern: usvfs-bins_${{ matrix.config }}_* - - name: Merge USVFS tests + + - name: Merge USVFS test files uses: actions/upload-artifact/merge@v4 with: name: usvfs-tests_${{ matrix.config }} pattern: usvfs-tests_${{ matrix.config }}_* - # merge x86 / x64 artifacts (install folder) + # Merge x86/x64 install artifacts merge-artifacts-for-release: runs-on: ubuntu-latest name: Merge Install Artifacts @@ -90,7 +111,7 @@ jobs: matrix: config: [Debug, Release] steps: - - name: Merge USVFS install + - name: Merge USVFS install files uses: actions/upload-artifact/merge@v4 with: name: usvfs_${{ matrix.config }} @@ -106,33 +127,52 @@ jobs: arch: [x86, x64] steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@master + + - uses: actions/download-artifact@v4 with: name: usvfs-libs_${{ matrix.config }} path: ./lib - - uses: actions/download-artifact@master + + - uses: actions/download-artifact@v4 with: name: usvfs-bins_${{ matrix.config }} path: ./bin - - uses: actions/download-artifact@master + + - uses: actions/download-artifact@v4 with: name: usvfs-tests_${{ matrix.config }} path: ./test/bin - - run: ./test/bin/shared_test_${{ matrix.arch }}.exe + + - name: Run shared tests + run: ./test/bin/shared_test_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/testinject_bin_${{ matrix.arch }}.exe + + - name: Run injection tests + run: ./test/bin/testinject_bin_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/thooklib_test_${{ matrix.arch }}.exe + + - name: Run hook library tests + run: ./test/bin/thooklib_test_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/tinjectlib_test_${{ matrix.arch }}.exe + + - name: Run inject library tests + run: ./test/bin/tinjectlib_test_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/tvfs_test_${{ matrix.arch }}.exe + + - name: Run VFS tests + run: ./test/bin/tvfs_test_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/usvfs_test_runner_${{ matrix.arch }}.exe + + - name: Run USVFS test runner + run: ./test/bin/usvfs_test_runner_${{ matrix.arch }}.exe if: always() - - run: ./test/bin/usvfs_global_test_runner_${{ matrix.arch }}.exe + + - name: Run USVFS global test runner + run: ./test/bin/usvfs_global_test_runner_${{ matrix.arch }}.exe if: always() - - uses: actions/upload-artifact@v4 + + - name: Upload test outputs + uses: actions/upload-artifact@v4 if: always() with: name: tests-outputs_${{ matrix.config }}_${{ matrix.arch }} @@ -148,26 +188,26 @@ jobs: permissions: contents: write steps: - # USVFS does not use different names for debug and release so we are going to + # USVFS does not use different names for Debug and Release so we are going to # retrieve both install artifacts and put them under install/ and install/debug/ - - - name: Download Release Artifact - uses: actions/download-artifact@master + + - name: Download Release artifact + uses: actions/download-artifact@v4 with: name: usvfs_Release path: ./install - - - name: Download Debug Artifact - uses: actions/download-artifact@master + + - name: Download Debug artifact + uses: actions/download-artifact@v4 with: name: usvfs_Debug path: ./install/debug - - - name: Create USVFS Base archive + + - name: Create USVFS base archive run: 7z a usvfs_${{ github.ref_name }}.7z ./install/* - + - name: Publish Release env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - run: gh release create --draft=false --notes="Release ${{ github.ref_name }}" "${{ github.ref_name }}" ./usvfs_${{ github.ref_name }}.7z + run: gh release create --draft=false --notes="Release ${{ github.ref_name }}" "${{ github.ref_name }}" ./usvfs_${{ github.ref_name }}.7z \ No newline at end of file diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index d5e3f4b..c831916 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -9,7 +9,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Check format uses: ModOrganizer2/check-formatting-action@master with: diff --git a/src/shared/ntdll_declarations.cpp b/src/shared/ntdll_declarations.cpp index b4bf0f5..b4c6866 100644 --- a/src/shared/ntdll_declarations.cpp +++ b/src/shared/ntdll_declarations.cpp @@ -31,6 +31,7 @@ NtQueryFullAttributesFile_type NtQueryFullAttributesFile; NtQueryAttributesFile_type NtQueryAttributesFile; NtQueryObject_type NtQueryObject; NtQueryInformationFile_type NtQueryInformationFile; +NtSetInformationFile_type NtSetInformationFile; NtQueryInformationByName_type NtQueryInformationByName; NtOpenFile_type NtOpenFile; NtCreateFile_type NtCreateFile; @@ -41,6 +42,8 @@ RtlDosPathNameToRelativeNtPathName_U_WithStatus_type RtlReleaseRelativeName_type RtlReleaseRelativeName; RtlGetVersion_type RtlGetVersion; NtTerminateProcess_type NtTerminateProcess; +NtReadFile_type NtReadFile; +NtWriteFile_type NtWriteFile; static bool ntdll_initialized; @@ -55,6 +58,7 @@ void ntdll_declarations_init() LOAD_EXT(ntDLLMod, NtQueryAttributesFile); LOAD_EXT(ntDLLMod, NtQueryObject); LOAD_EXT(ntDLLMod, NtQueryInformationFile); + LOAD_EXT(ntDLLMod, NtSetInformationFile); LOAD_EXT(ntDLLMod, NtQueryInformationByName); LOAD_EXT(ntDLLMod, NtCreateFile); LOAD_EXT(ntDLLMod, NtOpenFile); @@ -64,6 +68,8 @@ void ntdll_declarations_init() LOAD_EXT(ntDLLMod, RtlReleaseRelativeName); LOAD_EXT(ntDLLMod, RtlGetVersion); LOAD_EXT(ntDLLMod, NtTerminateProcess); + LOAD_EXT(ntDLLMod, NtReadFile); + LOAD_EXT(ntDLLMod, NtWriteFile); ntdll_initialized = true; } diff --git a/src/shared/ntdll_declarations.h b/src/shared/ntdll_declarations.h index 813ad8c..e8e9583 100644 --- a/src/shared/ntdll_declarations.h +++ b/src/shared/ntdll_declarations.h @@ -358,6 +358,8 @@ typedef struct _FILE_REPARSE_POINT_INFORMATION #define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L) #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) #define STATUS_NO_SUCH_FILE ((NTSTATUS)0xC000000FL) +#define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L) +#define STATUS_CANNOT_DELETE ((NTSTATUS)0xC0000121L) #define SL_RESTART_SCAN 0x01 #define SL_RETURN_SINGLE_ENTRY 0x02 @@ -375,6 +377,7 @@ typedef enum _FILE_INFORMATION_CLASS FileNameInformation = 9, FileRenameInformation = 10, FileNamesInformation = 12, + FilePositionInformation = 14, FileAllInformation = 18, FileObjectIdInformation = 29, FileReparsePointInformation = 33, @@ -563,6 +566,9 @@ using NtQueryObject_type = NTSTATUS(WINAPI*)( using NtQueryInformationFile_type = NTSTATUS(WINAPI*)( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); +using NtSetInformationFile_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, @@ -579,6 +585,18 @@ using NtClose_type = NTSTATUS(WINAPI*)(HANDLE); using NtTerminateProcess_type = NTSTATUS(WINAPI*)(HANDLE ProcessHandle, NTSTATUS ExitStatus); +using NtReadFile_type = NTSTATUS(WINAPI*)(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key); + +using NtWriteFile_type = NTSTATUS(WINAPI*)(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key); + // Rtl using RtlDoesFileExists_U_type = NTSYSAPI BOOLEAN(NTAPI*)(PCWSTR); @@ -594,11 +612,15 @@ extern NtQueryFullAttributesFile_type NtQueryFullAttributesFile; extern NtQueryAttributesFile_type NtQueryAttributesFile; extern NtQueryObject_type NtQueryObject; extern NtQueryInformationFile_type NtQueryInformationFile; +extern NtSetInformationFile_type NtSetInformationFile; extern NtQueryInformationByName_type NtQueryInformationByName; extern NtOpenFile_type NtOpenFile; extern NtCreateFile_type NtCreateFile; extern NtClose_type NtClose; extern NtTerminateProcess_type NtTerminateProcess; +extern NtReadFile_type NtReadFile; +extern NtWriteFile_type NtWriteFile; + extern RtlDoesFileExists_U_type RtlDoesFileExists_U; extern RtlDosPathNameToRelativeNtPathName_U_WithStatus_type RtlDosPathNameToRelativeNtPathName_U_WithStatus; diff --git a/src/usvfs_dll/hookmanager.cpp b/src/usvfs_dll/hookmanager.cpp index 731b856..123391b 100644 --- a/src/usvfs_dll/hookmanager.cpp +++ b/src/usvfs_dll/hookmanager.cpp @@ -296,12 +296,15 @@ void HookManager::initHooks() installHook(ntdllMod, nullptr, "NtQueryDirectoryFileEx", hook_NtQueryDirectoryFileEx); installHook(ntdllMod, nullptr, "NtQueryObject", hook_NtQueryObject); installHook(ntdllMod, nullptr, "NtQueryInformationFile", hook_NtQueryInformationFile); + installHook(ntdllMod, nullptr, "NtSetInformationFile", hook_NtSetInformationFile); 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(ntdllMod, nullptr, "NtReadFile", hook_NtReadFile); + installHook(ntdllMod, nullptr, "NtWriteFile", hook_NtWriteFile); installHook(kbaseMod, k32Mod, "LoadLibraryExA", hook_LoadLibraryExA); installHook(kbaseMod, k32Mod, "LoadLibraryExW", hook_LoadLibraryExW); diff --git a/src/usvfs_dll/hooks/file_information_utils.h b/src/usvfs_dll/hooks/file_information_utils.h index dbd3597..6d11f09 100644 --- a/src/usvfs_dll/hooks/file_information_utils.h +++ b/src/usvfs_dll/hooks/file_information_utils.h @@ -16,8 +16,8 @@ namespace usvfs::details #define DECLARE_HAS_FIELD(Field) \ template \ - constexpr auto HasFieldImpl##Field(int)->decltype(std::declval().Field, void(), \ - std::true_type()) \ + constexpr auto HasFieldImpl##Field(int) \ + -> decltype(std::declval().Field, void(), std::true_type()) \ { \ return {}; \ } \ diff --git a/src/usvfs_dll/hooks/kernel32.cpp b/src/usvfs_dll/hooks/kernel32.cpp index 6599eb0..7bf4490 100644 --- a/src/usvfs_dll/hooks/kernel32.cpp +++ b/src/usvfs_dll/hooks/kernel32.cpp @@ -5,6 +5,7 @@ #include "../hookcontext.h" #include "../hookmanager.h" #include "../maptracker.h" +#include "settings.h" #include #include #include @@ -296,20 +297,57 @@ BOOL WINAPI usvfs::hook_CreateProcessInternalW( } std::wstring cmdline; + bool useCmdline = false; if (cend && cmdReroute.fileName()) { - auto fileName = cmdReroute.fileName(); - cmdline.reserve(wcslen(fileName) + wcslen(cend) + 2); - if (*fileName != '"') + std::wstring fileName = cmdReroute.fileName(); + if (fileName.length() >= 4 && fileName.compare(0, 4, L"\\\\?\\") == 0) { + fileName = fileName.substr(4); + } else if (fileName.length() >= 4 && fileName.compare(0, 4, L"\\??\\") == 0) { + fileName = fileName.substr(4); + } + + cmdline.reserve(fileName.length() + wcslen(cend) + 2); + if (fileName.length() > 0 && fileName[0] != '"') cmdline += L"\""; cmdline += fileName; - if (*fileName != '"') + if (fileName.length() > 0 && fileName[0] != '"') cmdline += L"\""; cmdline += cend; + useCmdline = true; + } else if (lpCommandLine) { + cmdline = lpCommandLine; + useCmdline = true; } + if (useCmdline) { + size_t pos = 0; + while ((pos = cmdline.find(L"\\\\?\\", pos)) != std::wstring::npos) { + cmdline.replace(pos, 4, L""); + } + pos = 0; + while ((pos = cmdline.find(L"\\??\\", pos)) != std::wstring::npos) { + cmdline.replace(pos, 4, L""); + } + } + + std::wstring appName; + LPCWSTR lpAppName = applicationReroute.fileName(); + if (lpAppName) { + if (wcsncmp(lpAppName, L"\\\\?\\", 4) == 0) { + appName = lpAppName + 4; + lpAppName = appName.c_str(); + } else if (wcsncmp(lpAppName, L"\\??\\", 4) == 0) { + appName = lpAppName + 4; + lpAppName = appName.c_str(); + } + } + + spdlog::get("hooks")->info("CreateProcessInternalW: app={}, cmd={}", + lpAppName ? string_cast(lpAppName) : "null", + useCmdline ? string_cast(cmdline) : "null"); + PRE_REALCALL - res = CreateProcessInternalW(token, applicationReroute.fileName(), - cmdline.empty() ? lpCommandLine : &cmdline[0], + res = CreateProcessInternalW(token, lpAppName, useCmdline ? &cmdline[0] : nullptr, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation, newToken); @@ -576,6 +614,54 @@ BOOL WINAPI usvfs::hook_DeleteFileW(LPCWSTR lpFileName) RerouteW reroute = RerouteW::create(READ_CONTEXT(), callContext, path.c_str()); + if (usvfs::settings::enableCoW) { + std::wstring physicalPath = reroute.fileName(); + if (physicalPath.rfind(L"\\??\\", 0) == 0 || + physicalPath.rfind(L"\\\\?\\", 0) == 0) { + physicalPath = physicalPath.substr(4); + } + + bool isInModsDir = false; + std::wstring modsDirW; + if (!usvfs::settings::mods_dir.empty()) { + modsDirW = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); + if (physicalPath.size() >= modsDirW.size() && + _wcsnicmp(physicalPath.c_str(), modsDirW.c_str(), modsDirW.size()) == 0) { + wchar_t nextChar = physicalPath.c_str()[modsDirW.size()]; + if (nextChar == L'\0' || nextChar == L'\\') { + isInModsDir = true; + } + } + } + + // Check if file is in excluded directory + bool isExcluded = false; + if (isInModsDir) { + for (const auto& excludedPath : usvfs::settings::exclude_mods) { + std::wstring excludedPathW = + ush::string_cast(excludedPath, ush::CodePage::UTF8); + if (physicalPath.size() >= excludedPathW.size() && + _wcsnicmp(physicalPath.c_str(), excludedPathW.c_str(), + excludedPathW.size()) == 0) { + wchar_t nextExChar = physicalPath.c_str()[excludedPathW.size()]; + if (nextExChar == L'\0' || nextExChar == L'\\') { + isExcluded = true; + break; + } + } + } + } + + if (isInModsDir && !isExcluded) { + spdlog::get("hooks")->info( + "DeleteFileW: CoW - Remove mapping instead of deleting file: {}", + physicalPath); + reroute.removeMapping(READ_CONTEXT(), false); + res = true; + } + } + PRE_REALCALL if (reroute.wasRerouted()) { res = ::DeleteFileW(reroute.fileName()); diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index ad53418..cd5badf 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -1,4 +1,5 @@ #include "ntdll.h" +#include "settings.h" #include #include @@ -949,10 +950,22 @@ DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( FileInformationClass); } - PRE_REALCALL - res = ::NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, - FileInformationClass); - POST_REALCALL + auto& writeAccessMap = + READ_CONTEXT()->customData(WriteAccessHandles); + auto WriteIter = writeAccessMap.find(FileHandle); + if (WriteIter != writeAccessMap.end() && + WriteIter->second.RerouteHandle != INVALID_HANDLE_VALUE) { + auto WriteInfo = WriteIter->second; + PRE_REALCALL + res = ::NtQueryInformationFile(WriteInfo.RerouteHandle, IoStatusBlock, + FileInformation, Length, FileInformationClass); + POST_REALCALL + } else { + 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 @@ -969,8 +982,13 @@ DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( FileInformationClass == FileNormalizedNameInformation)) || (res == STATUS_SUCCESS && FileInformationClass == FileAllInformation)) { - const auto trackerInfo = ntdllHandleTracker.lookup(FileHandle); - const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + HandleTracker::info_type trackerInfo; + if (WriteIter != writeAccessMap.end() && + WriteIter->second.RerouteHandle != INVALID_HANDLE_VALUE) + trackerInfo = ntdllHandleTracker.lookup(WriteIter->second.RerouteHandle); + else + trackerInfo = ntdllHandleTracker.lookup(FileHandle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); // TODO: difference between FileNameInformation and FileNormalizedNameInformation @@ -1029,6 +1047,42 @@ DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( return res; } +DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtSetInformationFile( + 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()) { + res = ::NtSetInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + callContext.updateLastError(); + return res; + } + + auto& writeAccessMap = + READ_CONTEXT()->customData(WriteAccessHandles); + auto WriteIter = writeAccessMap.find(FileHandle); + if (WriteIter != writeAccessMap.end() && + WriteIter->second.RerouteHandle != INVALID_HANDLE_VALUE) { + auto WriteInfo = WriteIter->second; + PRE_REALCALL + res = ::NtSetInformationFile(WriteInfo.RerouteHandle, IoStatusBlock, + FileInformation, Length, FileInformationClass); + POST_REALCALL + } else { + PRE_REALCALL + res = ::NtSetInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, + FileInformationClass); + POST_REALCALL + } + + HOOK_END + return res; +} + unique_ptr_deleter makeObjectAttributes(RedirectionInfo& redirInfo, POBJECT_ATTRIBUTES attributeTemplate) { @@ -1143,6 +1197,59 @@ NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, unique_ptr_deleter adjustedAttributes = makeObjectAttributes(redir, ObjectAttributes); + auto logger = spdlog::get("hooks"); + + bool needReroute = false; + std::wstring physicalPath; + if (usvfs::settings::enableCoW && + (DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | + FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0) { + physicalPath = adjustedAttributes->ObjectName->Buffer; + + // Clean up NT path prefix if present + if (physicalPath.rfind(L"\\??\\", 0) == 0 || + physicalPath.rfind(L"\\\\?\\", 0) == 0) { + physicalPath = physicalPath.substr(4); + } + + // Check if file is in mods directory + bool isInModsDir = false; + std::wstring modsDirW; + if (!usvfs::settings::mods_dir.empty()) { + modsDirW = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); + if (physicalPath.size() >= modsDirW.size() && + _wcsnicmp(physicalPath.c_str(), modsDirW.c_str(), modsDirW.size()) == 0) { + wchar_t nextChar = physicalPath.c_str()[modsDirW.size()]; + if (nextChar == L'\0' || nextChar == L'\\') { + isInModsDir = true; + } + } + } + + // Check if file is in excluded directory + bool isExcluded = false; + if (isInModsDir) { + for (const auto& excludedPath : usvfs::settings::exclude_mods) { + std::wstring excludedPathW = + ush::string_cast(excludedPath, ush::CodePage::UTF8); + if (physicalPath.size() >= excludedPathW.size() && + _wcsnicmp(physicalPath.c_str(), excludedPathW.c_str(), + excludedPathW.size()) == 0) { + wchar_t nextExChar = physicalPath.c_str()[excludedPathW.size()]; + if (nextExChar == L'\0' || nextExChar == L'\\') { + isExcluded = true; + break; + } + } + } + } + + // Only track for CoW if in mods directory and not excluded + if (isInModsDir && !isExcluded) + needReroute = true; + } + PRE_REALCALL res = ::NtOpenFile(FileHandle, DesiredAccess, adjustedAttributes.get(), IoStatusBlock, ShareAccess, OpenOptions); @@ -1154,6 +1261,13 @@ NTSTATUS ntdll_mess_NtOpenFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, #pragma message("need to clean up this handle in CloseHandle call") } + if (needReroute) { + logger->debug("NtOpenFile: write access detected for file: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + WRITE_CONTEXT()->customData( + WriteAccessHandles)[*FileHandle] = {nullptr, physicalPath}; + } + if (redir.redirected) { LOG_CALL() .addParam("source", ObjectAttributes) @@ -1199,6 +1313,8 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + auto logger = spdlog::get("hooks"); + HOOK_START_GROUP(MutExHookGroup::OPEN_FILE) if (!callContext.active()) { return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, @@ -1209,10 +1325,12 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, UnicodeString inPath = CreateUnicodeString(ObjectAttributes); LPCWSTR inPathW = static_cast(inPath); + logger->debug("NtCreateFile: Original path: {0}", + ush::string_cast(ObjectAttributes->ObjectName->Buffer)); + if (inPath.size() == 0) { - spdlog::get("hooks")->info( - "failed to set from handle: {0}", - ush::string_cast(ObjectAttributes->ObjectName->Buffer)); + logger->info("NtCreateFile: failed to set from handle: {0}", + ush::string_cast(ObjectAttributes->ObjectName->Buffer)); return ::NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength); @@ -1245,7 +1363,7 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, convertedDisposition = CREATE_ALWAYS; break; default: - spdlog::get("hooks")->error("invalid disposition: {0}", CreateDisposition); + logger->error("NtCreateFile: invalid disposition: {0}", CreateDisposition); break; } @@ -1283,8 +1401,138 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, break; } - RedirectionInfo redir = applyReroute(rerouter); + RedirectionInfo redir = applyReroute(rerouter); + std::wstring physicalPath = rerouter.fileName(); + + // Clean up NT path prefix if present + if (physicalPath.rfind(L"\\??\\", 0) == 0 || + physicalPath.rfind(L"\\\\?\\", 0) == 0) { + physicalPath = physicalPath.substr(4); + } + + logger->debug("NtCreateFile: Rerouted path: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + + bool isDestructive = + (CreateDisposition == FILE_SUPERSEDE || CreateDisposition == FILE_OVERWRITE || + CreateDisposition == FILE_OVERWRITE_IF); + + bool isWrite = + (DesiredAccess & (GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA | + FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES)) != 0; + + std::wstring overwriteRedirectPath; + + // Check if file is in mods directory + bool isInModsDir = false; + std::wstring modsDirW; + if (usvfs::settings::enableCoW && !usvfs::settings::mods_dir.empty() && + (isDestructive || isWrite)) { + modsDirW = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); + if (physicalPath.size() >= modsDirW.size() && + _wcsnicmp(physicalPath.c_str(), modsDirW.c_str(), modsDirW.size()) == 0) { + wchar_t nextChar = physicalPath.c_str()[modsDirW.size()]; + if (nextChar == L'\0' || nextChar == L'\\') { + isInModsDir = true; + } + } + } + + // Check if file is in excluded directory + bool isExcluded = false; + if (isInModsDir) { + for (const auto& excludedPath : usvfs::settings::exclude_mods) { + std::wstring excludedPathW = + ush::string_cast(excludedPath, ush::CodePage::UTF8); + if (physicalPath.size() >= excludedPathW.size() && + _wcsnicmp(physicalPath.c_str(), excludedPathW.c_str(), + excludedPathW.size()) == 0) { + wchar_t nextExChar = physicalPath.c_str()[excludedPathW.size()]; + if (nextExChar == L'\0' || nextExChar == L'\\') { + isExcluded = true; + logger->debug( + "NtCreateFile: File is in excluded directory: {}", + ush::string_cast(excludedPathW, ush::CodePage::UTF8)); + break; + } + } + } + } + + // Handle destructive operations on mods files + if (isInModsDir && !isExcluded && isDestructive) { + logger->warn("NtCreateFile: mod file will be truncated - original path: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + + if (!usvfs::settings::overwrite_dir.empty()) { + const wchar_t* relToMods = physicalPath.c_str() + modsDirW.size(); + while (*relToMods == L'\\' || *relToMods == L'/') + relToMods++; + + const wchar_t* nextSep = wcschr(relToMods, L'\\'); + const wchar_t* nextSepSlash = wcschr(relToMods, L'/'); + if (nextSepSlash && (!nextSep || nextSepSlash < nextSep)) + nextSep = nextSepSlash; + + if (nextSep) { + const wchar_t* relToModRoot = nextSep + 1; + std::wstring overwriteDirW = ush::string_cast( + usvfs::settings::overwrite_dir, ush::CodePage::UTF8); + bfs::path destPath(overwriteDirW); + destPath /= relToModRoot; + + boost::system::error_code ec; + bfs::create_directories(destPath.parent_path(), ec); + if (!ec) { + if (bfs::exists(destPath, ec)) { + bfs::remove(destPath, ec); + } + bfs::copy_file(physicalPath, destPath, ec); + + if (ec) { + logger->error( + "NtCreateFile: Failed to copy to overwrite: {} (source: {}, dest: " + "{})", + ush::string_cast(ush::string_cast( + ec.message(), ush::CodePage::LOCAL), + ush::CodePage::UTF8), + ush::string_cast(physicalPath, ush::CodePage::UTF8), + destPath.string()); + } else { + logger->info("NtCreateFile: Copied to overwrite: {}", destPath.string()); + overwriteRedirectPath = destPath.wstring(); + } + } else { + logger->error( + "NtCreateFile: Failed to create directories for overwrite: {} (dest: " + "{})", + ush::string_cast( + ush::string_cast(ec.message(), ush::CodePage::LOCAL), + ush::CodePage::UTF8), + ush::string_cast(overwriteDirW, ush::CodePage::UTF8)); + } + } + } + } + + if (!overwriteRedirectPath.empty()) { + std::wstring ntPath = LR"(\??\)" + overwriteRedirectPath; + redir.path = UnicodeString(ntPath.c_str()); + redir.redirected = true; + + std::wstring lookupPathW = inPathW; + if (lookupPathW.rfind(L"\\??\\", 0) == 0 || + lookupPathW.rfind(L"\\\\?\\", 0) == 0) { + lookupPathW = lookupPathW.substr(4); + } + std::string lookupPath = + ush::string_cast(lookupPathW, ush::CodePage::UTF8); + std::string targetPath = + ush::string_cast(overwriteRedirectPath, ush::CodePage::UTF8); + WRITE_CONTEXT()->redirectionTable().addFile(lookupPath, targetPath); + } unique_ptr_deleter adjustedAttributes = makeObjectAttributes(redir, ObjectAttributes); @@ -1306,6 +1554,15 @@ NTSTATUS ntdll_mess_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, WRITE_CONTEXT()->customData(SearchHandles)[*FileHandle] = inPathW; } + + // Register write access tracking AFTER successful file creation when we have a + // valid handle + if (isInModsDir && !isExcluded && !isDestructive && isWrite) { + logger->debug("NtCreateFile: write access detected for file: {}", + ush::string_cast(physicalPath, ush::CodePage::UTF8)); + WRITE_CONTEXT()->customData( + WriteAccessHandles)[*FileHandle] = {INVALID_HANDLE_VALUE, physicalPath}; + } } if (rerouter.wasRerouted() || rerouter.changedError() || @@ -1391,6 +1648,18 @@ NTSTATUS WINAPI usvfs::hook_NtClose(HANDLE Handle) } } + WriteAccessHandleMap& writeAccessHandles = + WRITE_CONTEXT()->customData(WriteAccessHandles); + auto writeIter = writeAccessHandles.find(Handle); + if (writeIter != writeAccessHandles.end()) { + if (writeIter->second.RerouteHandle != INVALID_HANDLE_VALUE) { + spdlog::get("hooks")->debug("NtClose: CoW - Closing reroute file: {}", + writeIter->second.ReroutePath); + ::NtClose(writeIter->second.RerouteHandle); + } + writeAccessHandles.erase(writeIter); + } + if (GetFileType(Handle) == FILE_TYPE_DISK) ntdllHandleTracker.erase(Handle); @@ -1518,3 +1787,211 @@ NTSTATUS WINAPI usvfs::hook_NtTerminateProcess(HANDLE ProcessHandle, return res; } + +NTSTATUS WINAPI usvfs::hook_NtReadFile(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key) +{ + using namespace usvfs; + + NTSTATUS res = STATUS_SUCCESS; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + auto logger = spdlog::get("hooks"); + + HOOK_START + if (!callContext.active()) { + return ::NtReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + Buffer, Length, ByteOffset, Key); + } + + auto& writeAccessMap = + READ_CONTEXT()->customData(WriteAccessHandles); + + auto writeIter = writeAccessMap.find(FileHandle); + if (writeIter != writeAccessMap.end()) { + if (writeIter->second.RerouteHandle != INVALID_HANDLE_VALUE) { + // File has been copied, read from the rerouted file + logger->debug("NtReadFile: Reading from rerouted file: {}", + writeIter->second.ReroutePath); + PRE_REALCALL + res = ::NtReadFile(writeIter->second.RerouteHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, Buffer, Length, ByteOffset, Key); + POST_REALCALL + } else { + // File is in CoW state but not yet copied, read from original file + logger->debug("NtReadFile: Reading from original file (CoW pending): {}", + writeIter->second.ReroutePath); + PRE_REALCALL + res = ::NtReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + Buffer, Length, ByteOffset, Key); + POST_REALCALL + } + } else { + // File not in write access map, read normally + PRE_REALCALL + res = ::NtReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, Buffer, + Length, ByteOffset, Key); + POST_REALCALL + } + HOOK_END + + return res; +} + +NTSTATUS WINAPI usvfs::hook_NtWriteFile(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key) +{ + using namespace usvfs; + + NTSTATUS res = STATUS_ACCESS_DENIED; + + PreserveGetLastError ntFunctionsDoNotChangeGetLastError; + + auto logger = spdlog::get("hooks"); + + HOOK_START_GROUP(MutExHookGroup::ALL_GROUPS) + if (!callContext.active()) { + return ::NtWriteFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + Buffer, Length, ByteOffset, Key); + } + + auto& writeAccessMap = + READ_CONTEXT()->customData(WriteAccessHandles); + + auto writeIter = writeAccessMap.find(FileHandle); + if (writeIter != writeAccessMap.end()) { + // handle write access + WriteAccessHandle& writeAccessInfo = writeIter->second; + if (writeAccessInfo.RerouteHandle != INVALID_HANDLE_VALUE) { + logger->debug("NtWriteFile: Write operation detected for file: {}", + writeAccessInfo.ReroutePath); + PRE_REALCALL + res = ::NtWriteFile(writeAccessInfo.RerouteHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, Buffer, Length, ByteOffset, Key); + POST_REALCALL + } else if (writeAccessInfo.RerouteHandle == INVALID_HANDLE_VALUE) { + logger->info("NtWriteFile: Copy on Write for file: {}", + writeAccessInfo.ReroutePath); + + // CoW: Copy the original file to the reroute path first + std::wstring reroutePath = writeAccessInfo.ReroutePath; + if (!usvfs::settings::overwrite_dir.empty()) { + std::wstring overwriteDirW = ush::string_cast( + usvfs::settings::overwrite_dir, ush::CodePage::UTF8); + + // Extract the relative path from the mods directory + std::wstring modsDirW = ush::string_cast( + usvfs::settings::mods_dir, ush::CodePage::UTF8); + const wchar_t* pathW = writeAccessInfo.ReroutePath.c_str(); + + if (writeAccessInfo.ReroutePath.size() >= modsDirW.size() && + _wcsnicmp(pathW, modsDirW.c_str(), modsDirW.size()) == 0) { + const wchar_t* relToMods = pathW + modsDirW.size(); + while (*relToMods == L'\\' || *relToMods == L'/') + relToMods++; + + const wchar_t* nextSep = wcschr(relToMods, L'\\'); + const wchar_t* nextSepSlash = wcschr(relToMods, L'/'); + if (nextSepSlash && (!nextSep || nextSepSlash < nextSep)) + nextSep = nextSepSlash; + + if (nextSep) { + const wchar_t* relToModRoot = nextSep + 1; + bfs::path destPath(overwriteDirW); + destPath /= relToModRoot; + + boost::system::error_code ec; + bfs::create_directories(destPath.parent_path(), ec); + if (!ec) { + if (bfs::exists(destPath, ec)) { + bfs::remove(destPath, ec); + } + bfs::copy_file(writeAccessInfo.ReroutePath, destPath, ec); + + if (!ec) { + logger->info("NtWriteFile: CoW - Copied file to overwrite: {}", + destPath.string()); + reroutePath = destPath.wstring(); + + FILE_POSITION_INFORMATION FilePointer; + IO_STATUS_BLOCK iosbQuery, iosbSet; + HANDLE rerouteHandle = INVALID_HANDLE_VALUE; + NTSTATUS queryStatus = ::NtQueryInformationFile( + FileHandle, &iosbQuery, &FilePointer, sizeof(FilePointer), + FilePositionInformation); + + if (queryStatus == STATUS_SUCCESS) { + rerouteHandle = + CreateFileW(reroutePath.c_str(), GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (rerouteHandle != INVALID_HANDLE_VALUE) { + NTSTATUS setStatus = ::NtSetInformationFile( + rerouteHandle, &iosbSet, &FilePointer, sizeof(FilePointer), + FilePositionInformation); + if (setStatus != STATUS_SUCCESS) { + logger->error("NtWriteFile: CoW - Failed to set file pointer for " + "rerouted file: {}", + reroutePath); + CloseHandle(rerouteHandle); + rerouteHandle = INVALID_HANDLE_VALUE; + } + } + } + + if (rerouteHandle != INVALID_HANDLE_VALUE) { + writeAccessInfo.RerouteHandle = rerouteHandle; + writeAccessInfo.ReroutePath = reroutePath; + PRE_REALCALL + res = ::NtWriteFile(rerouteHandle, Event, ApcRoutine, ApcContext, + IoStatusBlock, Buffer, Length, ByteOffset, Key); + POST_REALCALL + } else { + logger->error("NtWriteFile: CoW - Failed to open rerouted file: {}", + reroutePath); + res = STATUS_CANNOT_DELETE; + } + } else { + std::wstring errorMsgW = ush::string_cast( + ush::string_cast(ec.message(), ush::CodePage::LOCAL), + ush::CodePage::UTF8); + logger->error("NtWriteFile: CoW - Failed to copy file: {}", errorMsgW); + res = STATUS_CANNOT_DELETE; + } + } else { + std::wstring errorMsgW = ush::string_cast( + ush::string_cast(ec.message(), ush::CodePage::LOCAL), + ush::CodePage::UTF8); + logger->error("NtWriteFile: CoW - Failed to create directories: {}", + errorMsgW); + res = STATUS_CANNOT_DELETE; + } + } else { + logger->warn("NtWriteFile: CoW - Could not determine relative path"); + res = STATUS_INVALID_PARAMETER; + } + } else { + logger->warn("NtWriteFile: CoW - File not in mods directory"); + res = STATUS_INVALID_PARAMETER; + } + } else { + logger->warn("NtWriteFile: CoW - overwrite_dir not configured"); + res = STATUS_INVALID_PARAMETER; + } + } + } else { + PRE_REALCALL + res = ::NtWriteFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + Buffer, Length, ByteOffset, Key); + POST_REALCALL + } + HOOK_END + + return res; +} diff --git a/src/usvfs_dll/hooks/ntdll.h b/src/usvfs_dll/hooks/ntdll.h index f3db4a6..57abb0a 100644 --- a/src/usvfs_dll/hooks/ntdll.h +++ b/src/usvfs_dll/hooks/ntdll.h @@ -34,6 +34,10 @@ DLLEXPORT NTSTATUS WINAPI hook_NtQueryInformationFile( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); +DLLEXPORT NTSTATUS WINAPI hook_NtSetInformationFile( + 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); @@ -54,4 +58,16 @@ DLLEXPORT NTSTATUS WINAPI hook_NtClose(HANDLE Handle); DLLEXPORT NTSTATUS WINAPI hook_NtTerminateProcess(HANDLE ProcessHandle, NTSTATUS ExitStatus); +DLLEXPORT NTSTATUS WINAPI hook_NtReadFile(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key); + +DLLEXPORT NTSTATUS WINAPI hook_NtWriteFile(HANDLE FileHandle, HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, + ULONG Length, PLARGE_INTEGER ByteOffset, + PULONG Key); + } // namespace usvfs diff --git a/src/usvfs_dll/hooks/settings.h b/src/usvfs_dll/hooks/settings.h new file mode 100644 index 0000000..df931cd --- /dev/null +++ b/src/usvfs_dll/hooks/settings.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include + +namespace usvfs +{ +namespace settings +{ + inline bool enableCoW = false; + inline std::string current_process; + inline std::string mods_dir; + inline std::string overwrite_dir; + inline std::vector exclude_mods; + inline std::map output_directories; +} // namespace settings +} // namespace usvfs diff --git a/src/usvfs_dll/hooks/sharedids.h b/src/usvfs_dll/hooks/sharedids.h index 9a7531c..d96e839 100644 --- a/src/usvfs_dll/hooks/sharedids.h +++ b/src/usvfs_dll/hooks/sharedids.h @@ -7,3 +7,15 @@ typedef std::map SearchHandleMap; // maps handles opened for searching to the original search path, which is // necessary if the handle creation was rerouted DATA_ID(SearchHandles); + +// RerouteHandle init as nullptr +// ReroutePath init as physical path +// When Write Operation occurs, RerouteHandle is set to a new handle at overwrite +// ReroutePath is updated to the new path +typedef struct +{ + HANDLE RerouteHandle; + std::wstring ReroutePath; +} WriteAccessHandle; +typedef std::map WriteAccessHandleMap; +DATA_ID(WriteAccessHandles); diff --git a/src/usvfs_dll/maptracker.h b/src/usvfs_dll/maptracker.h index 6ddf471..4a746fb 100644 --- a/src/usvfs_dll/maptracker.h +++ b/src/usvfs_dll/maptracker.h @@ -358,10 +358,11 @@ class RerouteW static fs::path absolutePath(const wchar_t* inPath) { + fs::path p; if (shared::startswith(inPath, LR"(\\?\)") || shared::startswith(inPath, LR"(\??\)")) { inPath += 4; - return inPath; + p = inPath; } else if ((shared::startswith(inPath, LR"(\\localhost\)") || shared::startswith(inPath, LR"(\\127.0.0.1\)")) && inPath[13] == L'$') { @@ -369,17 +370,47 @@ class RerouteW newPath += towupper(inPath[12]); newPath += L':'; newPath += &inPath[14]; - return newPath; + p = newPath; } else if (inPath[0] == L'\0' || inPath[1] == L':') { - return inPath; + p = inPath; } else if (inPath[0] == L'\\' || inPath[0] == L'/') { - return fs::path(winapi::wide::getFullPathName(inPath).first); + p = fs::path(winapi::wide::getFullPathName(inPath).first); + } else { + WCHAR currentDirectory[MAX_PATH]; + ::GetCurrentDirectoryW(MAX_PATH, currentDirectory); + p = fs::path(currentDirectory) / inPath; + } + + std::wstring pathStr = p.wstring(); + if (pathStr.find(L'~') != std::wstring::npos) { + std::vector buffer(MAX_PATH); + DWORD res = GetLongPathNameW(pathStr.c_str(), buffer.data(), buffer.size()); + if (res > buffer.size()) { + buffer.resize(res); + res = GetLongPathNameW(pathStr.c_str(), buffer.data(), buffer.size()); + } + if (res > 0) { + return fs::path(std::wstring(buffer.data(), res)); + } + + fs::path existing = p; + fs::path remainder; + while (!existing.empty() && existing != existing.root_path()) { + std::wstring eStr = existing.wstring(); + res = GetLongPathNameW(eStr.c_str(), buffer.data(), buffer.size()); + if (res > buffer.size()) { + buffer.resize(res); + res = GetLongPathNameW(eStr.c_str(), buffer.data(), buffer.size()); + } + if (res > 0) { + fs::path longExisting(std::wstring(buffer.data(), res)); + return longExisting / remainder; + } + remainder = existing.filename() / remainder; + existing = existing.parent_path(); + } } - WCHAR currentDirectory[MAX_PATH]; - ::GetCurrentDirectoryW(MAX_PATH, currentDirectory); - fs::path finalPath = fs::path(currentDirectory) / inPath; - return finalPath; - // winapi::wide::getFullPathName(inPath).first; + return p; } static fs::path canonizePath(const fs::path& inPath) diff --git a/src/usvfs_dll/usvfs.cpp b/src/usvfs_dll/usvfs.cpp index 56d9f71..485e325 100644 --- a/src/usvfs_dll/usvfs.cpp +++ b/src/usvfs_dll/usvfs.cpp @@ -20,6 +20,7 @@ along with usvfs. If not, see . */ #include "usvfs.h" #include "hookmanager.h" +#include "hooks/settings.h" #include "loghelpers.h" #include "redirectiontree.h" #include "usvfs_version.h" @@ -35,6 +36,8 @@ along with usvfs. If not, see . // note that there's a mix of boost and std filesystem stuff in this file and // that they're not completely compatible #include +#include +#include namespace bfs = boost::filesystem; namespace ush = usvfs::shared; @@ -371,12 +374,149 @@ LONG WINAPI VEHandler(PEXCEPTION_POINTERS exceptionPtrs) // Exported functions // +void LoadSettings() +{ + wchar_t modulePath[MAX_PATH]; + if (GetModuleFileNameW(dllModule, modulePath, MAX_PATH) == 0) { + return; + } + std::wstring iniPath = modulePath; + size_t lastSlash = iniPath.find_last_of(L"\\/"); + if (lastSlash != std::wstring::npos) { + iniPath = iniPath.substr(0, lastSlash + 1); + } else { + iniPath = L""; + } + iniPath += L"usvfs_redirect.ini"; + + auto logger = spdlog::get("usvfs"); + + bfs::path dllDir = bfs::path(modulePath).parent_path(); + + auto makeAbsolute = [&](std::string pathStr) -> std::string { + if (pathStr.empty()) + return ""; + bfs::path p = ush::string_cast(pathStr, ush::CodePage::UTF8); + if (p.is_relative()) { + p = dllDir / p; + } + return ush::string_cast(p.lexically_normal().wstring(), + ush::CodePage::UTF8); + }; + + // Helper to read string and convert to UTF-8 + auto readString = [&](const wchar_t* section, const wchar_t* key, + const wchar_t* def) { + wchar_t buffer[4096]; + GetPrivateProfileStringW(section, key, def, buffer, 4096, iniPath.c_str()); + return ush::string_cast(buffer, ush::CodePage::UTF8); + }; + + usvfs::settings::enableCoW = readString(L"General", L"enable_cow", L"1") != "0"; + usvfs::settings::mods_dir = makeAbsolute(readString(L"General", L"mods_dir", L"")); + usvfs::settings::overwrite_dir = + makeAbsolute(readString(L"General", L"overwrite_dir", L"")); + + if (!bfs::exists(bfs::path(usvfs::settings::mods_dir)) || + !bfs::is_directory(bfs::path(usvfs::settings::mods_dir))) + logger->error("Mods directory does not exist or is not a directory: '{}'", + usvfs::settings::mods_dir); + if (!bfs::exists(bfs::path(usvfs::settings::overwrite_dir)) || + !bfs::is_directory(bfs::path(usvfs::settings::overwrite_dir))) + logger->error("Overwrite directory does not exist or is not a directory: '{}'", + usvfs::settings::overwrite_dir); + + std::string exclude_raw = readString(L"General", L"exclude_dir", L""); + usvfs::settings::exclude_mods.clear(); + if (!exclude_raw.empty()) { + std::stringstream ss(exclude_raw); + std::string item; + while (std::getline(ss, item, '|')) { + if (!item.empty()) { + bfs::path p = ush::string_cast(item, ush::CodePage::UTF8); + if (p.is_relative() && !usvfs::settings::mods_dir.empty()) { + bfs::path mods = ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8); + p = mods / p; + } else if (p.is_relative()) { + p = bfs::path( + ush::string_cast(makeAbsolute(item), ush::CodePage::UTF8)); + } + // only add if directory exists + if (bfs::exists(p) && bfs::is_directory(p)) + usvfs::settings::exclude_mods.push_back(ush::string_cast( + p.lexically_normal().wstring(), ush::CodePage::UTF8)); + } + } + } + + // Read Output section + wchar_t buffer[32768]; // 32KB is max for GetPrivateProfileSection + if (GetPrivateProfileSectionW(L"Output", buffer, 32768, iniPath.c_str()) > 0) { + wchar_t* p = buffer; + while (*p) { + std::wstring line = p; + size_t eq = line.find(L'='); + if (eq != std::wstring::npos) { + std::wstring key = line.substr(0, eq); + std::wstring val = line.substr(eq + 1); + usvfs::settings::output_directories[ush::string_cast( + key, ush::CodePage::UTF8)] = + ush::string_cast(val, ush::CodePage::UTF8); + } + p += line.length() + 1; + } + } + + if (!usvfs::settings::current_process.empty()) { + auto it = + usvfs::settings::output_directories.find(usvfs::settings::current_process); + if (it != usvfs::settings::output_directories.end()) { + std::string relativeOverwrite = it->second; + if (!usvfs::settings::mods_dir.empty()) { + bfs::path mods(ush::string_cast(usvfs::settings::mods_dir, + ush::CodePage::UTF8)); + bfs::path rel( + ush::string_cast(relativeOverwrite, ush::CodePage::UTF8)); + bfs::path newOverwrite = mods / rel; + if (bfs::exists(newOverwrite) && bfs::is_directory(newOverwrite)) { + usvfs::settings::overwrite_dir = ush::string_cast( + newOverwrite.lexically_normal().wstring(), ush::CodePage::UTF8); + logger->info("Instance match: '{}' -> Overwrite set to: '{}'", + usvfs::settings::current_process, + usvfs::settings::overwrite_dir); + } + } + } + } + usvfs::settings::exclude_mods.push_back(usvfs::settings::overwrite_dir); + + logger->info("Settings loaded:"); + logger->info(" enable_cow: {}", usvfs::settings::enableCoW); + logger->info(" current_process: {}", usvfs::settings::current_process); + logger->info(" mods_dir: {}", usvfs::settings::mods_dir); + logger->info(" overwrite_dir: {}", usvfs::settings::overwrite_dir); + logger->info(" exclude_mods:"); + for (const auto& dir : usvfs::settings::exclude_mods) { + logger->info(" - {}", dir); + } + logger->info(" output_directories:"); + for (const auto& [exe, dir] : usvfs::settings::output_directories) { + logger->info(" - {} -> {}", exe, dir); + } +} + void __cdecl InitHooks(LPVOID parameters, size_t) { InitLoggingInternal(false, true); const usvfsParameters* params = reinterpret_cast(parameters); + // get process name from path + usvfs::settings::current_process = + bfs::path(winapi::ansi::getModuleFileName(nullptr)).filename().string(); + LoadSettings(); + // 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 diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 723deee..df8d809 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -9,13 +9,19 @@ "kind": "git", "repository": "https://github.com/Microsoft/vcpkg", "baseline": "294f76666c3000630d828703e675814c05a4fd43", - "packages": ["boost*", "boost-*"] + "packages": [ + "boost*", + "boost-*" + ] }, { "kind": "git", "repository": "https://github.com/ModOrganizer2/vcpkg-registry", "baseline": "27d8adbfe9e4ce88a875be3a45fadab69869eb60", - "packages": ["asmjit", "spdlog"] + "packages": [ + "asmjit", + "spdlog" + ] } ] }