From 1b53ae4ff4efe4f4def650f547e9f0860f2f50d9 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Wed, 29 Aug 2018 18:23:44 +0100 Subject: [PATCH] Fix case-insensitive detection of archives starting with a non-ASCII string --- src/api/game/game.cpp | 2 +- src/api/plugin.cpp | 39 +++++++++++++++++++++------ src/api/plugin.h | 4 +++ src/tests/api/internals/plugin_test.h | 33 +++++++++++++++++++++-- 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/api/game/game.cpp b/src/api/game/game.cpp index 5691e5b0..b6e31327 100644 --- a/src/api/game/game.cpp +++ b/src/api/game/game.cpp @@ -253,7 +253,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().u8string(), archiveFileExtension)) { + if (loot::equivalent(it->path(), replaceExtension(it->path(), archiveFileExtension))) { cache_->CacheArchivePath(it->path()); } } diff --git a/src/api/plugin.cpp b/src/api/plugin.cpp index d280ef81..56063975 100644 --- a/src/api/plugin.cpp +++ b/src/api/plugin.cpp @@ -264,7 +264,6 @@ uintmax_t Plugin::GetFileSize(std::filesystem::path pluginPath) { bool Plugin::operator<(const Plugin& rhs) const { return boost::ilexicographical_compare(name_, rhs.name_); - ; } bool Plugin::IsActive() const { return isActive_; } @@ -314,24 +313,48 @@ std::string GetArchiveFileExtension(const GameType gameType) { return ".bsa"; } +std::filesystem::path replaceExtension(std::filesystem::path path, const std::string& newExtension) { + return path.replace_extension(std::filesystem::u8path(newExtension)); +} + +bool equivalent(const std::filesystem::path& path1, const std::filesystem::path& path2) { + try { + return std::filesystem::equivalent(path1, path2); + } catch (std::filesystem::filesystem_error) { + // One of the paths checked for equivalence doesn't exist, + // so they can't be equivalent. + return false; + } +} + +// Get whether the plugin loads an archive (BSA/BA2) or not. bool Plugin::LoadsArchive(const GameType gameType, const std::shared_ptr gameCache, const std::filesystem::path& pluginPath) { - // Get whether the plugin loads an archive (BSA/BA2) or not. const string archiveExtension = GetArchiveFileExtension(gameType); - auto pluginName = pluginPath.filename().u8string(); if (gameType == GameType::tes5) { // Skyrim plugins only load BSAs that exactly match their basename. - auto filename = pluginName.substr(0, pluginName.length() - 4) + archiveExtension; - return std::filesystem::exists(pluginPath.parent_path() / std::filesystem::u8path(filename)); + return std::filesystem::exists(replaceExtension(pluginPath, archiveExtension)); } else if (gameType != GameType::tes4 || - boost::iends_with(pluginName, ".esp")) { + boost::iends_with(pluginPath.filename().u8string(), ".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); + + auto basenameLength = pluginPath.stem().native().length(); + auto pluginExtension = pluginPath.extension().native(); + for (const auto& archivePath : gameCache->GetArchivePaths()) { - if (boost::istarts_with(archivePath.filename().u8string(), basename)) { + // Need to check if it starts with the given plugin's basename, + // but case insensitively. This is hard to do accurately, so + // instead check if the plugin with the same length basename and + // and the given plugin's file extension is equivalent. + auto bsaPluginFilename = + archivePath.filename().native().substr(0, basenameLength) + + pluginExtension; + auto bsaPluginPath = + pluginPath.parent_path() / bsaPluginFilename; + if (loot::equivalent(pluginPath, bsaPluginPath)) { return true; } } diff --git a/src/api/plugin.h b/src/api/plugin.h index b6ef1967..9c82fb28 100644 --- a/src/api/plugin.h +++ b/src/api/plugin.h @@ -103,6 +103,10 @@ private: std::string GetArchiveFileExtension(const GameType gameType); bool hasPluginFileExtension(const std::string& filename, GameType gameType); + +bool equivalent(const std::filesystem::path& path1, const std::filesystem::path& path2); + +std::filesystem::path replaceExtension(std::filesystem::path path, const std::string& newExtension); } #endif diff --git a/src/tests/api/internals/plugin_test.h b/src/tests/api/internals/plugin_test.h index 315dd2aa..17b2325a 100644 --- a/src/tests/api/internals/plugin_test.h +++ b/src/tests/api/internals/plugin_test.h @@ -38,6 +38,7 @@ protected: emptyFile("EmptyFile.esm"), lowercaseBlankEsp("blank.esp"), nonAsciiEsp(u8"non\u00C1scii.esp"), + otherNonAsciiEsp(u8"other non\u00C1scii.esp"), game_(GetParam(), dataPath.parent_path(), localPath), blankArchive("Blank" + GetArchiveFileExtension(game_.Type())), blankSuffixArchive("Blank - Different - suffix" + @@ -58,9 +59,11 @@ protected: dataPath / lowercaseBlankEsp)); #endif - // Make sure the plugin with a non-ASCII filename exists. + // Make sure the plugins with non-ASCII filenames exists. ASSERT_NO_THROW(std::filesystem::copy_file(dataPath / blankEsp, dataPath / std::filesystem::u8path(nonAsciiEsp))); + ASSERT_NO_THROW(std::filesystem::copy_file(dataPath / blankEsp, + dataPath / std::filesystem::u8path(otherNonAsciiEsp))); if (GetParam() != GameType::fo4 && GetParam() != GameType::tes5se) { ASSERT_NO_THROW( @@ -73,13 +76,18 @@ protected: out.open(dataPath / blankSuffixArchive); out.close(); - auto nonAsciiArchivePath = dataPath / std::filesystem::u8path(u8"non\u00C1scii" + GetArchiveFileExtension(game_.Type())); + auto nonAsciiArchivePath = dataPath / std::filesystem::u8path(u8"non\u00E1scii" + GetArchiveFileExtension(game_.Type())); out.open(nonAsciiArchivePath); out.close(); + auto nonAsciiPrefixArchivePath = dataPath / std::filesystem::u8path(u8"other non\u00E1scii2 - suffix" + GetArchiveFileExtension(game_.Type())); + out.open(nonAsciiPrefixArchivePath); + out.close(); + game_.GetCache()->CacheArchivePath(dataPath / blankArchive); game_.GetCache()->CacheArchivePath(dataPath / blankSuffixArchive); game_.GetCache()->CacheArchivePath(dataPath / nonAsciiArchivePath); + game_.GetCache()->CacheArchivePath(dataPath / nonAsciiPrefixArchivePath); } uintmax_t getGhostedPluginFileSize() { @@ -101,6 +109,7 @@ protected: const std::string emptyFile; const std::string lowercaseBlankEsp; const std::string nonAsciiEsp; + const std::string otherNonAsciiEsp; const std::string blankArchive; const std::string blankSuffixArchive; @@ -281,6 +290,7 @@ TEST_P( EXPECT_TRUE(loadsArchive); } +#ifdef _WIN32 TEST_P( PluginTest, loadsArchiveForAnArchiveThatExactlyMatchesANonAsciiEspFileBasenameShouldReturnTrue) { @@ -291,6 +301,7 @@ TEST_P( true) .LoadsArchive()); } +#endif TEST_P( PluginTest, @@ -335,6 +346,24 @@ TEST_P( EXPECT_TRUE(loadsArchive); } +#ifdef _WIN32 +TEST_P( + PluginTest, + loadsArchiveForAnArchiveWithAFilenameWhichStartsWithTheNonAsciiEspFileBasenameShouldReturnTrueForAllGamesExceptSkyrim) { + bool loadsArchive = Plugin(game_.Type(), + game_.GetCache(), + game_.GetLoadOrderHandler(), + game_.DataPath() / std::filesystem::u8path(otherNonAsciiEsp), + true) + .LoadsArchive(); + + if (GetParam() == GameType::tes5) + EXPECT_FALSE(loadsArchive); + else + EXPECT_TRUE(loadsArchive); +} +#endif + TEST_P(PluginTest, loadsArchiveShouldReturnFalseForAPluginThatDoesNotLoadAnArchive) { EXPECT_FALSE(Plugin(game_.Type(),