From ed9068285d9d682e678e7d7c55a596139187c16c Mon Sep 17 00:00:00 2001 From: Oleksii Gaidadim Date: Sat, 6 Jun 2026 19:34:28 +0200 Subject: [PATCH 1/4] chore(vscode): Remove unnecessary VSCode settings --- .vscode/settings.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2cf65f1..71ad00a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -143,12 +143,10 @@ "**/.cache": true }, "indentRainbow.updateDelay": 100, - "cmake.ctest.testExplorerIntegrationEnabled": false, "sonarlint.connectedMode.project": { "connectionId": "olxgdm", "projectKey": "olxgdm_cfgsync" }, "sonarlint.pathToCompileCommands": "${workspaceFolder}/build/compile_commands.json", - "sonarlint.output.showVerboseLogs": true, - "sonarlint.output.showAnalyzerLogs": true + "sonarlint.output.showVerboseLogs": true } From 6f7268a5342ddc77599e18523d1bc0ed567778f4 Mon Sep 17 00:00:00 2001 From: Oleksii Gaidadim Date: Sat, 6 Jun 2026 19:34:28 +0200 Subject: [PATCH 2/4] test(path): Introduce helper for expected storage paths and necessary includes --- tests/PathFileUtilsTests.cpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/PathFileUtilsTests.cpp b/tests/PathFileUtilsTests.cpp index 1e163f7..b22aac6 100644 --- a/tests/PathFileUtilsTests.cpp +++ b/tests/PathFileUtilsTests.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #ifdef _WIN32 #include @@ -68,6 +70,35 @@ void RestoreEnvironmentVariable(const char* name, const std::optional Date: Sat, 6 Jun 2026 19:34:28 +0200 Subject: [PATCH 3/4] test(path): Enhance MakeStorageRelativePath and add ValidateStoredRelativePath tests --- tests/PathFileUtilsTests.cpp | 119 ++++++++++++++++++++++++++++------- 1 file changed, 97 insertions(+), 22 deletions(-) diff --git a/tests/PathFileUtilsTests.cpp b/tests/PathFileUtilsTests.cpp index b22aac6..4b359e9 100644 --- a/tests/PathFileUtilsTests.cpp +++ b/tests/PathFileUtilsTests.cpp @@ -183,6 +183,12 @@ TEST_F(PathFileUtilsTest, MakeStorageRelativePathMapsPosixPathUnderFiles) { EXPECT_EQ(storagePath.generic_string(), "files/home/user/.gitconfig"); } +TEST_F(PathFileUtilsTest, MakeStorageRelativePathNormalizesPosixPathBeforeMapping) { + const auto storagePath = cfgsync::utils::MakeStorageRelativePath("/home/user/../user/.config/./nvim/init.lua"); + + EXPECT_EQ(storagePath.generic_string(), "files/home/user/.config/nvim/init.lua"); +} + TEST_F(PathFileUtilsTest, MakeStorageRelativePathMapsNestedPosixPathUnderFiles) { const auto storagePath = cfgsync::utils::MakeStorageRelativePath("/home/user/.config/nvim/init.lua"); @@ -195,38 +201,107 @@ TEST_F(PathFileUtilsTest, MakeStorageRelativePathMapsWindowsDrivePathUnderFiles) EXPECT_EQ(storagePath.generic_string(), "files/C/Users/Oleksii/.gitconfig"); } +TEST_F(PathFileUtilsTest, MakeStorageRelativePathMapsWindowsDrivePathsWithSlashVariantsUnderFiles) { + struct TestCase { + std::string_view Input; + std::string_view Expected; + }; + + const std::vector testCases{ + {R"(C:/Users/Oleksii/.gitconfig)", "files/C/Users/Oleksii/.gitconfig"}, + {R"(C:\Users\Oleksii\.gitconfig)", "files/C/Users/Oleksii/.gitconfig"}, + {R"(C:\Users/Oleksii\.config/nvim/init.lua)", "files/C/Users/Oleksii/.config/nvim/init.lua"}, + }; + + for (const auto& testCase : testCases) { + SCOPED_TRACE(testCase.Input); + const auto storagePath = cfgsync::utils::MakeStorageRelativePath(fs::path{testCase.Input}); + + EXPECT_EQ(storagePath.generic_string(), testCase.Expected); + } +} + TEST_F(PathFileUtilsTest, MakeStorageRelativePathNormalizesRelativeInputUnderFiles) { const auto inputPath = fs::path{"relative"} / ".." / "cfgsync-relative.conf"; const auto normalizedPath = cfgsync::utils::NormalizePath(inputPath); - fs::path expectedPath{"files"}; - const auto rootName = normalizedPath.root_name().generic_string(); - if (!rootName.empty()) { - std::string sanitizedRoot = rootName; - sanitizedRoot.erase( - std::remove_if(sanitizedRoot.begin(), sanitizedRoot.end(), - [](char character) { return character == ':' || character == '/' || character == '\\'; }), - sanitizedRoot.end()); - if (!sanitizedRoot.empty()) { - expectedPath /= sanitizedRoot; - } - } + const auto storagePath = cfgsync::utils::MakeStorageRelativePath(inputPath); - for (const auto& component : normalizedPath) { - if (!normalizedPath.root_name().empty() && component == normalizedPath.root_name()) { - continue; - } + EXPECT_EQ(storagePath, ExpectedStorageRelativePathForNormalizedPath(normalizedPath)); + EXPECT_EQ(cfgsync::utils::ValidateStoredRelativePath(storagePath.generic_string()), + cfgsync::utils::StoredRelativePathValidationError::None); +} - if (component == normalizedPath.root_directory()) { - continue; - } +TEST_F(PathFileUtilsTest, ValidateStoredRelativePathAcceptsFilesChildrenWithSlashVariants) { + const std::vector validStoredRelativePaths{ + "files/home/user/.gitconfig", + R"(files\home\user\.gitconfig)", + R"(files/home\user/.config\nvim/init.lua)", + }; + + for (const auto& storedRelativePath : validStoredRelativePaths) { + SCOPED_TRACE(storedRelativePath); + EXPECT_EQ(cfgsync::utils::ValidateStoredRelativePath(storedRelativePath), + cfgsync::utils::StoredRelativePathValidationError::None); + } +} - expectedPath /= component; +TEST_F(PathFileUtilsTest, ValidateStoredRelativePathRejectsMalformedOrRootedPaths) { + struct TestCase { + std::string StoredRelativePath; + cfgsync::utils::StoredRelativePathValidationError ExpectedError; + }; + + const std::vector testCases{ + {"", cfgsync::utils::StoredRelativePathValidationError::Empty}, + {"/files/x", cfgsync::utils::StoredRelativePathValidationError::Absolute}, + {"C:files/x", cfgsync::utils::StoredRelativePathValidationError::Absolute}, + {R"(C:\files\x)", cfgsync::utils::StoredRelativePathValidationError::Absolute}, + {"files/../x", cfgsync::utils::StoredRelativePathValidationError::ParentTraversal}, + {R"(files\..\x)", cfgsync::utils::StoredRelativePathValidationError::ParentTraversal}, + {"backup/foo", cfgsync::utils::StoredRelativePathValidationError::OutsideFiles}, + {"files", cfgsync::utils::StoredRelativePathValidationError::MissingFilesChild}, + }; + + for (const auto& testCase : testCases) { + SCOPED_TRACE(testCase.StoredRelativePath); + EXPECT_EQ(cfgsync::utils::ValidateStoredRelativePath(testCase.StoredRelativePath), testCase.ExpectedError); } +} - const auto storagePath = cfgsync::utils::MakeStorageRelativePath(inputPath); +TEST_F(PathFileUtilsTest, GeneratedStorageRelativePathsAlwaysValidate) { + const auto home = GetTestRoot() / "home"; + cfgsync::utils::EnsureDirectoryExists(home); + +#ifdef _WIN32 + const auto previousUserProfile = GetEnvironmentVariable("USERPROFILE"); + SetEnvironmentVariable("USERPROFILE", home.string()); +#else + const auto previousHome = GetEnvironmentVariable("HOME"); + SetEnvironmentVariable("HOME", home.string()); +#endif + + const std::vector originalPaths{ + "/home/user/.gitconfig", + R"(C:\Users\Oleksii\.gitconfig)", + R"(C:\Users/Oleksii\.config/nvim/init.lua)", + fs::path{"relative"} / ".." / "cfgsync-relative.conf", + "~/config/file.conf", + }; + + for (const auto& originalPath : originalPaths) { + SCOPED_TRACE(originalPath.generic_string()); + const auto storedRelativePath = cfgsync::utils::MakeStorageRelativePath(originalPath).generic_string(); - EXPECT_EQ(storagePath, expectedPath); + EXPECT_EQ(cfgsync::utils::ValidateStoredRelativePath(storedRelativePath), + cfgsync::utils::StoredRelativePathValidationError::None); + } + +#ifdef _WIN32 + RestoreEnvironmentVariable("USERPROFILE", previousUserProfile); +#else + RestoreEnvironmentVariable("HOME", previousHome); +#endif } TEST_F(PathFileUtilsTest, OrdinaryFileValidationAcceptsRegularFile) { From 224d9f61dc0417644516b3a525a81b8b6e2163bb Mon Sep 17 00:00:00 2001 From: Oleksii Gaidadim Date: Sat, 6 Jun 2026 19:34:28 +0200 Subject: [PATCH 4/4] test(validation): Extend unsafe path tests in Registry and StorageManager --- tests/RegistryTests.cpp | 12 +++++++++++- tests/StorageManagerTests.cpp | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/RegistryTests.cpp b/tests/RegistryTests.cpp index 3102161..9823f6a 100644 --- a/tests/RegistryTests.cpp +++ b/tests/RegistryTests.cpp @@ -206,7 +206,17 @@ TEST_F(RegistryTest, DuplicateOriginalPathsInFileThrowClearError) { TEST_F(RegistryTest, UnsafeStoredRelativePathsThrowClearError) { const std::vector unsafeStoredRelativePaths{ - "", "../escape", "files/../../escape", (StorageRoot() / "escape").string(), "backup/foo", "files", + "", + "../escape", + "files/../../escape", + "files/../x", + R"(files\..\x)", + (StorageRoot() / "escape").string(), + "/files/x", + "C:files/x", + R"(C:\files\x)", + "backup/foo", + "files", }; for (const auto& storedRelativePath : unsafeStoredRelativePaths) { diff --git a/tests/StorageManagerTests.cpp b/tests/StorageManagerTests.cpp index 16e3fe5..464360d 100644 --- a/tests/StorageManagerTests.cpp +++ b/tests/StorageManagerTests.cpp @@ -69,7 +69,17 @@ TEST_F(StorageManagerTest, ResolvesStoredPathFromTrackedRelativePath) { TEST_F(StorageManagerTest, UnsafeStoredRelativePathsThrowFileError) { const cfgsync::storage::StorageManager storageManager{StorageRoot()}; const std::vector unsafeStoredRelativePaths{ - "", "../escape", "files/../../escape", (StorageRoot() / "escape").string(), "backup/foo", "files", + "", + "../escape", + "files/../../escape", + "files/../x", + R"(files\..\x)", + (StorageRoot() / "escape").string(), + "/files/x", + "C:files/x", + R"(C:\files\x)", + "backup/foo", + "files", }; for (const auto& storedRelativePath : unsafeStoredRelativePaths) {