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 } diff --git a/tests/PathFileUtilsTests.cpp b/tests/PathFileUtilsTests.cpp index 1e163f7..4b359e9 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 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(cfgsync::utils::ValidateStoredRelativePath(storedRelativePath), + cfgsync::utils::StoredRelativePathValidationError::None); + } - EXPECT_EQ(storagePath, expectedPath); +#ifdef _WIN32 + RestoreEnvironmentVariable("USERPROFILE", previousUserProfile); +#else + RestoreEnvironmentVariable("HOME", previousHome); +#endif } TEST_F(PathFileUtilsTest, OrdinaryFileValidationAcceptsRegularFile) { 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) {