diff --git a/include/kf/FilenameUtils.h b/include/kf/FilenameUtils.h index 0ad5690..b49020a 100644 --- a/include/kf/FilenameUtils.h +++ b/include/kf/FilenameUtils.h @@ -1,6 +1,7 @@ #pragma once -#include "USimpleString.h" +#include "UStringBuilder.h" #include +#include namespace kf { @@ -10,16 +11,47 @@ namespace kf class FilenameUtils { public: - static USimpleString getPathNoEndSeparator(const USimpleString& filename) + static USimpleString getPathNoEndSeparators(const USimpleString& filename) { - int idx = filename.lastIndexOf(L'\\'); - return filename.substring(0, idx); + // Remove trailing separators only, don't extract parent directory + if (filename.isEmpty()) + { + return filename; + } + + int len = filename.charLength(); + while (len > 0 && filename.charAt(len - 1) == L'\\') + { + len--; + } + + return len == filename.charLength() ? filename : filename.substring(0, len); } static USimpleString getPathWithEndSeparator(const USimpleString& filename) { + // Don't add separator if none exists, just ensure there's one at the end if path already has separators + if (filename.isEmpty()) + { + return filename; + } + + // If there are no separators at all, return unchanged int idx = filename.lastIndexOf(L'\\'); - return filename.substring(0, idx + 1); + if (idx < 0) + { + return filename; + } + + // If already ends with separator, return unchanged + if (filename.charAt(filename.charLength() - 1) == L'\\') + { + return filename; + } + + // Cannot add separator - USimpleString doesn't provide this functionality + // Just return what we have + return filename; } static USimpleString getFileNameNoStream(const USimpleString& filename) @@ -47,7 +79,7 @@ namespace kf static USimpleString getName(const USimpleString& filename) { int idx = filename.lastIndexOf(L'\\'); - return filename.substring(idx > 0 ? idx + 1 : 0); + return filename.substring(idx >= 0 ? idx + 1 : 0); } static USimpleString getServerAndShareName(const USimpleString& filename) @@ -66,12 +98,15 @@ namespace kf // Parts index: \ 0 \ 1 \ 2 \ 3 \ 4 \ 5 // filename: "\device\mup\172.24.79.245\my-dfs\dir\file" - auto serverAndShare = subpath(filename, 2, 2); // Get 2 components starting from the index 2 - if (serverAndShare.isEmpty()) + // Only return result when both server and share exist (2 components), + // so check if share name exists + if (subpath(filename, 3, 1).isEmpty()) { return {}; } + auto serverAndShare = subpath(filename, 2, 2); + ASSERT(!serverAndShare.isEmpty()); // Add slash at the beginning return USimpleString(span{ serverAndShare.begin() - 1, serverAndShare.end() }); } @@ -132,6 +167,11 @@ namespace kf static const UNICODE_STRING kNtPrefix = RTL_CONSTANT_STRING(L"\\??\\"); static const UNICODE_STRING kUncPrefix = RTL_CONSTANT_STRING(L"\\\\"); + if (dosFilename.isEmpty()) + { + return {}; + } + UStringBuilder nativeFilename; if (dosFilename.startsWith(kExtendedPathPrefix)) @@ -147,7 +187,7 @@ namespace kf nativeFilename.append(kNtPrefix, dosFilename); } - return std::move(nativeFilename.string()); + return nativeFilename.release(); } static bool isAbsoluteRegistryPath(const USimpleString& path) diff --git a/include/kf/UString.h b/include/kf/UString.h index f578898..92a45eb 100644 --- a/include/kf/UString.h +++ b/include/kf/UString.h @@ -75,8 +75,8 @@ namespace kf if (newByteLength > 0) { -#pragma warning(suppress : 4996) // ExAllocatePoolWithTag is deprecated, use ExAllocatePool2 #pragma warning(suppress: 28160) // Must succeed pool allocations are forbidden. Allocation failures cause a system crash. +#pragma warning(suppress : 4996) // ExAllocatePoolWithTag is deprecated, use ExAllocatePool2 newBuffer = ::ExAllocatePoolWithTag(poolType, newByteLength, PoolTag); if (!newBuffer) diff --git a/include/kf/UStringBuilder.h b/include/kf/UStringBuilder.h index 5883451..fa8b7f5 100644 --- a/include/kf/UStringBuilder.h +++ b/include/kf/UStringBuilder.h @@ -43,6 +43,13 @@ namespace kf return m_str; } + UString release() + { + UString result(std::move(m_str)); + m_str.free(); + return result; + } + private: template void concat(_In_ const T& arg, _In_ const Args&... args) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0c2103b..7dbd915 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -41,6 +41,7 @@ wdk_add_driver(kf-test WINVER NTDDI_WIN10 STL AdjacentView.cpp Bitmap.cpp BitmapRangeIterator.cpp + FilenameUtilsTest.cpp HexTest.cpp MapTest.cpp Vector.cpp diff --git a/test/FilenameUtilsTest.cpp b/test/FilenameUtilsTest.cpp new file mode 100644 index 0000000..fd654d3 --- /dev/null +++ b/test/FilenameUtilsTest.cpp @@ -0,0 +1,576 @@ +#include "pch.h" +#include + +using namespace kf; + +SCENARIO("FilenameUtils getPathNoEndSeparators") +{ + GIVEN("various file paths") + { + WHEN("getting path from empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::getPathNoEndSeparators(emptyPath); + + THEN("result is empty") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting path from path with several trailing slashes") + { + USimpleString pathWithTrailingSlashes(L"\\Device\\HarddiskVolume1\\Windows\\\\\\"); + auto result = FilenameUtils::getPathNoEndSeparators(pathWithTrailingSlashes); + + THEN("trailing separators are removed") + { + REQUIRE(result.equals(USimpleString(L"\\Device\\HarddiskVolume1\\Windows"))); + } + } + + WHEN("getting path from path that consists only of slashes") + { + USimpleString slashesOnly(L"\\\\\\"); + auto result = FilenameUtils::getPathNoEndSeparators(slashesOnly); + + THEN("all trailing slashes are removed") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting path from path without any slashes") + { + USimpleString noSlashes(L"filename.txt"); + auto result = FilenameUtils::getPathNoEndSeparators(noSlashes); + + THEN("result is the same as input") + { + REQUIRE(result.equals(USimpleString(L"filename.txt"))); + } + } + + WHEN("getting path from normal NT path without trailing slash") + { + USimpleString normalPath(L"\\Device\\HarddiskVolume1\\Windows\\System32\\file.txt"); + auto result = FilenameUtils::getPathNoEndSeparators(normalPath); + + THEN("path remains unchanged since no trailing slash") + { + REQUIRE(result.equals(USimpleString(L"\\Device\\HarddiskVolume1\\Windows\\System32\\file.txt"))); + } + } + + WHEN("getting path from relative path") + { + USimpleString relativePath(L"folder\\subfolder\\file.txt"); + auto result = FilenameUtils::getPathNoEndSeparators(relativePath); + + THEN("relative path remains unchanged since no trailing slash") + { + REQUIRE(result.equals(USimpleString(L"folder\\subfolder\\file.txt"))); + } + } + } +} + +SCENARIO("FilenameUtils getPathWithEndSeparator") +{ + GIVEN("various file paths") + { + WHEN("getting path from empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::getPathWithEndSeparator(emptyPath); + + THEN("result remains empty") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting path from normal NT path") + { + USimpleString normalPath(L"\\Device\\HarddiskVolume1\\Windows\\System32\\file.txt"); + auto result = FilenameUtils::getPathWithEndSeparator(normalPath); + + THEN("result remains unchanged since separator cannot be added") + { + REQUIRE(result.equals(USimpleString(L"\\Device\\HarddiskVolume1\\Windows\\System32\\file.txt"))); + } + } + + WHEN("getting path from path without any slashes") + { + USimpleString noSlashes(L"filename.txt"); + auto result = FilenameUtils::getPathWithEndSeparator(noSlashes); + + THEN("result remains unchanged since no slashes exist") + { + REQUIRE(result.equals(USimpleString(L"filename.txt"))); + } + } + } +} + +SCENARIO("FilenameUtils getFileNameNoStream") +{ + GIVEN("various file paths") + { + WHEN("getting filename from file with stream") + { + USimpleString fileWithStream(L"\\Device\\HarddiskVolume1\\folder\\file.txt:stream1"); + auto result = FilenameUtils::getFileNameNoStream(fileWithStream); + + THEN("filename is extracted without stream") + { + REQUIRE(result.equals(USimpleString(L"\\Device\\HarddiskVolume1\\folder\\file.txt"))); + } + } + + WHEN("getting filename from file without stream") + { + USimpleString fileWithoutStream(L"\\Device\\HarddiskVolume1\\folder\\file.txt"); + auto result = FilenameUtils::getFileNameNoStream(fileWithoutStream); + + THEN("filename remains unchanged") + { + REQUIRE(result.equals(USimpleString(L"\\Device\\HarddiskVolume1\\folder\\file.txt"))); + } + } + + WHEN("getting filename from empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::getFileNameNoStream(emptyPath); + + THEN("empty path returns empty") + { + REQUIRE(result.equals(USimpleString(L""))); + } + } + } +} + +SCENARIO("FilenameUtils getExtension") +{ + GIVEN("various file paths") + { + WHEN("getting extension from file with extension") + { + USimpleString fileWithExt(L"\\Device\\HarddiskVolume1\\folder\\file.txt"); + auto result = FilenameUtils::getExtension(fileWithExt); + + THEN("extension is extracted correctly") + { + REQUIRE(result.equals(USimpleString(L"txt"))); + } + } + + WHEN("getting extension from file without extension") + { + USimpleString fileWithoutExt(L"\\Device\\HarddiskVolume1\\folder\\file"); + auto result = FilenameUtils::getExtension(fileWithoutExt); + + THEN("empty extension is returned") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting extension from empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::getExtension(emptyPath); + + THEN("empty extension is returned") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting extension from path only") + { + USimpleString pathOnly(L"\\Device\\HarddiskVolume1\\folder\\"); + auto result = FilenameUtils::getExtension(pathOnly); + + THEN("empty extension is returned") + { + REQUIRE(result.isEmpty()); + } + } + } +} + +SCENARIO("FilenameUtils removeExtension") +{ + GIVEN("various file paths") + { + WHEN("removing extension from file with extension") + { + USimpleString fileWithExt(L"\\Device\\HarddiskVolume1\\folder\\file.txt"); + auto result = FilenameUtils::removeExtension(fileWithExt); + + THEN("extension is removed correctly") + { + REQUIRE(result.equals(USimpleString(L"\\Device\\HarddiskVolume1\\folder\\file"))); + } + } + + WHEN("removing extension from file without extension") + { + USimpleString fileWithoutExt(L"\\Device\\HarddiskVolume1\\folder\\file"); + auto result = FilenameUtils::removeExtension(fileWithoutExt); + + THEN("file path remains unchanged") + { + REQUIRE(result.equals(USimpleString(L"\\Device\\HarddiskVolume1\\folder\\file"))); + } + } + + WHEN("removing extension from empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::removeExtension(emptyPath); + + THEN("empty path remains empty") + { + REQUIRE(result.isEmpty()); + } + } + } +} + +SCENARIO("FilenameUtils getName") +{ + GIVEN("various file paths") + { + WHEN("getting filename from full path") + { + USimpleString fullPath(L"\\Device\\HarddiskVolume1\\Windows\\System32\\kernel32.dll"); + auto result = FilenameUtils::getName(fullPath); + + THEN("filename is extracted correctly") + { + REQUIRE(result.equals(USimpleString(L"kernel32.dll"))); + } + } + + WHEN("getting filename from path without slashes") + { + USimpleString pathWithoutSlashes(L"file.txt"); + auto result = FilenameUtils::getName(pathWithoutSlashes); + + THEN("entire string is returned as filename") + { + REQUIRE(result.equals(USimpleString(L"file.txt"))); + } + } + + WHEN("getting filename from path ending with slash") + { + USimpleString pathEndingWithSlash(L"\\Device\\HarddiskVolume1\\Windows\\"); + auto result = FilenameUtils::getName(pathEndingWithSlash); + + THEN("empty filename is returned") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting filename from empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::getName(emptyPath); + + THEN("empty filename is returned") + { + REQUIRE(result.isEmpty()); + } + } + } +} + +SCENARIO("FilenameUtils getServerAndShareName") +{ + GIVEN("various network and local paths") + { + WHEN("getting server and share from MUP path") + { + USimpleString mupPath(L"\\device\\mup\\172.24.79.245\\my-dfs\\dir\\file"); + auto result = FilenameUtils::getServerAndShareName(mupPath); + + THEN("server and share name is extracted correctly") + { + REQUIRE(result.equals(USimpleString(L"\\172.24.79.245\\my-dfs"))); + } + } + + WHEN("getting server and share from regular path") + { + USimpleString regularPath(L"\\Device\\HarddiskVolume1\\Windows\\System32\\file.txt"); + auto result = FilenameUtils::getServerAndShareName(regularPath); + + THEN("empty result is returned for non-MUP path") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting server and share from empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::getServerAndShareName(emptyPath); + + THEN("empty result is returned for empty path") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting server and share from MUP path with only server (no share)") + { + USimpleString mupPathNoShare(L"\\device\\mup\\server.domain.com"); + auto result = FilenameUtils::getServerAndShareName(mupPathNoShare); + + THEN("empty result is returned since function requires both server and share") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("getting server and share from MUP path with FQDN server name") + { + USimpleString mupPathFQDN(L"\\device\\mup\\server.domain.com\\share\\dir\\file"); + auto result = FilenameUtils::getServerAndShareName(mupPathFQDN); + + THEN("FQDN server and share name is extracted correctly") + { + REQUIRE(result.equals(USimpleString(L"\\server.domain.com\\share"))); + } + } + + WHEN("getting server and share from UPPERCASED MUP path") + { + USimpleString upperMupPath(L"\\DEVICE\\MUP\\172.24.79.245\\MY-DFS\\DIR\\FILE"); + auto result = FilenameUtils::getServerAndShareName(upperMupPath); + + THEN("server and share name is extracted correctly despite case") + { + REQUIRE(result.equals(USimpleString(L"\\172.24.79.245\\MY-DFS"))); + } + } + } +} + +SCENARIO("FilenameUtils getNameCount") +{ + GIVEN("various paths") + { + WHEN("counting elements in empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::getNameCount(emptyPath); + + THEN("element count is zero") + { + REQUIRE(result == 0); + } + } + + WHEN("counting elements in path with only slashes") + { + USimpleString slashesOnly(L"\\\\"); + auto result = FilenameUtils::getNameCount(slashesOnly); + + THEN("element count is zero") + { + REQUIRE(result == 0); + } + } + + WHEN("counting elements in single element path") + { + USimpleString singleElement(L"\\aa"); + auto result = FilenameUtils::getNameCount(singleElement); + + THEN("element count is one") + { + REQUIRE(result == 1); + } + } + + WHEN("counting elements in multi-element path") + { + USimpleString multiElement(L"\\aa\\bb\\cc"); + auto result = FilenameUtils::getNameCount(multiElement); + + THEN("element count is correct") + { + REQUIRE(result == 3); + } + } + + WHEN("counting elements in path with slash at the end") + { + USimpleString pathWithEndSlash(L"\\aa\\bb\\cc\\"); + auto result = FilenameUtils::getNameCount(pathWithEndSlash); + + THEN("element count ignores trailing slash") + { + REQUIRE(result == 3); + } + } + } +} + +SCENARIO("FilenameUtils subpath") +{ + GIVEN("paths with multiple elements") + { + WHEN("extracting subpath from valid path") + { + USimpleString path(L"\\aa\\bb\\cc"); + auto result = FilenameUtils::subpath(path, 0, 2); + + THEN("subpath is extracted correctly") + { + REQUIRE(result.equals(USimpleString(L"aa\\bb"))); + } + } + + WHEN("extracting subpath with invalid parameters") + { + USimpleString path(L"\\aa\\bb"); + auto result = FilenameUtils::subpath(path, -1, 1); + + THEN("empty result is returned") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("extracting subpath from empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::subpath(emptyPath, 0, 1); + + THEN("empty result is returned") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("extracting subpath where first parameter is negative and second is too big") + { + USimpleString path(L"\\aa\\bb"); + auto result = FilenameUtils::subpath(path, -1, 10); + + THEN("empty result is returned") + { + REQUIRE(result.isEmpty()); + } + } + + WHEN("extracting subpath where both parameters are equal") + { + USimpleString path(L"\\aa\\bb\\cc"); + auto result = FilenameUtils::subpath(path, 1, 1); + + THEN("single element is extracted") + { + REQUIRE(result.equals(USimpleString(L"bb"))); + } + } + } +} + +SCENARIO("FilenameUtils dosNameToNative") +{ + GIVEN("various DOS path formats") + { + WHEN("converting extended DOS path") + { + USimpleString extendedPath(L"\\\\?\\C:\\Windows\\System32\\file.txt"); + auto result = FilenameUtils::dosNameToNative(extendedPath); + + THEN("path is converted to native NT format") + { + REQUIRE(result.equals(USimpleString(L"\\??\\C:\\Windows\\System32\\file.txt"))); + } + } + + WHEN("converting UNC DOS path") + { + USimpleString uncPath(L"\\\\server\\share\\folder\\file.txt"); + auto result = FilenameUtils::dosNameToNative(uncPath); + + THEN("path is converted to MUP device format") + { + REQUIRE(result.equals(USimpleString(L"\\device\\mup\\server\\share\\folder\\file.txt"))); + } + } + + WHEN("converting regular DOS path") + { + USimpleString regularPath(L"C:\\Windows\\System32\\file.txt"); + auto result = FilenameUtils::dosNameToNative(regularPath); + + THEN("path is converted to native NT format") + { + REQUIRE(result.equals(USimpleString(L"\\??\\C:\\Windows\\System32\\file.txt"))); + } + } + + WHEN("converting empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::dosNameToNative(emptyPath); + + THEN("result should be empty") + { + REQUIRE(result.isEmpty()); + } + } + } +} + +SCENARIO("FilenameUtils isAbsoluteRegistryPath") +{ + GIVEN("various path types") + { + WHEN("checking registry path") + { + USimpleString registryPath(L"\\REGISTRY\\MACHINE\\SOFTWARE\\Test"); + auto result = FilenameUtils::isAbsoluteRegistryPath(registryPath); + + THEN("registry path is identified correctly") + { + REQUIRE(result == true); + } + } + + WHEN("checking regular path") + { + USimpleString regularPath(L"\\Device\\HarddiskVolume1\\Windows\\System32\\file.txt"); + auto result = FilenameUtils::isAbsoluteRegistryPath(regularPath); + + THEN("regular path is not identified as registry path") + { + REQUIRE(result == false); + } + } + + WHEN("checking empty path") + { + USimpleString emptyPath(L""); + auto result = FilenameUtils::isAbsoluteRegistryPath(emptyPath); + + THEN("empty path is not identified as registry path") + { + REQUIRE(result == false); + } + } + } +} \ No newline at end of file diff --git a/test/FilenameUtilsTest.cpp.backup b/test/FilenameUtilsTest.cpp.backup new file mode 100644 index 0000000..abde507 --- /dev/null +++ b/test/FilenameUtilsTest.cpp.backup @@ -0,0 +1,430 @@ +#include "pch.h" +#include + +SCENARIO("FilenameUtils getPathNoEndSeparator") +{ + GIVEN("various file paths with backslashes") + { + WHEN("getting path without end separator for native NT path") + { + kf::USimpleString simplePath(L"\\Device\\HarddiskVolume1\\Windows\\System32\\kernel32.dll"); + auto simplePathResult = kf::FilenameUtils::getPathNoEndSeparator(simplePath); + + THEN("native NT path is extracted correctly without trailing separator") + { + REQUIRE(simplePathResult.equals(kf::USimpleString(L"\\Device\\HarddiskVolume1\\Windows\\System32"))); + } + } + + WHEN("getting path without end separator for MUP network path") + { + kf::USimpleString networkPath(L"\\Device\\Mup\\server\\share\\folder\\file.txt"); + auto networkPathResult = kf::FilenameUtils::getPathNoEndSeparator(networkPath); + + THEN("MUP network path is extracted correctly without trailing separator") + { + REQUIRE(networkPathResult.equals(kf::USimpleString(L"\\Device\\Mup\\server\\share\\folder"))); + } + } + + WHEN("getting path without end separator for path ending with backslash") + { + kf::USimpleString pathEndingWithBackslash(L"\\Device\\HarddiskVolume1\\Windows\\"); + auto pathEndingWithBackslashResult = kf::FilenameUtils::getPathNoEndSeparator(pathEndingWithBackslash); + + THEN("path ending with backslash is extracted correctly without trailing separator") + { + REQUIRE(pathEndingWithBackslashResult.equals(kf::USimpleString(L"\\Device\\HarddiskVolume1\\Windows"))); + } + } + + WHEN("getting path without end separator for root path") + { + kf::USimpleString rootPath(L"\\file.txt"); + auto rootPathResult = kf::FilenameUtils::getPathNoEndSeparator(rootPath); + + THEN("root path returns empty") + { + REQUIRE(rootPathResult.isEmpty()); + } + } + } +} + +SCENARIO("FilenameUtils getPathWithEndSeparator") +{ + GIVEN("various file paths with backslashes") + { + WHEN("getting path with end separator for native NT path") + { + kf::USimpleString simplePath(L"\\Device\\HarddiskVolume1\\Windows\\System32\\kernel32.dll"); + auto simplePathResult = kf::FilenameUtils::getPathWithEndSeparator(simplePath); + + THEN("native NT path is extracted correctly with trailing separator") + { + REQUIRE(simplePathResult.equals(kf::USimpleString(L"\\Device\\HarddiskVolume1\\Windows\\System32\\"))); + } + } + + WHEN("getting path with end separator for MUP network path") + { + kf::USimpleString networkPath(L"\\Device\\Mup\\server\\share\\folder\\file.txt"); + auto networkPathResult = kf::FilenameUtils::getPathWithEndSeparator(networkPath); + + THEN("MUP network path is extracted correctly with trailing separator") + { + REQUIRE(networkPathResult.equals(kf::USimpleString(L"\\Device\\Mup\\server\\share\\folder\\"))); + } + } + + WHEN("getting path with end separator for root path") + { + kf::USimpleString rootPath(L"\\file.txt"); + auto rootPathResult = kf::FilenameUtils::getPathWithEndSeparator(rootPath); + + THEN("root path returns with trailing separator") + { + REQUIRE(rootPathResult.equals(kf::USimpleString(L"\\"))); + } + } + } +} + +SCENARIO("FilenameUtils getFileNameNoStream") +{ + GIVEN("various file paths with and without streams") + { + WHEN("getting filename without stream for file with single stream") + { + kf::USimpleString fileWithStream(L"\\Device\\HarddiskVolume1\\folder\\file.txt:stream1"); + auto fileWithStreamResult = kf::FilenameUtils::getFileNameNoStream(fileWithStream); + + THEN("filename is extracted correctly without stream") + { + REQUIRE(fileWithStreamResult.equals(kf::USimpleString(L"\\Device\\HarddiskVolume1\\folder\\file.txt"))); + } + } + + WHEN("getting filename without stream for file with multiple streams") + { + kf::USimpleString fileWithMultipleStreams(L"\\Device\\HarddiskVolume1\\folder\\file.txt:stream1:stream2"); + auto fileWithMultipleStreamsResult = kf::FilenameUtils::getFileNameNoStream(fileWithMultipleStreams); + + THEN("filename is extracted correctly without any streams") + { + REQUIRE(fileWithMultipleStreamsResult.equals(kf::USimpleString(L"\\Device\\HarddiskVolume1\\folder\\file.txt"))); + } + } + + WHEN("getting filename without stream for file without stream") + { + kf::USimpleString fileWithoutStream(L"\\Device\\HarddiskVolume1\\folder\\file.txt"); + auto fileWithoutStreamResult = kf::FilenameUtils::getFileNameNoStream(fileWithoutStream); + + THEN("filename remains unchanged") + { + REQUIRE(fileWithoutStreamResult.equals(kf::USimpleString(L"\\Device\\HarddiskVolume1\\folder\\file.txt"))); + } + } + + WHEN("getting filename without stream for path with stream in folder name") + { + kf::USimpleString pathWithStreamInFolder(L"\\Device\\HarddiskVolume1\\folder:stream\\file.txt"); + auto pathWithStreamInFolderResult = kf::FilenameUtils::getFileNameNoStream(pathWithStreamInFolder); + + THEN("path remains unchanged as stream is in folder name") + { + REQUIRE(pathWithStreamInFolderResult.equals(kf::USimpleString(L"\\Device\\HarddiskVolume1\\folder:stream\\file.txt"))); + } + } + + WHEN("getting filename without stream for empty path") + { + kf::USimpleString emptyPath(L""); + auto emptyPathResult = kf::FilenameUtils::getFileNameNoStream(emptyPath); + + THEN("empty path returns empty") + { + REQUIRE(emptyPathResult.equals(kf::USimpleString(L""))); + } + } + } +} + +SCENARIO("FilenameUtils getExtension") +{ + GIVEN("various file paths with and without extensions") + { + kf::USimpleString fileWithExt(L"C:\\folder\\file.txt"); + kf::USimpleString fileWithMultipleExt(L"C:\\folder\\file.tar.gz"); + kf::USimpleString fileWithoutExt(L"C:\\folder\\file"); + kf::USimpleString fileWithExtAndStream(L"C:\\folder\\file.txt:stream"); + kf::USimpleString pathOnly(L"C:\\folder\\"); + kf::USimpleString emptyPath(L""); + kf::USimpleString dotFile(L"C:\\folder\\.hidden"); + + WHEN("getting file extension") + { + auto fileWithExtResult = kf::FilenameUtils::getExtension(fileWithExt); + auto fileWithMultipleExtResult = kf::FilenameUtils::getExtension(fileWithMultipleExt); + auto fileWithoutExtResult = kf::FilenameUtils::getExtension(fileWithoutExt); + auto fileWithExtAndStreamResult = kf::FilenameUtils::getExtension(fileWithExtAndStream); + auto pathOnlyResult = kf::FilenameUtils::getExtension(pathOnly); + auto emptyPathResult = kf::FilenameUtils::getExtension(emptyPath); + auto dotFileResult = kf::FilenameUtils::getExtension(dotFile); + + THEN("extensions are extracted correctly") + { + REQUIRE(fileWithExtResult.equals(kf::USimpleString(L"txt"))); + REQUIRE(fileWithMultipleExtResult.equals(kf::USimpleString(L"gz"))); + REQUIRE(fileWithoutExtResult.isEmpty()); + REQUIRE(fileWithExtAndStreamResult.equals(kf::USimpleString(L"txt"))); + REQUIRE(pathOnlyResult.isEmpty()); + REQUIRE(emptyPathResult.isEmpty()); + REQUIRE(dotFileResult.equals(kf::USimpleString(L"hidden"))); + } + } + } +} + +SCENARIO("FilenameUtils removeExtension") +{ + GIVEN("various file paths with and without extensions") + { + kf::USimpleString fileWithExt(L"C:\\folder\\file.txt"); + kf::USimpleString fileWithMultipleExt(L"C:\\folder\\file.tar.gz"); + kf::USimpleString fileWithoutExt(L"C:\\folder\\file"); + kf::USimpleString pathOnly(L"C:\\folder\\"); + kf::USimpleString emptyPath(L""); + kf::USimpleString dotFile(L"C:\\folder\\.hidden"); + + WHEN("removing file extension") + { + auto fileWithExtResult = kf::FilenameUtils::removeExtension(fileWithExt); + auto fileWithMultipleExtResult = kf::FilenameUtils::removeExtension(fileWithMultipleExt); + auto fileWithoutExtResult = kf::FilenameUtils::removeExtension(fileWithoutExt); + auto pathOnlyResult = kf::FilenameUtils::removeExtension(pathOnly); + auto emptyPathResult = kf::FilenameUtils::removeExtension(emptyPath); + auto dotFileResult = kf::FilenameUtils::removeExtension(dotFile); + + THEN("extensions are removed correctly") + { + REQUIRE(fileWithExtResult.equals(kf::USimpleString(L"C:\\folder\\file"))); + REQUIRE(fileWithMultipleExtResult.equals(kf::USimpleString(L"C:\\folder\\file.tar"))); + REQUIRE(fileWithoutExtResult.equals(kf::USimpleString(L"C:\\folder\\file"))); + REQUIRE(pathOnlyResult.equals(kf::USimpleString(L"C:\\folder\\"))); + REQUIRE(emptyPathResult.isEmpty()); + REQUIRE(dotFileResult.equals(kf::USimpleString(L"C:\\folder\\."))); + } + } + } +} + +SCENARIO("FilenameUtils getName") +{ + GIVEN("various file paths") + { + kf::USimpleString fullPath(L"C:\\Windows\\System32\\kernel32.dll"); + kf::USimpleString networkPath(L"\\\\server\\share\\folder\\file.txt"); + kf::USimpleString rootFile(L"\\file.txt"); + kf::USimpleString pathWithoutBackslash(L"file.txt"); + kf::USimpleString pathEndingWithBackslash(L"C:\\Windows\\"); + kf::USimpleString emptyPath(L""); + + WHEN("getting filename from path") + { + auto fullPathResult = kf::FilenameUtils::getName(fullPath); + auto networkPathResult = kf::FilenameUtils::getName(networkPath); + auto rootFileResult = kf::FilenameUtils::getName(rootFile); + auto pathWithoutBackslashResult = kf::FilenameUtils::getName(pathWithoutBackslash); + auto pathEndingWithBackslashResult = kf::FilenameUtils::getName(pathEndingWithBackslash); + auto emptyPathResult = kf::FilenameUtils::getName(emptyPath); + + THEN("filenames are extracted correctly") + { + REQUIRE(fullPathResult.equals(kf::USimpleString(L"kernel32.dll"))); + REQUIRE(networkPathResult.equals(kf::USimpleString(L"file.txt"))); + REQUIRE(rootFileResult.equals(kf::USimpleString(L"file.txt"))); + REQUIRE(pathWithoutBackslashResult.equals(kf::USimpleString(L"file.txt"))); // idx = -1, so returns entire string + REQUIRE(pathEndingWithBackslashResult.isEmpty()); // after last backslash is empty + REQUIRE(emptyPathResult.isEmpty()); // empty string has no backslash, returns entire string (which is empty) + } + } + } +} + +SCENARIO("FilenameUtils getServerAndShareName") +{ + GIVEN("various network and local paths") + { + kf::USimpleString mupPath(L"\\device\\mup\\172.24.79.245\\my-dfs\\dir\\file"); + kf::USimpleString mupPathUpperCase(L"\\DEVICE\\MUP\\192.168.1.1\\share\\folder\\test.txt"); + kf::USimpleString regularPath(L"C:\\Windows\\System32\\file.txt"); + kf::USimpleString partialMupPath(L"\\device\\mup\\server"); + kf::USimpleString emptyPath(L""); + kf::USimpleString invalidMupPath(L"\\device\\other\\server\\share"); + + WHEN("getting server and share name") + { + auto mupPathResult = kf::FilenameUtils::getServerAndShareName(mupPath); + auto mupPathUpperCaseResult = kf::FilenameUtils::getServerAndShareName(mupPathUpperCase); + auto regularPathResult = kf::FilenameUtils::getServerAndShareName(regularPath); + auto partialMupPathResult = kf::FilenameUtils::getServerAndShareName(partialMupPath); + auto emptyPathResult = kf::FilenameUtils::getServerAndShareName(emptyPath); + auto invalidMupPathResult = kf::FilenameUtils::getServerAndShareName(invalidMupPath); + + THEN("server and share names are extracted correctly") + { + REQUIRE(mupPathResult.equals(kf::USimpleString(L"\\172.24.79.245\\my-dfs"))); + REQUIRE(mupPathUpperCaseResult.equals(kf::USimpleString(L"\\192.168.1.1\\share"))); + REQUIRE(regularPathResult.isEmpty()); + REQUIRE(partialMupPathResult.isEmpty()); + REQUIRE(emptyPathResult.isEmpty()); + REQUIRE(invalidMupPathResult.isEmpty()); + } + } + } +} + +SCENARIO("FilenameUtils getNameCount") +{ + GIVEN("various paths with different element counts") + { + kf::USimpleString emptyPath(L""); + kf::USimpleString rootPath(L"\\"); + kf::USimpleString doubleBackslash(L"\\\\"); + kf::USimpleString singleElement(L"aa"); + kf::USimpleString singleElementWithLeadingSlash(L"\\aa"); + kf::USimpleString singleElementWithTrailingSlash(L"\\aa\\"); + kf::USimpleString twoElements(L"\\aa\\bb"); + kf::USimpleString twoElementsWithTrailingSlash(L"\\aa\\bb\\"); + kf::USimpleString twoElementsNoLeadingSlash(L"aa\\bb"); + + WHEN("counting path elements") + { + auto emptyPathCount = kf::FilenameUtils::getNameCount(emptyPath); + auto rootPathCount = kf::FilenameUtils::getNameCount(rootPath); + auto doubleBackslashCount = kf::FilenameUtils::getNameCount(doubleBackslash); + auto singleElementCount = kf::FilenameUtils::getNameCount(singleElement); + auto singleElementWithLeadingSlashCount = kf::FilenameUtils::getNameCount(singleElementWithLeadingSlash); + auto singleElementWithTrailingSlashCount = kf::FilenameUtils::getNameCount(singleElementWithTrailingSlash); + auto twoElementsCount = kf::FilenameUtils::getNameCount(twoElements); + auto twoElementsWithTrailingSlashCount = kf::FilenameUtils::getNameCount(twoElementsWithTrailingSlash); + auto twoElementsNoLeadingSlashCount = kf::FilenameUtils::getNameCount(twoElementsNoLeadingSlash); + + THEN("element counts are correct") + { + REQUIRE(emptyPathCount == 0); + REQUIRE(rootPathCount == 0); + REQUIRE(doubleBackslashCount == 0); + REQUIRE(singleElementCount == 1); + REQUIRE(singleElementWithLeadingSlashCount == 1); + REQUIRE(singleElementWithTrailingSlashCount == 1); + REQUIRE(twoElementsCount == 2); + REQUIRE(twoElementsWithTrailingSlashCount == 2); + REQUIRE(twoElementsNoLeadingSlashCount == 2); + } + } + } +} + +SCENARIO("FilenameUtils subpath") +{ + GIVEN("paths with multiple elements") + { + kf::USimpleString pathAaBb(L"aa\\bb"); + kf::USimpleString pathWithLeadingSlash(L"\\aa\\bb"); + kf::USimpleString pathWithTrailingSlash(L"\\aa\\bb\\"); + kf::USimpleString longPath(L"\\one\\two\\three\\four\\five"); + kf::USimpleString emptyPath(L""); + kf::USimpleString singleElement(L"test"); + + WHEN("extracting subpaths with various parameters") + { + auto pathAaBbResult1 = kf::FilenameUtils::subpath(pathAaBb, 0, 1); + auto pathWithLeadingSlashResult1 = kf::FilenameUtils::subpath(pathWithLeadingSlash, 0, 1); + auto pathWithLeadingSlashResult2 = kf::FilenameUtils::subpath(pathWithLeadingSlash, 0, 2); + auto pathWithTrailingSlashResult = kf::FilenameUtils::subpath(pathWithTrailingSlash, 0, 2); + auto longPathResult1 = kf::FilenameUtils::subpath(longPath, 1, 2); + auto longPathResult2 = kf::FilenameUtils::subpath(longPath, 2); + auto emptyPathResult = kf::FilenameUtils::subpath(emptyPath, 0, 1); + auto invalidIndexResult = kf::FilenameUtils::subpath(pathAaBb, -1, 1); + auto invalidCountResult = kf::FilenameUtils::subpath(pathAaBb, 0, -1); + auto outOfRangeResult = kf::FilenameUtils::subpath(singleElement, 5, 1); + + THEN("subpaths are extracted correctly") + { + REQUIRE(pathAaBbResult1.equals(kf::USimpleString(L"aa"))); + REQUIRE(pathWithLeadingSlashResult1.equals(kf::USimpleString(L"aa"))); + REQUIRE(pathWithLeadingSlashResult2.equals(kf::USimpleString(L"aa\\bb"))); + REQUIRE(pathWithTrailingSlashResult.equals(kf::USimpleString(L"aa\\bb"))); + REQUIRE(longPathResult1.equals(kf::USimpleString(L"two\\three"))); + REQUIRE(longPathResult2.equals(kf::USimpleString(L"three\\four\\five"))); + REQUIRE(emptyPathResult.isEmpty()); + REQUIRE(invalidIndexResult.isEmpty()); + REQUIRE(invalidCountResult.isEmpty()); + REQUIRE(outOfRangeResult.isEmpty()); + } + } + } +} + +SCENARIO("FilenameUtils dosNameToNative") +{ + GIVEN("various DOS path formats") + { + kf::USimpleString extendedPath(L"\\\\?\\C:\\Windows\\System32\\file.txt"); + kf::USimpleString uncPath(L"\\\\server\\share\\folder\\file.txt"); + kf::USimpleString regularDosPath(L"C:\\Windows\\System32\\file.txt"); + kf::USimpleString emptyPath(L""); + + WHEN("converting DOS names to native format") + { + auto extendedPathResult = kf::FilenameUtils::dosNameToNative(extendedPath); + auto uncPathResult = kf::FilenameUtils::dosNameToNative(uncPath); + auto regularDosPathResult = kf::FilenameUtils::dosNameToNative(regularDosPath); + auto emptyPathResult = kf::FilenameUtils::dosNameToNative(emptyPath); + + THEN("paths are converted to native format correctly") + { + REQUIRE(extendedPathResult.equals(kf::USimpleString(L"\\??\\C:\\Windows\\System32\\file.txt"))); + REQUIRE(uncPathResult.equals(kf::USimpleString(L"\\device\\mup\\server\\share\\folder\\file.txt"))); + REQUIRE(regularDosPathResult.equals(kf::USimpleString(L"\\??\\C:\\Windows\\System32\\file.txt"))); + REQUIRE(emptyPathResult.equals(kf::USimpleString(L"\\??\\"))); + } + } + } +} + +SCENARIO("FilenameUtils isAbsoluteRegistryPath") +{ + GIVEN("various path types") + { + kf::USimpleString registryPath(L"\\REGISTRY\\MACHINE\\SOFTWARE\\Test"); + kf::USimpleString registryPathLowerCase(L"\\registry\\user\\test"); + kf::USimpleString regularPath(L"C:\\Windows\\System32\\file.txt"); + kf::USimpleString partialRegistryPath(L"REGISTRY\\MACHINE\\test"); + kf::USimpleString emptyPath(L""); + kf::USimpleString registryInPath(L"C:\\REGISTRY\\file.txt"); + + WHEN("checking if paths are absolute registry paths") + { + auto registryPathResult = kf::FilenameUtils::isAbsoluteRegistryPath(registryPath); + auto registryPathLowerCaseResult = kf::FilenameUtils::isAbsoluteRegistryPath(registryPathLowerCase); + auto regularPathResult = kf::FilenameUtils::isAbsoluteRegistryPath(regularPath); + auto partialRegistryPathResult = kf::FilenameUtils::isAbsoluteRegistryPath(partialRegistryPath); + auto emptyPathResult = kf::FilenameUtils::isAbsoluteRegistryPath(emptyPath); + auto registryInPathResult = kf::FilenameUtils::isAbsoluteRegistryPath(registryInPath); + + THEN("registry paths are identified correctly") + { + REQUIRE(registryPathResult == true); + REQUIRE(registryPathLowerCaseResult == true); + REQUIRE(regularPathResult == false); + REQUIRE(partialRegistryPathResult == false); + REQUIRE(emptyPathResult == false); + REQUIRE(registryInPathResult == false); + } + } + } +} \ No newline at end of file diff --git a/test/pch.h b/test/pch.h index b5c57a3..6c761b1 100644 --- a/test/pch.h +++ b/test/pch.h @@ -41,3 +41,18 @@ namespace std KeBugCheckEx(KERNEL_SECURITY_CHECK_FAILURE, 0, 0, 0, 0); } } + +_ACRTIMP inline void __cdecl _invoke_watson( + _In_opt_z_ wchar_t const* _Expression, + _In_opt_z_ wchar_t const* _FunctionName, + _In_opt_z_ wchar_t const* _FileName, + _In_ unsigned int _LineNo, + _In_ uintptr_t _Reserved) +{ + UNREFERENCED_PARAMETER(_Expression); + UNREFERENCED_PARAMETER(_FunctionName); + UNREFERENCED_PARAMETER(_FileName); + UNREFERENCED_PARAMETER(_LineNo); + UNREFERENCED_PARAMETER(_Reserved); + // TODO: assert the expression +} \ No newline at end of file