diff --git a/CMakeLists.txt b/CMakeLists.txt index 11f6fb35..73d19045 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,7 +128,7 @@ set(PSEUDOSEM_INCLUDE_DIRS "${SOURCE_DIR}/include") ExternalProject_Add(testing-metadata PREFIX "external" GIT_REPOSITORY "https://github.com/loot/testing-metadata" - GIT_TAG "1.3.0" + GIT_TAG "1.4.0" CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "") @@ -253,8 +253,7 @@ set (LOOT_API_HEADERS "${CMAKE_SOURCE_DIR}/include/loot/api.h" "${CMAKE_SOURCE_DIR}/src/api/helpers/git_helper.h" "${CMAKE_SOURCE_DIR}/src/api/helpers/crc.h" "${CMAKE_SOURCE_DIR}/src/api/helpers/logging.h" - "${CMAKE_SOURCE_DIR}/src/api/helpers/version.h" - "${CMAKE_SOURCE_DIR}/src/api/helpers/windows_encoding_converters.h") + "${CMAKE_SOURCE_DIR}/src/api/helpers/version.h") set (LOOT_TESTS_SRC "${CMAKE_SOURCE_DIR}/src/tests/api/internals/main.cpp") diff --git a/src/api/game/game.cpp b/src/api/game/game.cpp index bd33a6c4..dc206d3f 100644 --- a/src/api/game/game.cpp +++ b/src/api/game/game.cpp @@ -251,7 +251,7 @@ void Game::CacheArchives() { // file extension with the archive extension resolves to the same file. // Could use boost::iends_with, but it's less obvious here that the // test string is ASCII-only. - if (boost::iequals(it->path().extension().string(), archiveFileExtension)) { + if (boost::iequals(it->path().extension().u8string(), archiveFileExtension)) { cache_->CacheArchivePath(it->path()); } } diff --git a/src/api/helpers/git_helper.cpp b/src/api/helpers/git_helper.cpp index 01329e9e..71349078 100644 --- a/src/api/helpers/git_helper.cpp +++ b/src/api/helpers/git_helper.cpp @@ -195,7 +195,7 @@ void GitHelper::Clone(const std::filesystem::path& path, if (logger_) { logger_->trace("Target repo path not empty, cloning into temporary directory."); } - auto directory = "LOOT-" + path.filename().string() + "-" + + auto directory = "LOOT-" + path.filename().u8string() + "-" + boost::lexical_cast((boost::uuids::random_generator())()); repoPath = fs::temp_directory_path() / directory; diff --git a/src/api/helpers/version.cpp b/src/api/helpers/version.cpp index a16fdd5c..335b530d 100644 --- a/src/api/helpers/version.cpp +++ b/src/api/helpers/version.cpp @@ -28,8 +28,6 @@ #include #include -#include "api/helpers/windows_encoding_converters.h" - #ifdef _WIN32 #ifndef UNICODE #define UNICODE @@ -97,14 +95,14 @@ Version::Version(const std::string& ver) { Version::Version(const std::filesystem::path& file) { #ifdef _WIN32 DWORD dummy = 0; - DWORD size = GetFileVersionInfoSize(ToWinWide(file.string()).c_str(), &dummy); + DWORD size = GetFileVersionInfoSize(file.wstring().c_str(), &dummy); if (size > 0) { LPBYTE point = new BYTE[size]; UINT uLen; VS_FIXEDFILEINFO* info; - GetFileVersionInfo(ToWinWide(file.string()).c_str(), 0, size, point); + GetFileVersionInfo(file.wstring().c_str(), 0, size, point); VerQueryValue(point, L"\\", (LPVOID*)&info, &uLen); @@ -122,11 +120,11 @@ Version::Version(const std::filesystem::path& file) { #else // ensure filename has no quote characters in it to avoid command injection // attacks - if (std::string::npos != file.string().find('"')) { + if (std::string::npos != file.u8string().find('"')) { // command mostly borrowed from the gnome-exe-thumbnailer.sh script // wrestool is part of the icoutils package std::string cmd = - "wrestool --extract --raw --type=version \"" + file.string() + + "wrestool --extract --raw --type=version \"" + file.u8string() + "\" | tr '\\0, ' '\\t.\\0' | sed 's/\\t\\t/_/g' | tr -c -d '[:print:]' " "| sed -r 's/.*Version[^0-9]*([0-9]+(\\.[0-9]+)+).*/\\1/'"; diff --git a/src/api/helpers/windows_encoding_converters.h b/src/api/helpers/windows_encoding_converters.h deleted file mode 100644 index e25b059d..00000000 --- a/src/api/helpers/windows_encoding_converters.h +++ /dev/null @@ -1,80 +0,0 @@ -/* LOOT - - A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and - Fallout: New Vegas. - - Copyright (C) 2012-2016 WrinklyNinja - - This file is part of LOOT. - - LOOT is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public License - as published by the Free Software Foundation, either version 3 of - the License, or (at your option) any later version. - - LOOT is distributed in the hope that it will - be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with LOOT. If not, see - . - */ - -#ifndef LOOT_API_HELPERS_WINDOWS_ENCODING_CONVERTERS -#define LOOT_API_HELPERS_WINDOWS_ENCODING_CONVERTERS - -#ifdef _WIN32 - -#include - -#ifndef UNICODE -#define UNICODE -#endif -#ifndef _UNICODE -#define _UNICODE -#endif -#include "shlobj.h" -#include "shlwapi.h" -#include "windows.h" - -namespace loot { -/** - * Convert a UTF-8 std::string to a UTF-16 std::wstring. - * - * This isn't strictly part of the LOOT API, but is used within the API and the - * LOOT application, so is shared through the API. - * @param str - * A string encoded in UTF-8. - * @return A wstring encoded in UTF-16. - */ -inline std::wstring ToWinWide(const std::string& str) { - size_t len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), str.length(), 0, 0); - std::wstring wstr(len, 0); - MultiByteToWideChar(CP_UTF8, 0, str.c_str(), str.length(), &wstr[0], len); - return wstr; -} - -/** - * Convert a UTF-16 std::wstring to a UTF-8 std::string. - * - * This isn't strictly part of the LOOT API, but is used within the API and the - * LOOT application, so is shared through the API. - * @param wstr - * A wstring encoded in UTF-16. - * @return A string encoded in UTF-8. - */ -inline std::string FromWinWide(const std::wstring& wstr) { - size_t len = WideCharToMultiByte( - CP_UTF8, 0, wstr.c_str(), wstr.length(), NULL, 0, NULL, NULL); - std::string str(len, 0); - WideCharToMultiByte( - CP_UTF8, 0, wstr.c_str(), wstr.length(), &str[0], len, NULL, NULL); - return str; -} -} - -#endif - -#endif diff --git a/src/api/masterlist.cpp b/src/api/masterlist.cpp index 07a8876e..ac0b4cca 100644 --- a/src/api/masterlist.cpp +++ b/src/api/masterlist.cpp @@ -66,7 +66,7 @@ MasterlistInfo Masterlist::GetInfo(const std::filesystem::path& path, logger->trace("Diffing masterlist HEAD and working copy."); } info.is_modified = - GitHelper::IsFileDifferent(path.parent_path(), path.filename().string()); + GitHelper::IsFileDifferent(path.parent_path(), path.filename().u8string()); return info; } @@ -102,7 +102,7 @@ bool Masterlist::Update(const std::filesystem::path& path, GitHelper git; auto logger = getLogger(); fs::path repoPath = path.parent_path(); - string filename = path.filename().string(); + string filename = path.filename().u8string(); if (path.empty() || repoUrl.empty() || repoBranch.empty()) throw std::invalid_argument("Repository path, URL and branch must not be empty."); diff --git a/src/api/metadata/condition_evaluator.cpp b/src/api/metadata/condition_evaluator.cpp index 0c6c5748..d610d564 100644 --- a/src/api/metadata/condition_evaluator.cpp +++ b/src/api/metadata/condition_evaluator.cpp @@ -29,6 +29,8 @@ #include "api/metadata/condition_grammar.h" #include "loot/exception/condition_syntax_error.h" +using std::filesystem::u8path; + namespace loot { ConditionEvaluator::ConditionEvaluator() : gameType_(GameType::tes4), @@ -163,10 +165,10 @@ bool ConditionEvaluator::fileExists(const std::string& filePath) const { // Not a loaded plugin, check the filesystem. if (hasPluginFileExtension(filePath, gameType_)) - return std::filesystem::exists(dataPath_ / filePath) || - std::filesystem::exists(dataPath_ / (filePath + ".ghost")); + return std::filesystem::exists(dataPath_ / u8path(filePath)) || + std::filesystem::exists(dataPath_ / u8path(filePath + ".ghost")); else - return std::filesystem::exists(dataPath_ / filePath); + return std::filesystem::exists(dataPath_ / u8path(filePath)); } bool ConditionEvaluator::regexMatchExists( @@ -295,7 +297,7 @@ std::filesystem::path ConditionEvaluator::getRegexParentPath( if (pos == std::string::npos) return std::filesystem::path(); - return std::filesystem::path(regexString.substr(0, pos)); + return u8path(regexString.substr(0, pos)); } std::string ConditionEvaluator::getRegexFilename( @@ -359,7 +361,7 @@ bool ConditionEvaluator::isRegexMatchInDataDirectory( std::filesystem::directory_iterator(dataPath_ / pathRegex.first), std::filesystem::directory_iterator(), [&](const std::filesystem::directory_entry& entry) { - const std::string filename = entry.path().filename().string(); + const std::string filename = entry.path().filename().u8string(); return std::regex_match(filename, pathRegex.second) && condition(filename); }); @@ -425,7 +427,7 @@ Version ConditionEvaluator::getVersion(const std::string& filePath) const { .GetVersion() .value_or("")); - return Version(dataPath_ / filePath); + return Version(dataPath_ / u8path(filePath)); } } bool ConditionEvaluator::shouldParseOnly() const { @@ -453,12 +455,12 @@ uint32_t ConditionEvaluator::getCrc(const std::string & file) const { // Otherwise calculate it from the file. if (crc == 0) { - if (std::filesystem::exists(dataPath_ / file)) { - crc = GetCrc32(dataPath_ / file); + if (std::filesystem::exists(dataPath_ / u8path(file))) { + crc = GetCrc32(dataPath_ / u8path(file)); } else if (hasPluginFileExtension(file, gameType_) && - std::filesystem::exists(dataPath_ / (file + ".ghost"))) { - crc = GetCrc32(dataPath_ / (file + ".ghost")); + std::filesystem::exists(dataPath_ / u8path(file + ".ghost"))) { + crc = GetCrc32(dataPath_ / u8path(file + ".ghost")); } } diff --git a/src/api/plugin.cpp b/src/api/plugin.cpp index c3e78260..8cc5b0f0 100644 --- a/src/api/plugin.cpp +++ b/src/api/plugin.cpp @@ -55,7 +55,7 @@ Plugin::Plugin(const GameType gameType, auto logger = getLogger(); try { - std::filesystem::path filepath = dataPath / name_; + std::filesystem::path filepath = dataPath / std::filesystem::u8path(name_); // In case the plugin is ghosted. if (!std::filesystem::exists(filepath)) { @@ -239,7 +239,7 @@ bool Plugin::IsValid(const std::string& filename, return false; bool isValid; - auto path = dataPath / filename; + auto path = dataPath / std::filesystem::u8path(filename); int ret = esp_plugin_is_valid( GetEspluginGameId(gameType), path.u8string().c_str(), true, &isValid); @@ -255,7 +255,7 @@ bool Plugin::IsValid(const std::string& filename, uintmax_t Plugin::GetFileSize(const std::string& filename, const std::filesystem::path& dataPath) { - std::filesystem::path realPath = dataPath / filename; + std::filesystem::path realPath = dataPath / std::filesystem::u8path(filename); if (!std::filesystem::exists(realPath)) realPath += ".ghost"; @@ -323,16 +323,15 @@ bool Plugin::LoadsArchive(const std::string& pluginName, if (gameType == GameType::tes5) { // Skyrim plugins only load BSAs that exactly match their basename. - return std::filesystem::exists( - dataPath / - (pluginName.substr(0, pluginName.length() - 4) + archiveExtension)); + auto filename = pluginName.substr(0, pluginName.length() - 4) + archiveExtension; + return std::filesystem::exists(dataPath / std::filesystem::u8path(filename)); } else if (gameType != GameType::tes4 || boost::iends_with(pluginName, ".esp")) { // Oblivion .esp files and FO3, FNV, FO4 plugins can load archives which // begin with the plugin basename. string basename = pluginName.substr(0, pluginName.length() - 4); for (const auto& archivePath : gameCache->GetArchivePaths()) { - if (boost::istarts_with(archivePath.filename().string(), basename)) { + if (boost::istarts_with(archivePath.filename().u8string(), basename)) { return true; } } diff --git a/src/tests/api/internals/helpers/version_test.h b/src/tests/api/internals/helpers/version_test.h index c929ce2f..7bb37125 100644 --- a/src/tests/api/internals/helpers/version_test.h +++ b/src/tests/api/internals/helpers/version_test.h @@ -33,6 +33,32 @@ along with LOOT. If not, see namespace loot { namespace test { #ifdef _WIN32 +class VersionTest : public CommonGameTestFixture { +protected: + VersionTest() : nonAsciiDll(u8"loot_ap\u00ED.dll") {} + + void SetUp() { + CommonGameTestFixture::SetUp(); + + ASSERT_NO_THROW(std::filesystem::copy_file("loot_api.dll", + dataPath / std::filesystem::u8path(nonAsciiDll))); + } + + const std::string nonAsciiDll; +}; + +// Pass an empty first argument, as it's a prefix for the test instantation, +// but we only have the one so no prefix is necessary. +// Just test with one game because if it works for one it will work for them +// all. +INSTANTIATE_TEST_CASE_P(, VersionTest, ::testing::Values(GameType::tes5)); + +TEST_P(VersionTest, shouldExtractVersionFromNonAsciiDll) { + Version version(dataPath / std::filesystem::u8path(nonAsciiDll)); + std::string expected(LootVersion::string() + ".0"); + EXPECT_EQ(expected, version.AsString()); +} + TEST(Version, shouldExtractVersionFromApiDll) { // Use the API DLL built. Version version(std::filesystem::path("loot_api.dll")); @@ -40,6 +66,7 @@ TEST(Version, shouldExtractVersionFromApiDll) { EXPECT_EQ(expected, version.AsString()); } #endif + TEST(Version, defaultConstructorShouldSetEmptyVersionString) { EXPECT_EQ("", Version().AsString()); } diff --git a/src/tests/api/internals/masterlist_test.h b/src/tests/api/internals/masterlist_test.h index e4e061df..33599f27 100644 --- a/src/tests/api/internals/masterlist_test.h +++ b/src/tests/api/internals/masterlist_test.h @@ -37,7 +37,8 @@ protected: repoBranch("master"), oldBranch("old-branch"), repoUrl("https://github.com/loot/testing-metadata.git"), - masterlistPath(localPath / "masterlist.yaml") {} + masterlistPath(localPath / "masterlist.yaml"), + nonAsciiMasterlistPath(localPath / std::filesystem::u8path(u8"masterl\u00EDst.yaml")) {} void SetUp() { CommonGameTestFixture::SetUp(); @@ -48,14 +49,23 @@ protected: ASSERT_TRUE(std::filesystem::exists(metadataFilesPath / "masterlist.yaml")); ASSERT_FALSE(std::filesystem::exists(masterlistPath)); + ASSERT_FALSE(std::filesystem::exists(nonAsciiMasterlistPath)); ASSERT_FALSE(std::filesystem::exists(localPath / ".git")); } + void runRepoCommand(const std::string& command) { + auto testPath = std::filesystem::current_path(); + std::filesystem::current_path(masterlistPath.parent_path()); + system(command.c_str()); + std::filesystem::current_path(testPath); + } + const std::string repoUrl; const std::string repoBranch; const std::string oldBranch; const std::filesystem::path masterlistPath; + const std::filesystem::path nonAsciiMasterlistPath; }; // Pass an empty first argument, as it's a prefix for the test instantation, @@ -125,16 +135,23 @@ TEST_P(MasterlistTest, updateShouldReturnFalseIfAnUpToDateMasterlistExists) { EXPECT_FALSE(masterlist.Update(masterlistPath, repoUrl, repoBranch)); } +TEST_P(MasterlistTest, updateShouldReturnFalseIfAnUpToDateMasterlistWithANonAsciiFilenameExists) { + Masterlist masterlist; + + EXPECT_TRUE(masterlist.Update(nonAsciiMasterlistPath, repoUrl, repoBranch)); + EXPECT_TRUE(std::filesystem::exists(nonAsciiMasterlistPath)); + + EXPECT_FALSE(masterlist.Update(nonAsciiMasterlistPath, repoUrl, repoBranch)); + EXPECT_TRUE(std::filesystem::exists(nonAsciiMasterlistPath)); +} + TEST_P(MasterlistTest, updateShouldDiscardLocalHistoryIfRemoteHistoryIsDifferent) { Masterlist masterlist; ASSERT_TRUE(masterlist.Update(masterlistPath, repoUrl, repoBranch)); - auto testPath = std::filesystem::current_path(); - std::filesystem::current_path(masterlistPath.parent_path()); - system("git config commit.gpgsign false"); - system("git commit --amend -m \"changing local history\""); - std::filesystem::current_path(testPath); + runRepoCommand("git config commit.gpgsign false"); + runRepoCommand("git commit --amend -m \"changing local history\""); EXPECT_TRUE(masterlist.Update(masterlistPath, repoUrl, repoBranch)); } @@ -192,6 +209,24 @@ TEST_P( EXPECT_TRUE(info.is_modified); } +TEST_P(MasterlistTest, + getInfoShouldDetectWhenAMasterlistWithANonAsciiFilenameHasBeenEdited) { + Masterlist masterlist; + ASSERT_TRUE(masterlist.Update(masterlistPath, repoUrl, repoBranch)); + + auto nonAsciiPath = masterlistPath.parent_path() / std::filesystem::u8path(u8"non\u00C1scii.yaml"); + std::filesystem::copy_file(masterlistPath, nonAsciiPath); + + runRepoCommand("git add " + nonAsciiPath.string()); + std::ofstream out(nonAsciiPath); + out.close(); + + MasterlistInfo info = masterlist.GetInfo(nonAsciiPath, false); + EXPECT_EQ(40, info.revision_id.length()); + EXPECT_EQ(10, info.revision_date.length()); + EXPECT_TRUE(info.is_modified); +} + TEST_P(MasterlistTest, isLatestShouldThrowIfTheGivenPathDoesNotBelongToAGitRepository) { ASSERT_NO_THROW(std::filesystem::copy(metadataFilesPath / "masterlist.yaml", diff --git a/src/tests/api/internals/metadata/condition_evaluator_test.h b/src/tests/api/internals/metadata/condition_evaluator_test.h index a4cbc088..ca7e4676 100644 --- a/src/tests/api/internals/metadata/condition_evaluator_test.h +++ b/src/tests/api/internals/metadata/condition_evaluator_test.h @@ -42,9 +42,29 @@ protected: evaluator_(game_.Type(), game_.DataPath(), game_.GetCache(), - game_.GetLoadOrderHandler()) {} + game_.GetLoadOrderHandler()), + nonAsciiEsm(u8"non\u00C1scii.esm"), + nonAsciiNestedFile(u8"non\u00C1scii/test.txt") { + // Make sure the plugin with a non-ASCII filename exists. + std::filesystem::copy_file(dataPath / blankEsm, + dataPath / std::filesystem::u8path(nonAsciiEsm)); + + auto nonAsciiPath = dataPath / std::filesystem::u8path(nonAsciiNestedFile); + std::filesystem::create_directory(nonAsciiPath.parent_path()); + + std::ofstream out(nonAsciiPath); + out.close(); + } + + std::string IntToHexString(const uint32_t value) { + std::stringstream stream; + stream << std::hex << value; + return stream.str(); + } const std::vector info_; + const std::string nonAsciiEsm; + const std::string nonAsciiNestedFile; Game game_; ConditionEvaluator evaluator_; @@ -75,6 +95,36 @@ TEST_P(ConditionEvaluatorTest, EXPECT_TRUE(evaluator_.evaluate("file(\"" + blankEsm + "\")")); } +TEST_P(ConditionEvaluatorTest, + evaluateFileConditionShouldReturnTrueForANonAsciiFileThatExists) { + EXPECT_TRUE(evaluator_.evaluate("file(\"" + nonAsciiEsm + "\")")); +} + +TEST_P(ConditionEvaluatorTest, + evaluateChecksumConditionShouldBeAbleToGetTheCrcOfANonAsciiFile) { + std::string condition("checksum(\"" + nonAsciiEsm + "\", " + + IntToHexString(blankEsmCrc) + ")"); + EXPECT_TRUE(evaluator_.evaluate(condition)); +} + +TEST_P(ConditionEvaluatorTest, + evaluateVersionConditionShouldBeAbleToGetTheVersionOfANonAsciiFile) { + std::string condition("version(\"" + nonAsciiEsm + "\", \"5.0\", ==)"); + EXPECT_TRUE(evaluator_.evaluate(condition)); +} + +TEST_P(ConditionEvaluatorTest, + evaluateRegexFileConditionShouldReturnTrueForANonAsciiFileThatExists) { + std::string condition(u8"file(\"non\u00C1scii.*\\.esm\")"); + EXPECT_TRUE(evaluator_.evaluate(condition)); +} + +TEST_P(ConditionEvaluatorTest, + evaluateRegexFileConditionShouldReturnTrueForANonAsciiNestedFileThatExists) { + std::string condition(u8"file(\"non\u00C1scii/.+\\.txt\")"); + EXPECT_TRUE(evaluator_.evaluate(condition)); +} + TEST_P(ConditionEvaluatorTest, evaluateShouldReturnFalseForAConditionThatIsFalse) { EXPECT_FALSE(evaluator_.evaluate("file(\"" + missingEsp + "\")")); @@ -88,6 +138,14 @@ TEST_P( EXPECT_TRUE(evaluator_.evaluate(dirtyInfo, blankEsm)); } +TEST_P( + ConditionEvaluatorTest, + evaluateShouldBeTrueIfTheCrcInTheCleaningDataMatchesTheCrcOfANonAsciiPlugin) { + PluginCleaningData dirtyInfo(blankEsmCrc, "cleaner", info_, 2, 10, 30); + + EXPECT_TRUE(evaluator_.evaluate(dirtyInfo, nonAsciiEsm)); +} + TEST_P( ConditionEvaluatorTest, evaluateShouldBeFalseIfTheCrcInThePluginCleaningDataGivenDoesNotMatchTheRealPluginCrc) { diff --git a/src/tests/api/internals/plugin_test.h b/src/tests/api/internals/plugin_test.h index cc064fa4..e9304d3a 100644 --- a/src/tests/api/internals/plugin_test.h +++ b/src/tests/api/internals/plugin_test.h @@ -37,6 +37,7 @@ protected: PluginTest() : emptyFile("EmptyFile.esm"), lowercaseBlankEsp("blank.esp"), + nonAsciiEsp(u8"non\u00C1scii.esp"), game_(GetParam(), dataPath.parent_path(), localPath), blankArchive("Blank" + GetArchiveFileExtension(game_.Type())), blankSuffixArchive("Blank - Different - suffix" + @@ -57,6 +58,10 @@ protected: dataPath / lowercaseBlankEsp)); #endif + // Make sure the plugin with a non-ASCII filename exists. + ASSERT_NO_THROW(std::filesystem::copy_file(dataPath / blankEsp, + dataPath / std::filesystem::u8path(nonAsciiEsp))); + if (GetParam() != GameType::fo4 && GetParam() != GameType::tes5se) { ASSERT_NO_THROW( std::filesystem::copy(dataPath / blankEsp, dataPath / blankEsl)); @@ -68,14 +73,34 @@ protected: out.open(dataPath / blankSuffixArchive); out.close(); + auto nonAsciiArchivePath = dataPath / std::filesystem::u8path(u8"non\u00C1scii" + GetArchiveFileExtension(game_.Type())); + out.open(nonAsciiArchivePath); + out.close(); + game_.GetCache()->CacheArchivePath(dataPath / blankArchive); game_.GetCache()->CacheArchivePath(dataPath / blankSuffixArchive); + game_.GetCache()->CacheArchivePath(dataPath / nonAsciiArchivePath); + } + + uintmax_t getGhostedPluginFileSize() { + if (GetParam() == GameType::tes4) + return 390; + else + return 1358; + } + + uintmax_t getNonAsciiEspFileSize() { + if (GetParam() == GameType::tes4) + return 55; + else + return 1019; } Game game_; const std::string emptyFile; const std::string lowercaseBlankEsp; + const std::string nonAsciiEsp; const std::string blankArchive; const std::string blankSuffixArchive; @@ -117,6 +142,18 @@ INSTANTIATE_TEST_CASE_P(, GameType::fo4, GameType::tes5se)); +TEST_P(PluginTest, loadingShouldHandleNonAsciiFilenamesCorrectly) { + Plugin plugin(game_.Type(), + game_.DataPath(), + game_.GetCache(), + game_.GetLoadOrderHandler(), + nonAsciiEsp, + true); + + EXPECT_EQ(nonAsciiEsp, plugin.GetName()); + EXPECT_EQ(nonAsciiEsp, plugin.GetName()); +} + TEST_P(PluginTest, loadingHeaderOnlyShouldReadHeaderData) { Plugin plugin(game_.Type(), game_.DataPath(), @@ -259,16 +296,28 @@ TEST_P( TEST_P( PluginTest, - loadsArchiveForAnArchiveThatExactlyMatchesAnEspFileBasenameShouldReturnTrue) { + loadsArchiveForAnArchiveThatExactlyMatchesANonAsciiEspFileBasenameShouldReturnTrue) { EXPECT_TRUE(Plugin(game_.Type(), game_.DataPath(), game_.GetCache(), game_.GetLoadOrderHandler(), - blankEsp, + nonAsciiEsp, true) .LoadsArchive()); } +TEST_P( + PluginTest, + loadsArchiveForAnArchiveThatExactlyMatchesAnEspFileBasenameShouldReturnTrue) { + EXPECT_TRUE(Plugin(game_.Type(), + game_.DataPath(), + game_.GetCache(), + game_.GetLoadOrderHandler(), + blankEsp, + true) + .LoadsArchive()); +} + TEST_P( PluginTest, loadsArchiveForAnArchiveWithAFilenameWhichStartsWithTheEsmFileBasenameShouldReturnTrueForAllGamesExceptOblivionAndSkyrim) { @@ -318,6 +367,10 @@ TEST_P(PluginTest, isValidShouldReturnTrueForAValidPlugin) { EXPECT_TRUE(Plugin::IsValid(blankEsm, game_.Type(), game_.DataPath())); } +TEST_P(PluginTest, isValidShouldReturnTrueForAValidNonAsciiPlugin) { + EXPECT_TRUE(Plugin::IsValid(nonAsciiEsp, game_.Type(), game_.DataPath())); +} + TEST_P(PluginTest, isValidShouldReturnFalseForANonPluginFile) { EXPECT_FALSE(Plugin::IsValid(nonPluginFile, game_.Type(), game_.DataPath())); } @@ -326,6 +379,18 @@ TEST_P(PluginTest, isValidShouldReturnFalseForAnEmptyFile) { EXPECT_FALSE(Plugin::IsValid(emptyFile, game_.Type(), game_.DataPath())); } +TEST_P(PluginTest, getFileSizeShouldThrowForAMissingPlugin) { + EXPECT_THROW(Plugin::GetFileSize(missingEsp, game_.DataPath()), std::filesystem::filesystem_error); +} + +TEST_P(PluginTest, getFileSizeShouldReturnCorrectValueForAPlugin) { + EXPECT_EQ(getNonAsciiEspFileSize(), Plugin::GetFileSize(nonAsciiEsp, game_.DataPath())); +} + +TEST_P(PluginTest, getFileSizeShouldReturnCorrectValueForAGhostedPlugin) { + EXPECT_EQ(getGhostedPluginFileSize(), Plugin::GetFileSize(blankMasterDependentEsm, game_.DataPath())); +} + TEST_P(PluginTest, isActiveShouldReturnTrueForAPluginThatIsActive) { EXPECT_TRUE(Plugin(game_.Type(), game_.DataPath(),