diff --git a/FEXCore/Source/CMakeLists.txt b/FEXCore/Source/CMakeLists.txt index 633b397e9..850a96e30 100644 --- a/FEXCore/Source/CMakeLists.txt +++ b/FEXCore/Source/CMakeLists.txt @@ -6,7 +6,8 @@ set(FEXCORE_BASE_SRCS Utils/FileLoading.cpp Utils/ForcedAssert.cpp Utils/LogManager.cpp - Utils/SpinWaitLock.cpp) + Utils/SpinWaitLock.cpp + Utils/WildcardMatcher.cpp) if (NOT MINGW) list(APPEND FEXCORE_BASE_SRCS diff --git a/FEXCore/Source/Utils/WildcardMatcher.cpp b/FEXCore/Source/Utils/WildcardMatcher.cpp new file mode 100644 index 000000000..70dad5632 --- /dev/null +++ b/FEXCore/Source/Utils/WildcardMatcher.cpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +#include + +namespace FEXCore::Utils::Wildcard { +static bool matchHelper(std::string_view pattern, std::string_view text, size_t p_idx, size_t t_idx) { + if (p_idx == pattern.size()) { + // Pattern exhausted + return (t_idx == text.size()); + } else if (pattern[p_idx] == '*') { + // Wildcard: Try matching zero characters, or one or more characters + return matchHelper(pattern, text, p_idx + 1, t_idx) || (t_idx < text.size() && matchHelper(pattern, text, p_idx, t_idx + 1)); + } else { + // Match normally + return (t_idx < text.size() && pattern[p_idx] == text[t_idx] && matchHelper(pattern, text, p_idx + 1, t_idx + 1)); + } +} + +bool Matches(std::string_view pattern, std::string_view text) { + return matchHelper(pattern, text, 0, 0); +} + +} // namespace FEXCore::Utils::Wildcard diff --git a/FEXCore/include/FEXCore/Utils/WildcardMatcher.h b/FEXCore/include/FEXCore/Utils/WildcardMatcher.h new file mode 100644 index 000000000..6bee8d5a0 --- /dev/null +++ b/FEXCore/include/FEXCore/Utils/WildcardMatcher.h @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +#pragma once +#include + +namespace FEXCore::Utils::Wildcard { +bool Matches(std::string_view pattern, std::string_view text); +} // namespace FEXCore::Utils::Wildcard diff --git a/Source/Common/Config.cpp b/Source/Common/Config.cpp index 62fa61f94..62bea17cd 100644 --- a/Source/Common/Config.cpp +++ b/Source/Common/Config.cpp @@ -7,11 +7,12 @@ #include #include #include +#include #include #include +#include #include #include - #include #include #include @@ -28,7 +29,8 @@ namespace FEX::Config { namespace JSON { - static void LoadJSonConfig(const fextl::string& Config, std::function Func) { + static void LoadJSonConfig(const fextl::string& Config, std::optional AppName, + std::function Func) { fextl::vector Data; if (!FEXCore::FileLoading::LoadFile(Data, Config)) { return; @@ -48,21 +50,35 @@ namespace JSON { return; } - for (const json_t* ConfigItem = json_getChild(ConfigList); ConfigItem != nullptr; ConfigItem = json_getSibling(ConfigItem)) { - const char* ConfigName = json_getName(ConfigItem); - const char* ConfigString = json_getValue(ConfigItem); + fextl::vector ConfigBlocks; + ConfigBlocks.push_back(ConfigList); - if (!ConfigName) { - LogMan::Msg::EFmt("JSON file '{}': Couldn't get config name for an item", Config); - return; + if (AppName) { + const json_t* OverrideList = json_getProperty(json, "AppOverrides"); + if (OverrideList) { + for (const json_t* Item = json_getChild(OverrideList); Item != nullptr; Item = json_getSibling(Item)) { + const char* AppPattern = json_getName(Item); + + // Find the first match, then break + if (FEXCore::Utils::Wildcard::Matches(AppPattern, *AppName)) { + ConfigBlocks.push_back(Item); + break; + } + } } + } - if (!ConfigString) { - LogMan::Msg::EFmt("JSON file '{}': Couldn't get value for config item '{}'", Config, ConfigName); - return; + for (auto ConfigBlock : ConfigBlocks) { + for (const json_t* ConfigItem = json_getChild(ConfigBlock); ConfigItem != nullptr; ConfigItem = json_getSibling(ConfigItem)) { + const char* ConfigName = json_getName(ConfigItem); + const char* ConfigString = json_getValue(ConfigItem); + + if (!ConfigString) { + LogMan::Msg::EFmt("JSON file '{}': Couldn't get value for config item '{}'", Config, ConfigName); + return; + } + Func(ConfigName, ConfigString); } - - Func(ConfigName, ConfigString); } } } // namespace JSON @@ -167,22 +183,24 @@ protected: class MainLoader final : public OptionMapper { public: - explicit MainLoader(FEXCore::Config::LayerType Type); - explicit MainLoader(fextl::string ConfigFile); + explicit MainLoader(FEXCore::Config::LayerType Type, std::optional AppName = std::nullopt); + explicit MainLoader(fextl::string ConfigFile, std::optional AppName = std::nullopt); explicit MainLoader(FEXCore::Config::LayerType Type, std::string_view ConfigFile); void Load() override; private: + std::optional AppName; fextl::string Config; }; class AppLoader final : public OptionMapper { public: - explicit AppLoader(const fextl::string& Filename, FEXCore::Config::LayerType Type); + explicit AppLoader(const fextl::string& AppName, FEXCore::Config::LayerType Type); void Load(); private: + const fextl::string AppName; fextl::string Config; }; @@ -221,12 +239,14 @@ void OptionMapper::MapNameToOption(const char* ConfigName, const char* ConfigStr #include } -MainLoader::MainLoader(FEXCore::Config::LayerType Type) +MainLoader::MainLoader(FEXCore::Config::LayerType Type, std::optional AppName) : OptionMapper(Type) + , AppName {AppName} , Config {FEXCore::Config::GetConfigFileLocation(Type == FEXCore::Config::LayerType::LAYER_GLOBAL_MAIN)} {} -MainLoader::MainLoader(fextl::string ConfigFile) +MainLoader::MainLoader(fextl::string ConfigFile, std::optional AppName) : OptionMapper(FEXCore::Config::LayerType::LAYER_MAIN) + , AppName {AppName} , Config {std::move(ConfigFile)} {} @@ -236,13 +256,14 @@ MainLoader::MainLoader(FEXCore::Config::LayerType Type, std::string_view ConfigF void MainLoader::Load() { SetCurrentConfigFile(Config); - JSON::LoadJSonConfig(Config, [this](const char* Name, const char* ConfigString) { MapNameToOption(Name, ConfigString); }); + JSON::LoadJSonConfig(Config, AppName, [this](const char* Name, const char* ConfigString) { MapNameToOption(Name, ConfigString); }); } -AppLoader::AppLoader(const fextl::string& Filename, FEXCore::Config::LayerType Type) - : OptionMapper(Type) { +AppLoader::AppLoader(const fextl::string& AppName, FEXCore::Config::LayerType Type) + : OptionMapper(Type) + , AppName {AppName} { const bool Global = Type == FEXCore::Config::LayerType::LAYER_GLOBAL_STEAM_APP || Type == FEXCore::Config::LayerType::LAYER_GLOBAL_APP; - Config = FEXCore::Config::GetApplicationConfig(Filename, Global); + Config = FEXCore::Config::GetApplicationConfig(AppName, Global); // Immediately load so we can reload the meta layer Load(); @@ -250,7 +271,7 @@ AppLoader::AppLoader(const fextl::string& Filename, FEXCore::Config::LayerType T void AppLoader::Load() { SetCurrentConfigFile(Config); - JSON::LoadJSonConfig(Config, [this](const char* Name, const char* ConfigString) { MapNameToOption(Name, ConfigString); }); + JSON::LoadJSonConfig(Config, AppName, [this](const char* Name, const char* ConfigString) { MapNameToOption(Name, ConfigString); }); } EnvLoader::EnvLoader(char* const _envp[]) @@ -320,11 +341,11 @@ fextl::unique_ptr CreateGlobalMainLayer() { return fextl::make_unique(FEXCore::Config::LayerType::LAYER_GLOBAL_MAIN); } -fextl::unique_ptr CreateMainLayer(const fextl::string* File) { +fextl::unique_ptr CreateMainLayer(const fextl::string* File, std::optional AppName) { if (File) { - return fextl::make_unique(*File); + return fextl::make_unique(*File, std::move(AppName)); } else { - return fextl::make_unique(FEXCore::Config::LayerType::LAYER_MAIN); + return fextl::make_unique(FEXCore::Config::LayerType::LAYER_MAIN, std::move(AppName)); } } @@ -461,7 +482,7 @@ void LoadConfig(fextl::string ProgramName, char** const envp, const PortableInfo if (!IsPortable) { FEXCore::Config::AddLayer(CreateGlobalMainLayer()); } - FEXCore::Config::AddLayer(CreateMainLayer()); + FEXCore::Config::AddLayer(CreateMainLayer(nullptr, ProgramName.empty() ? std::nullopt : std::optional {ProgramName})); if (!ProgramName.empty()) { if (!IsPortable) { diff --git a/Source/Common/Config.h b/Source/Common/Config.h index 3ba689315..f0ee1f35e 100644 --- a/Source/Common/Config.h +++ b/Source/Common/Config.h @@ -81,7 +81,7 @@ fextl::unique_ptr CreateGlobalMainLayer(); * * @return unique_ptr for that layer */ -fextl::unique_ptr CreateMainLayer(const fextl::string* File = nullptr); +fextl::unique_ptr CreateMainLayer(const fextl::string* File = nullptr, std::optional AppName = std::nullopt); fextl::unique_ptr CreateUserOverrideLayer(std::string_view AppConfig); /** diff --git a/unittests/APITests/CMakeLists.txt b/unittests/APITests/CMakeLists.txt index 2e472e8d9..db3ccf122 100644 --- a/unittests/APITests/CMakeLists.txt +++ b/unittests/APITests/CMakeLists.txt @@ -6,9 +6,10 @@ set(TESTS FileMappingBaseAddress Filesystem InterruptableConditionVariable - StringUtils) + StringUtils + WildcardMatcher) -list(APPEND LIBS Common FEXCore JemallocLibs) +list(APPEND LIBS Common FEXCore FEXCore_Base JemallocLibs) foreach(API_TEST ${TESTS}) add_executable(${API_TEST} ${API_TEST}.cpp) diff --git a/unittests/APITests/WildcardMatcher.cpp b/unittests/APITests/WildcardMatcher.cpp new file mode 100644 index 000000000..60b53027c --- /dev/null +++ b/unittests/APITests/WildcardMatcher.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include + +using namespace FEXCore::Utils::Wildcard; + +TEST_CASE("Singular regex") { + CHECK(Matches("a", "a")); + CHECK(Matches("a*", "a*")); + CHECK(Matches("a*", "aaaaaaa")); +} + +TEST_CASE("Concat regex") { + CHECK(Matches("aaa", "aaa")); + CHECK(Matches("ab", "ab")); + CHECK(!Matches("a", "ab")); + CHECK(!Matches("ab", "a")); +} +TEST_CASE("Wildcard beginning end") { + CHECK(Matches("a*", "a")); + CHECK(Matches("*a", "a")); + CHECK(Matches("*a*", "a")); +} +TEST_CASE("Wildcard middle") { + CHECK(Matches("test*pattern", "test__pattern")); +} + +TEST_CASE("Wildcard mult") { + CHECK(Matches("test*pattern*more", "test__pattern__more")); + CHECK(Matches("test**pattern", "test_pattern")); +} + +TEST_CASE("Wildcard regex simple") { + CHECK(Matches("*", "")); + CHECK(Matches("*", "setup.json")); + CHECK(Matches("test*pattern", "test__pattern")); + CHECK(!Matches("setup.*", "setupjson")); + CHECK(Matches("setup*", "setup.json")); + CHECK(Matches("setup*", "setup/setup.json")); + CHECK(Matches("*setup*", "setup/setup.json")); +} + + +TEST_CASE("FEX regex") { + CHECK(Matches("*Config*", "/home/ubuntu/.fex-emu/Config.json")); + CHECK(Matches("*Config.json", "/home/ubuntu/.fex-emu/Config.json")); +}