From 81d4e8fe9d661be9c77bc33c8c2c32191c075771 Mon Sep 17 00:00:00 2001 From: badumbatish Date: Fri, 12 Dec 2025 21:01:27 +0000 Subject: [PATCH 1/2] Initial implementation for regex engine Add support for question mark and plus mark in regex, supply testing for star Added more characters to the regex alphabets, add more test case Added support for regex matching of configs, awaiting reviews Rename variable to CamelCase Addresses PR reviews Remove unnecessary features and test cases Rewrite to naive regex with dp Addresses PR reviews Build fixes Code Review --- FEXCore/Source/CMakeLists.txt | 3 +- FEXCore/Source/Utils/WildcardMatcher.cpp | 37 +++++++++++++ .../include/FEXCore/Utils/WildcardMatcher.h | 8 +++ Source/Common/Config.cpp | 55 ++++++++++++++----- unittests/APITests/CMakeLists.txt | 5 +- unittests/APITests/WildcardMatcher.cpp | 48 ++++++++++++++++ 6 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 FEXCore/Source/Utils/WildcardMatcher.cpp create mode 100644 FEXCore/include/FEXCore/Utils/WildcardMatcher.h create mode 100644 unittests/APITests/WildcardMatcher.cpp 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..2e0c94c25 --- /dev/null +++ b/FEXCore/Source/Utils/WildcardMatcher.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +#include + +namespace FEXCore::Utils::Wildcard { +class WildcardMatcher { +private: + std::string_view pattern; + std::string_view text; + +public: + WildcardMatcher(std::string_view pattern, std::string_view text) + : pattern(pattern) + , text(text) {} + + bool matchHelper(size_t p_idx, size_t t_idx); +}; + +bool WildcardMatcher::matchHelper(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(p_idx + 1, t_idx) || (t_idx < text.size() && matchHelper(p_idx, t_idx + 1)); + } else { + // Match normally + return (t_idx < text.size() && pattern[p_idx] == text[t_idx] && matchHelper(p_idx + 1, t_idx + 1)); + } +} + +bool Matches(std::string_view pattern, std::string_view text) { + WildcardMatcher matcher(pattern, text); + return matcher.matchHelper(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..265d32d62 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,7 @@ namespace FEX::Config { namespace JSON { - static void LoadJSonConfig(const fextl::string& Config, std::function Func) { + static void LoadJSonConfig(const fextl::string& Config, std::function Func) { fextl::vector Data; if (!FEXCore::FileLoading::LoadFile(Data, Config)) { return; @@ -48,21 +49,49 @@ 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; + 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* ItemName = json_getName(Item); + const json_t* OverrideNamedList = json_getProperty(OverrideList, ItemName); + + if (!ItemName) { + LogMan::Msg::EFmt("JSON file '{}': Couldn't get config name for an item", Config); + break; + } + + if (!OverrideNamedList) { + LogMan::Msg::EFmt("JSON file '{}': Couldn't get value for config item '{}'", Config, ItemName); + break; + } + + // Find the first match, then break + if (FEXCore::Utils::Wildcard::Matches(ItemName, Config)) { + ConfigBlocks.push_back(OverrideNamedList); + 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 (!ConfigName) { + LogMan::Msg::EFmt("JSON file '{}': Couldn't get config name for an item", Config); + return; + } + + 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 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")); +} From 4018d23c39bb267054f61d8f2e763b48da59c779 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Fri, 20 Mar 2026 15:26:06 -0700 Subject: [PATCH 2/2] Config: Finish wiring up Regex app overrides This wasn't quite wired up exactly how we wanted it. It was previously matching against the opaque file config handle, which can be anything. Instead compare it to the appname that now gets passed over to it for matching. This allows us to do the following: ``` { "Config": { "ProfileStats": "1", "X87ReducedPrecision": "1", "TSOEnabled": "1", "VectorTSOEnabled": "0", "MemcpySetTSOEnabled": "0", "HalfBarrierTSOEnabled":"1", "MaxInst": "500", "Multiblock": "1" }, "AppOverrides" : { "setup*" : { "Comment": [ "292030 - The Witcher 3: Wild Hunt" ], "X87ReducedPrecision": "0" } } } ``` Based on #121 which needs to get merged first. Code Review Code Review: Class deletion --- FEXCore/Source/Utils/WildcardMatcher.cpp | 22 ++------ Source/Common/Config.cpp | 70 +++++++++++------------- Source/Common/Config.h | 2 +- 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/FEXCore/Source/Utils/WildcardMatcher.cpp b/FEXCore/Source/Utils/WildcardMatcher.cpp index 2e0c94c25..70dad5632 100644 --- a/FEXCore/Source/Utils/WildcardMatcher.cpp +++ b/FEXCore/Source/Utils/WildcardMatcher.cpp @@ -3,35 +3,21 @@ #include namespace FEXCore::Utils::Wildcard { -class WildcardMatcher { -private: - std::string_view pattern; - std::string_view text; - -public: - WildcardMatcher(std::string_view pattern, std::string_view text) - : pattern(pattern) - , text(text) {} - - bool matchHelper(size_t p_idx, size_t t_idx); -}; - -bool WildcardMatcher::matchHelper(size_t p_idx, size_t t_idx) { +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(p_idx + 1, t_idx) || (t_idx < text.size() && matchHelper(p_idx, t_idx + 1)); + 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(p_idx + 1, t_idx + 1)); + 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) { - WildcardMatcher matcher(pattern, text); - return matcher.matchHelper(0, 0); + return matchHelper(pattern, text, 0, 0); } } // namespace FEXCore::Utils::Wildcard diff --git a/Source/Common/Config.cpp b/Source/Common/Config.cpp index 265d32d62..62bea17cd 100644 --- a/Source/Common/Config.cpp +++ b/Source/Common/Config.cpp @@ -29,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; @@ -52,26 +53,17 @@ namespace JSON { fextl::vector ConfigBlocks; ConfigBlocks.push_back(ConfigList); - 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* ItemName = json_getName(Item); - const json_t* OverrideNamedList = json_getProperty(OverrideList, ItemName); + 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); - if (!ItemName) { - LogMan::Msg::EFmt("JSON file '{}': Couldn't get config name for an item", Config); - break; - } - - if (!OverrideNamedList) { - LogMan::Msg::EFmt("JSON file '{}': Couldn't get value for config item '{}'", Config, ItemName); - break; - } - - // Find the first match, then break - if (FEXCore::Utils::Wildcard::Matches(ItemName, Config)) { - ConfigBlocks.push_back(OverrideNamedList); - break; + // Find the first match, then break + if (FEXCore::Utils::Wildcard::Matches(AppPattern, *AppName)) { + ConfigBlocks.push_back(Item); + break; + } } } } @@ -81,11 +73,6 @@ namespace JSON { const char* ConfigName = json_getName(ConfigItem); const char* ConfigString = json_getValue(ConfigItem); - if (!ConfigName) { - LogMan::Msg::EFmt("JSON file '{}': Couldn't get config name for an item", Config); - return; - } - if (!ConfigString) { LogMan::Msg::EFmt("JSON file '{}': Couldn't get value for config item '{}'", Config, ConfigName); return; @@ -196,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; }; @@ -250,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)} {} @@ -265,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(); @@ -279,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[]) @@ -349,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)); } } @@ -490,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); /**