diff --git a/src/shared/formatters.h b/src/shared/formatters.h index ab963a7..ace62fc 100644 --- a/src/shared/formatters.h +++ b/src/shared/formatters.h @@ -74,6 +74,16 @@ struct std::formatter : std::formatter +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(UNICODE_STRING v, FmtContext& ctx) const + { + return std::formatter::format(usvfs::log::to_string(&v), ctx); + } +}; + template requires (std::is_pointer_v && !std::is_same_v diff --git a/src/shared/ntdll_declarations.h b/src/shared/ntdll_declarations.h index 981cb12..8430207 100644 --- a/src/shared/ntdll_declarations.h +++ b/src/shared/ntdll_declarations.h @@ -209,10 +209,11 @@ typedef struct _FILE_REPARSE_POINT_INFORMATION { } 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_NO_SUCH_FILE ((NTSTATUS)0xC000000FL) +#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 @@ -324,9 +325,15 @@ typedef struct _OBJECT_HANDLE_INFORMATION { 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; diff --git a/src/usvfs_dll/hooks/ntdll.cpp b/src/usvfs_dll/hooks/ntdll.cpp index ca69ccc..c6aa606 100644 --- a/src/usvfs_dll/hooks/ntdll.cpp +++ b/src/usvfs_dll/hooks/ntdll.cpp @@ -1068,9 +1068,66 @@ DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryObject( ObjectInformationLength, ReturnLength); POST_REALCALL - LOG_CALL() - .addParam("path", ntdllHandleTracker.lookup(Handle)) - .PARAM(ObjectInformationClass); + if (res == STATUS_SUCCESS && (ObjectInformationClass == ObjectNameInformation)) { + const auto trackerInfo = ntdllHandleTracker.lookup(Handle); + const auto redir = applyReroute(READ_CONTEXT(), callContext, trackerInfo); + + OBJECT_NAME_INFORMATION* info = + reinterpret_cast(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(trackerInfo)); + buffer[6] = L'\0'; + + const auto charCount = QueryDosDeviceW(buffer.data() + 4, deviceName, ARRAYSIZE(deviceName)); + + buffer = + std::wstring(deviceName) + L'\\' + std::wstring(buffer.data() + 7, buffer.size() - 7); + + // TODO: check this... + if (ObjectInformationLength < buffer.size() * 2 + sizeof(OBJECT_NAME_INFORMATION)) { + res = STATUS_INFO_LENGTH_MISMATCH; + + if (ReturnLength) { + *ReturnLength = buffer.size() * 2 + sizeof(OBJECT_NAME_INFORMATION); + } + } else { + // fill the object with 0 - not sure if mandatory + memset(ObjectInformation, L'\0', ObjectInformationLength); + + // put the unicode buffer at the end of the object + const auto unicodeBufferLength = + ObjectInformationLength - sizeof(OBJECT_NAME_INFORMATION); + LPWSTR unicodeBuffer = reinterpret_cast( + static_cast(ObjectInformation) + sizeof(OBJECT_NAME_INFORMATION)); + + // copy the path into the buffer + wmemcpy(unicodeBuffer, buffer.data(), buffer.size()); + + // update the actual unicode string + info->Name.Buffer = unicodeBuffer; + info->Name.Length = buffer.size() * 2; + info->Name.MaximumLength = unicodeBufferLength; + } + } + + LOG_CALL() + .PARAMWRAP(res) + .PARAM(ObjectInformationLength) + .addParam("return_length", ReturnLength ? *ReturnLength : -1) + .addParam("tracker_path", trackerInfo) + .PARAM(ObjectInformationClass) + .PARAM(redir.redirected) + .PARAM(redir.path) + .addParam("name_info", info->Name); + } HOOK_END return res; @@ -1112,15 +1169,21 @@ DLLEXPORT NTSTATUS WINAPI usvfs::hook_NtQueryInformationFile( if (redir.redirected) { - SetInfoFilename(FileInformation, FileInformationClass, static_cast(redir.path)); + LPCWSTR filenameFixed = static_cast(trackerInfo); + if (info->FileName[0] == L'\\') { + // strip the \??\X: prefix (X being the drive name) + filenameFixed = filenameFixed + 6; + } + SetInfoFilename(FileInformation, FileInformationClass, filenameFixed); }; LOG_CALL() + .PARAMWRAP(res) .addParam("tracker_path", trackerInfo) .PARAM(FileInformationClass) .PARAM(redir.redirected) .PARAM(redir.path) - .addParam("name_info", std::wstring{info->FileName, info->FileNameLength}); + .addParam("name_info", std::wstring{info->FileName, info->FileNameLength / sizeof(WCHAR)}); } diff --git a/test/tvfs_test/main.cpp b/test/tvfs_test/main.cpp index 253b75c..553a251 100644 --- a/test/tvfs_test/main.cpp +++ b/test/tvfs_test/main.cpp @@ -291,9 +291,8 @@ HANDLE hooked_NtOpenFile(LPCWSTR path, ACCESS_MASK accessMask, ULONG shareAccess attributes.ObjectName = &string; HANDLE ret = INVALID_HANDLE_VALUE; - if (usvfs::hook_NtOpenFile(&ret, accessMask | SYNCHRONIZE, - &attributes, &statusBlock, shareAccess, - openOptions | FILE_SYNCHRONOUS_IO_NONALERT) != STATUS_SUCCESS) + if (usvfs::hook_NtOpenFile(&ret, accessMask, &attributes, + &statusBlock, shareAccess, openOptions) != STATUS_SUCCESS) { return INVALID_HANDLE_VALUE; } @@ -310,7 +309,7 @@ TEST_F(USVFSTest, NtQueryDirectoryFileRegularFile) L"C:\\" , FILE_GENERIC_READ , FILE_SHARE_READ | FILE_SHARE_WRITE - , OPEN_EXISTING); + , FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); ASSERT_NE(INVALID_HANDLE_VALUE, hdl); IO_STATUS_BLOCK status; @@ -329,6 +328,8 @@ TEST_F(USVFSTest, NtQueryDirectoryFileRegularFile) , TRUE); ASSERT_EQ(STATUS_SUCCESS, status.Status); + + usvfs::hook_NtClose(hdl); } TEST_F(USVFSTest, NtQueryDirectoryFileFindsVirtualFile) @@ -343,7 +344,7 @@ TEST_F(USVFSTest, NtQueryDirectoryFileFindsVirtualFile) L"C:\\" , FILE_GENERIC_READ , FILE_SHARE_READ | FILE_SHARE_WRITE - , OPEN_EXISTING); + , FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); ASSERT_NE(INVALID_HANDLE_VALUE, hdl); IO_STATUS_BLOCK status; @@ -366,8 +367,63 @@ TEST_F(USVFSTest, NtQueryDirectoryFileFindsVirtualFile) FILE_DIRECTORY_INFORMATION *info = reinterpret_cast(buffer); ASSERT_EQ(STATUS_SUCCESS, status.Status); ASSERT_EQ(0, wcscmp(info->FileName, L"np.exe")); + + usvfs::hook_NtClose(hdl); } +TEST_F(USVFSTest, NtQueryObjectVirtualFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile(L"C:\\np.exe", usvfs::RedirectionDataLocal(REAL_FILEA)); + + HANDLE hdl = hooked_NtOpenFile(L"C:\\np.exe" + , FILE_GENERIC_READ + , FILE_SHARE_READ | FILE_SHARE_WRITE + , FILE_NON_DIRECTORY_FILE | FILE_OPEN_FOR_BACKUP_INTENT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl) << "last error=" << ::GetLastError(); + + { + char buffer[1024]; + IO_STATUS_BLOCK status; + const auto res = usvfs::hook_NtQueryInformationFile( + hdl, &status, buffer, sizeof(buffer), FileNameInformation); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + + FILE_NAME_INFORMATION* fileNameInfo = + reinterpret_cast(buffer); + ASSERT_EQ(0, wcscmp(fileNameInfo->FileName, L"\\np.exe")); + } + + { + char buffer[1024]; + IO_STATUS_BLOCK status; + const auto res = usvfs::hook_NtQueryInformationFile( + hdl, &status, buffer, sizeof(buffer), FileNormalizedNameInformation); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + + FILE_NAME_INFORMATION* fileNameInfo = + reinterpret_cast(buffer); + ASSERT_EQ(0, wcscmp(fileNameInfo->FileName, L"\\np.exe")); + } + + { + char buffer[2048]; + const auto res = usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(buffer), nullptr); + ASSERT_EQ(STATUS_SUCCESS, res); + + OBJECT_NAME_INFORMATION *information = reinterpret_cast(buffer); + ASSERT_EQ(L"\\Device\\HarddiskVolume3\\np.exe", std::wstring(information->Name.Buffer, information->Name.Length / sizeof(wchar_t))); + } + + usvfs::hook_NtClose(hdl); +} + + TEST_F(USVFSTestAuto, CannotCreateLinkToFileInNonexistantDirectory) { ASSERT_EQ(FALSE, usvfsVirtualLinkFile(REAL_FILEW, L"c:/this_directory_shouldnt_exist/np.exe", FALSE)); diff --git a/test/usvfs_global_test/usvfs_global_test.cpp b/test/usvfs_global_test/usvfs_global_test.cpp index 3447e4b..5982bec 100644 --- a/test/usvfs_global_test/usvfs_global_test.cpp +++ b/test/usvfs_global_test/usvfs_global_test.cpp @@ -34,6 +34,28 @@ class const auto& data() const { return m_data; } } parameters; +// simple guard for handle +class HandleGuard +{ + HANDLE m_handle = INVALID_HANDLE_VALUE; + +public: + HandleGuard() = default; + HandleGuard(HANDLE handle) : m_handle{handle} {} + + ~HandleGuard() { close(); } + + operator HANDLE() { return m_handle; } + + void close() + { + if (m_handle != INVALID_HANDLE_VALUE) { + ::CloseHandle(m_handle); + m_handle = INVALID_HANDLE_VALUE; + } + } +}; + // simple function to write content to a specified path void write_content(const std::filesystem::path& path, const std::string_view content) { @@ -55,6 +77,27 @@ TEST(BasicTest, SimpleTest) ASSERT_TRUE(exists(data / "info.txt")); remove(data / "info.txt"); ASSERT_FALSE(exists(data / "info.txt")); + + { + const auto doc_txt = data / "docs" / "doc.txt"; + HandleGuard hdl = CreateFileW(doc_txt.c_str(), FILE_READ_ATTRIBUTES, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + WCHAR filepath[1024]; + const auto length = GetFinalPathNameByHandleW( + hdl, filepath, sizeof(filepath) / sizeof(WCHAR), FILE_NAME_NORMALIZED); + const auto lastError = ::GetLastError(); + ASSERT_NE(0, length) << "last error=" << ::GetLastError(); + + // we need to construct a new path because the format returned by + // GetFinalPathNameByHandleW is not really standardized (or is it?) + + // TODO: more tests for this + + ASSERT_EQ(data / "docs" / "doc.txt", + canonical(std::filesystem::path(std::wstring(filepath, length)))); + } } // see https://github.com/ModOrganizer2/modorganizer/issues/2039 for context diff --git a/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp b/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp index 0a9e591..4f6e672 100644 --- a/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp +++ b/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp @@ -74,6 +74,13 @@ DWORD spawn_usvfs_hooked_process( return exit; } +bool UsvfsGlobalTest::m_force_usvfs_logs = false; + +void UsvfsGlobalTest::ForceUsvfsLogs() +{ + m_force_usvfs_logs = true; +} + class UsvfsGlobalTest::UsvfsGuard { public: @@ -183,7 +190,7 @@ int UsvfsGlobalTest::Run() const L"--gtest_brief=1", m_data_folder.native()}); // TODO: try to do this with gtest itself? - if (res != 0) { + if (m_force_usvfs_logs || res != 0) { const auto log_path = test::path_of_test_bin(m_group + L".log"); std::ofstream os{log_path}; std::string buffer(1024, '\0'); diff --git a/test/usvfs_global_test_runner/usvfs_global_test_fixture.h b/test/usvfs_global_test_runner/usvfs_global_test_fixture.h index ba869dc..277e78e 100644 --- a/test/usvfs_global_test_runner/usvfs_global_test_fixture.h +++ b/test/usvfs_global_test_runner/usvfs_global_test_fixture.h @@ -7,13 +7,16 @@ class UsvfsGlobalTest : public testing::Test { +public: + // enable log mode - this will generate USVFS log file for all tests regardless of + // success or failure (default is to only generate for failure) + // + static void ForceUsvfsLogs(); + public: UsvfsGlobalTest(); - void SetUp() override - { - PrepareFileSystem(); - } + void SetUp() override { PrepareFileSystem(); } void TearDown() override { CleanUp(); } @@ -42,6 +45,9 @@ class UsvfsGlobalTest : public testing::Test private: class UsvfsGuard; + // always generate usvfs logs + static bool m_force_usvfs_logs; + // prepare the filesystem by copying files and folders from the relevant fixtures // folder to the temporary folder // diff --git a/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp b/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp index b030c36..9be08cc 100644 --- a/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp +++ b/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp @@ -44,6 +44,8 @@ int main(int argc, char* argv[]) testing::InitGoogleTest(&argc, argv); + UsvfsGlobalTest::ForceUsvfsLogs(); + usvfsInitLogging(false); return RUN_ALL_TESTS();