// SPDX-License-Identifier: MIT #include "Common/ArgumentLoader.h" #include "Common/Config.h" #include "Common/JSONPool.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #include #endif #include #include #include #include #include namespace FEX::Config { namespace JSON { static void LoadJSonConfig(const fextl::string& Config, std::optional AppName, std::function Func) { fextl::vector Data; if (!FEXCore::FileLoading::LoadFile(Data, Config)) { return; } FEX::JSON::JsonAllocator Pool {}; const json_t* json = FEX::JSON::CreateJSON(Data, Pool); if (!json) { ERROR_AND_DIE_FMT("Failed to parse JSON from file '{}' - invalid JSON format", Config); } const json_t* ConfigList = json_getProperty(json, "Config"); if (!ConfigList) { // This is a non-error if the configuration file exists but no Config section return; } fextl::vector ConfigBlocks; ConfigBlocks.push_back(ConfigList); 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; } } } } 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); } } } } // namespace JSON static constexpr std::pair ConfigLookup[] { #define OPT_BASE(type, group, enum, json, default) {#json, FEXCore::Config::ConfigOption::CONFIG_##enum}, #include }; static char* SaveLayerToJSON(char* JsonBuffer, const FEXCore::Config::Layer* Layer) { JsonBuffer = json_objOpen(JsonBuffer, "Config"); for (auto& it : Layer->GetOptionMap()) { std::string_view Name {}; for (auto& name_it : ConfigLookup) { if (name_it.second == it.first) { Name = name_it.first; break; } } if (std::holds_alternative(it.second)) { JsonBuffer = json_str(JsonBuffer, Name.data(), std::get(it.second).c_str()); } else if (std::holds_alternative(it.second)) { for (auto& var : std::get(it.second)) { JsonBuffer = json_str(JsonBuffer, Name.data(), var.c_str()); } } else { LogMan::Msg::AFmt("Trying to store config with pre-converted type"); } } return json_objClose(JsonBuffer); } void SaveLayerToJSON(const fextl::string& Filename, const FEXCore::Config::Layer* Layer, const fextl::unordered_map& HostLibs) { char Buffer[4096]; char* Dest {}; Dest = json_objOpen(Buffer, nullptr); Dest = SaveLayerToJSON(Dest, Layer); Dest = json_objOpen(Dest, "ThunksDB"); for (auto& [Name, Enabled] : HostLibs) { Dest = json_int(Dest, Name.c_str(), Enabled); } Dest = json_objClose(Dest); Dest = json_objClose(Dest); json_end(Dest); LogMan::Throw::AFmt(Dest <= std::end(Buffer), "Exceeded JSON buffer size"); auto File = FEXCore::File::File(Filename.c_str(), FEXCore::File::FileModes::WRITE | FEXCore::File::FileModes::CREATE | FEXCore::File::FileModes::TRUNCATE); if (File.IsValid()) { File.Write(Buffer, strlen(Buffer)); } } void SaveLayerToJSON(const fextl::string& Filename, const FEXCore::Config::Layer* Layer) { fextl::unordered_map HostLibsDB; // Load existing ThunksDB entry to persist it { fextl::vector FileData; if (!FEXCore::FileLoading::LoadFile(FileData, Filename)) { goto WriteConfig; } // Find bounds of previously existing Config entry (if any) FEX::JSON::JsonAllocator Pool {}; const json_t* json = FEX::JSON::CreateJSON(FileData, Pool); if (!json) { goto WriteConfig; } const json_t* ThunksDB = json_getProperty(json, "ThunksDB"); if (!ThunksDB) { goto WriteConfig; } for (const json_t* Item = json_getChild(ThunksDB); Item != nullptr; Item = json_getSibling(Item)) { HostLibsDB.emplace(json_getName(Item), (json_getInteger(Item) != 0)); } } WriteConfig: SaveLayerToJSON(Filename, Layer, HostLibsDB); } // Application loaders class OptionMapper : public FEXCore::Config::Layer { public: explicit OptionMapper(FEXCore::Config::LayerType Layer); protected: void MapNameToOption(const char* ConfigName, const char* ConfigString); void SetCurrentConfigFile(const fextl::string& Filename) { CurrentConfigFile = Filename; } fextl::string CurrentConfigFile; }; class MainLoader final : public OptionMapper { public: 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); explicit MainLoader(FEXCore::Config::LayerType Type, std::string_view ConfigFile, std::optional AppName = std::nullopt); void Load() override; private: std::optional AppName; fextl::string Config; }; class AppLoader final : public OptionMapper { public: explicit AppLoader(const fextl::string& AppName, FEXCore::Config::LayerType Type); void Load() override; private: const fextl::string AppName; fextl::string Config; }; class EnvLoader final : public FEXCore::Config::Layer { public: explicit EnvLoader(char* const _envp[]); void Load() override; private: char* const* envp; }; OptionMapper::OptionMapper(FEXCore::Config::LayerType Layer) : FEXCore::Config::Layer(Layer) {} void OptionMapper::MapNameToOption(const char* ConfigName, const char* ConfigString) { std::optional KeyOptionValue; for (auto& it : ConfigLookup) { if (it.first != ConfigName) { continue; } KeyOptionValue = it.second; break; } if (!KeyOptionValue.has_value()) { LogMan::Msg::IFmt("Unknown configuration option '{}' in JSON config file '{}'", ConfigName, CurrentConfigFile); return; } const auto KeyOption = *KeyOptionValue; const auto KeyName = std::string_view(ConfigName); const auto Value_View = std::string_view(ConfigString); #define JSONLOADER #include } MainLoader::MainLoader(FEXCore::Config::LayerType Type, std::optional AppName) : OptionMapper(Type) , AppName {std::move(AppName)} , Config {FEXCore::Config::GetConfigFileLocation(Type == FEXCore::Config::LayerType::LAYER_GLOBAL_MAIN)} {} MainLoader::MainLoader(fextl::string ConfigFile, std::optional AppName) : OptionMapper(FEXCore::Config::LayerType::LAYER_MAIN) , AppName {std::move(AppName)} , Config {std::move(ConfigFile)} {} MainLoader::MainLoader(FEXCore::Config::LayerType Type, std::string_view ConfigFile) : OptionMapper(Type) , Config {ConfigFile} {} MainLoader::MainLoader(FEXCore::Config::LayerType Type, std::string_view ConfigFile, std::optional AppName) : OptionMapper(Type) , AppName {std::move(AppName)} , Config {ConfigFile} {} void MainLoader::Load() { SetCurrentConfigFile(Config); JSON::LoadJSonConfig(Config, AppName, [this](const char* Name, const char* ConfigString) { MapNameToOption(Name, ConfigString); }); } 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(AppName, Global); // Immediately load so we can reload the meta layer Load(); } void AppLoader::Load() { SetCurrentConfigFile(Config); JSON::LoadJSonConfig(Config, AppName, [this](const char* Name, const char* ConfigString) { MapNameToOption(Name, ConfigString); }); } EnvLoader::EnvLoader(char* const _envp[]) : FEXCore::Config::Layer(FEXCore::Config::LayerType::LAYER_ENVIRONMENT) , envp {_envp} {} void EnvLoader::Load() { using EnvMapType = fextl::unordered_map; EnvMapType EnvMap; for (const char* const* pvar = envp; pvar && *pvar; pvar++) { std::string_view Var(*pvar); ///< All FEX environment variables start with `FEX_` if (!Var.starts_with("FEX_")) { continue; } size_t pos = Var.rfind('='); if (fextl::string::npos == pos) { continue; } std::string_view Key = Var.substr(0, pos); std::string_view Value_View {Var.substr(pos + 1)}; std::optional Value; #define ENVLOADER #include if (Value) { EnvMap.insert_or_assign(Key, std::move(*Value)); } else { EnvMap.insert_or_assign(Key, Value_View); } } auto GetVar = [](const EnvMapType& EnvMap, std::string_view id) -> std::optional { const auto EnvEntry = EnvMap.find(id); if (EnvEntry != EnvMap.end()) { return EnvEntry->second; } // If envp[] was empty, search using std::getenv() const char* vs = std::getenv(id.data()); if (vs) { return vs; } else { return std::nullopt; } }; std::optional Value; // Walk all the environment options and corresponding config option. #define OPT_BASE(type, group, enum, json, default) \ Value = GetVar(EnvMap, "FEX_" #enum); \ if (Value.has_value()) Set(FEXCore::Config::ConfigOption::CONFIG_##enum, *Value); #define OPT_STRARRAY(group, enum, json, default) \ Value = GetVar(EnvMap, "FEX_" #enum); \ if (Value.has_value()) AppendStrArrayValue(FEXCore::Config::ConfigOption::CONFIG_##enum, *Value); #include } fextl::unique_ptr CreateGlobalMainLayer() { return fextl::make_unique(FEXCore::Config::LayerType::LAYER_GLOBAL_MAIN); } fextl::unique_ptr CreateMainLayer(const fextl::string* File, std::optional AppName) { if (File) { return fextl::make_unique(*File, std::move(AppName)); } else { return fextl::make_unique(FEXCore::Config::LayerType::LAYER_MAIN, std::move(AppName)); } } fextl::unique_ptr CreateUserOverrideLayer(std::string_view AppConfig, std::optional AppName) { return fextl::make_unique(FEXCore::Config::LayerType::LAYER_USER_OVERRIDE, AppConfig, std::move(AppName)); } fextl::unique_ptr CreateAppLayer(const fextl::string& Filename, FEXCore::Config::LayerType Type) { return fextl::make_unique(Filename, Type); } fextl::unique_ptr CreateEnvironmentLayer(char* const _envp[]) { return fextl::make_unique(_envp); } static fextl::string RecoverGuestProgramFilename(fextl::string Program, bool ExecFDInterp, int ProgramFDFromEnv) { // If executed with a FEX FD then the Program argument might be empty. // In this case we need to scan the FD node to recover the application binary that exists on disk. // Only do this if the Program argument is empty, since we would prefer the application's expectation // of application name. if (ProgramFDFromEnv != -1 && Program.empty()) { // Get the `dev` node of the execveat fd string. Program = fextl::fmt::format("/dev/fd/{}", ProgramFDFromEnv); } // If we were provided a relative path then we need to canonicalize it to become absolute. // If the program name isn't resolved to an absolute path then glibc breaks inside it's `_dl_get_origin` function. // This is because we rewrite `/proc/self/exe` to the absolute program path calculated in here. if (!Program.starts_with('/')) { char ExistsTempPath[PATH_MAX]; char* RealPath = FHU::Filesystem::Absolute(Program.c_str(), ExistsTempPath); if (RealPath) { Program = RealPath; } } // If FEX was invoked through an FD path (either binfmt_misc or execveat) then we need to check the // Program to see if it is a symlink to find the real path. // // binfmt_misc: Arg[0] is actually the execve `pathname` argument or `/dev/fd/` path // - `pathname` with execve (See Side Note) // - FD path with execveat and FD doesn't have an existing file on the disk // // ProgramFDFromEnv: Arg[0] is Application provided data or `/dev/fd/` from above fix-up. // - execveat was either passed no arguments (argv=NULL) or the first argument is an empty string (argv[0]=""). // - FD path with execveat and FD doesn't have an existing file on the disk // // Side Note: // The `execve` syscall doesn't take an FD but binfmt_misc will give FEX an FD to execute still. // Arg[0] will always contain the `pathname` argument provided to execve. // It does not resolve symlinks, and it does not convert the path to absolute. // // Examples: // - Regular execve. Application must exist on disk. // execve binfmt_misc args layout: `FEX ...` // - Regular execveat with FD. FD is backed by application on disk. // execveat binfmt_misc args layout: `FEX ...` // - Regular execveat with FD. FD points to file on disk that has been deleted. // execveat binfmt_misc args layout: `FEX /dev/fd/ ...` #ifndef _WIN32 if (ExecFDInterp || ProgramFDFromEnv != -1) { // Only in the case that FEX is executing an FD will the program argument potentially be a symlink. // This symlink will be in the style of `/dev/fd/`. // // If the argument /is/ a symlink then resolve its path continuously until the original application name. while (FHU::Symlinks::IsSymlink(Program)) { char Filename[PATH_MAX]; auto SymlinkPath = FHU::Symlinks::ResolveSymlink(Program, Filename); if (!SymlinkPath.starts_with('/')) { break; } // This file was executed through an FD. // Remove the ` (deleted)` text if the file was deleted after the fact. // Otherwise just get the symlink without the deleted text. Program = fextl::string {SymlinkPath.substr(0, SymlinkPath.rfind(" (deleted)"))}; } } #endif return Program; } ApplicationNames GetApplicationNames(const fextl::vector& Args, bool ExecFDInterp, int ProgramFDFromEnv) { if (Args.empty()) { // Early exit if we weren't passed an argument return {}; } fextl::string Program {}; fextl::string ProgramName {}; Program = RecoverGuestProgramFilename(Args[0], ExecFDInterp, ProgramFDFromEnv); bool Wine = false; for (size_t CurrentProgramNameIndex = 0; CurrentProgramNameIndex < Args.size(); ++CurrentProgramNameIndex) { auto CurrentProgramName = FHU::Filesystem::GetFilename(Args[CurrentProgramNameIndex]); if (CurrentProgramName == "wine-preloader" || CurrentProgramName == "wine64-preloader") { // Wine preloader is required to be in the format of `wine-preloader ` // The preloader doesn't execve the executable, instead maps it directly itself // Skip the next argument since we know it is wine (potentially with custom wine executable name) ++CurrentProgramNameIndex; Wine = true; } else if (CurrentProgramName == "wine" || CurrentProgramName == "wine64") { // Next argument, this isn't the program we want // // If we are running wine or wine64 then we should check the next argument for the application name instead. // wine will change the active program name with `setprogname` or `prctl(PR_SET_NAME`. // Since FEX needs this data far earlier than libraries we need a different check. Wine = true; } else { if (Wine == true) { // If this was path separated with '\' then we need to check that. auto WinSeparator = CurrentProgramName.find_last_of('\\'); if (WinSeparator != CurrentProgramName.npos) { // Used windows separators CurrentProgramName = CurrentProgramName.substr(WinSeparator + 1); } } ProgramName = std::move(CurrentProgramName); // Past any wine program names break; } } return ApplicationNames {std::move(Program), std::move(ProgramName)}; } void LoadConfig(fextl::string ProgramName, char** const envp, const PortableInformation& PortableInfo) { const bool IsPortable = PortableInfo.IsPortable; FEX::Config::InitializeConfigs(PortableInfo); FEXCore::Config::Initialize(); if (!IsPortable) { FEXCore::Config::AddLayer(CreateGlobalMainLayer()); } FEXCore::Config::AddLayer(CreateMainLayer(nullptr, ProgramName.empty() ? std::nullopt : std::optional {ProgramName})); if (!ProgramName.empty()) { if (!IsPortable) { FEXCore::Config::AddLayer(CreateAppLayer(ProgramName, FEXCore::Config::LayerType::LAYER_GLOBAL_APP)); } FEXCore::Config::AddLayer(CreateAppLayer(ProgramName, FEXCore::Config::LayerType::LAYER_LOCAL_APP)); auto SteamID = getenv("SteamAppId"); if (SteamID) { // If a SteamID exists then let's search for Steam application configs as well. // We want to key off both the SteamAppId number /and/ the executable since we may not want to thunk all binaries. fextl::string SteamAppName = fextl::fmt::format("Steam_{}_{}", SteamID, ProgramName); if (!IsPortable) { FEXCore::Config::AddLayer(CreateAppLayer(SteamAppName, FEXCore::Config::LayerType::LAYER_GLOBAL_STEAM_APP)); } FEXCore::Config::AddLayer(CreateAppLayer(SteamAppName, FEXCore::Config::LayerType::LAYER_LOCAL_STEAM_APP)); } } const char* AppConfig = getenv("FEX_APP_CONFIG"); if (AppConfig) { fextl::string AppConfigStr = AppConfig; if (IsPortable && FHU::Filesystem::IsRelative(AppConfig)) { AppConfigStr = PortableInfo.InterpreterPath + AppConfigStr; } if (FHU::Filesystem::Exists(AppConfigStr)) { FEXCore::Config::AddLayer(CreateUserOverrideLayer(AppConfigStr, ProgramName.empty() ? std::nullopt : std::optional {ProgramName})); } } FEXCore::Config::AddLayer(CreateEnvironmentLayer(envp)); FEXCore::Config::Load(); } #ifndef _WIN32 static fextl::string FindUserHomeThroughUID() { // `getpwuid` allocates memory, parse `/etc/passwd` manually. // Format is trivial: `::::::` fextl::vector Data; if (!FEXCore::FileLoading::LoadFile(Data, "/etc/passwd")) { return {}; } auto to_string_view = [](auto rng) { return std::string_view(&*rng.begin(), ranges::distance(rng)); }; const auto uid = geteuid(); for (const auto entry : ranges::views::split(Data, '\n') | ranges::views::transform(to_string_view)) { const auto elements = ranges::views::split(entry, ':') | ranges::views::transform(to_string_view); // Reject bad entries. if (std::distance(elements.begin(), elements.end()) != 7) { continue; } auto iter = elements.begin(); ++iter; // name ++iter; // password hash ++iter; // comment // uid const auto uid_s = *iter; ++iter; ++iter; // gid // home const auto home_s = *iter; ++iter; ++iter; // shell uint64_t element_uid; auto Results = std::from_chars(uid_s.begin(), uid_s.end(), element_uid, 10); // Error parsing. if (Results.ptr == uid_s.begin()) { continue; } if (element_uid == uid) { return fextl::string(home_s); } } return {}; } fextl::string GetHomeDirectory() { const char* HomeDir = getenv("HOME"); // Try to get home directory from uid if (!HomeDir || !FHU::Filesystem::Exists(HomeDir)) { auto UIDHome = FindUserHomeThroughUID(); if (!UIDHome.empty() && FHU::Filesystem::Exists(UIDHome)) { return UIDHome; } } // try the PWD if (!HomeDir || !FHU::Filesystem::Exists(HomeDir)) { HomeDir = getenv("PWD"); } // Still doesn't exit? You get local if (!HomeDir || !FHU::Filesystem::Exists(HomeDir)) { HomeDir = "."; } return HomeDir; } #else fextl::string GetHomeDirectory() { const char* HomeDir = getenv("WINEHOMEDIR"); if (HomeDir) { // Skip over the \??\ prefix in the NT path since we want a DOS path HomeDir += 4; }; if (!HomeDir) { HomeDir = getenv("LOCALAPPDATA"); } if (!HomeDir) { HomeDir = "."; } return HomeDir; } #endif fextl::string GetDataDirectory(bool Global, const PortableInformation& PortableInfo) { #ifdef FEX_STEAM_SUPPORT const char* SteamDataPath = getenv("STEAM_COMPAT_DATA_PATH"); if (SteamDataPath) { return fextl::fmt::format("{}/fex-emu/", SteamDataPath); } #endif const char* DataOverride = getenv("FEX_APP_DATA_LOCATION"); if (PortableInfo.IsPortable && (Global || !DataOverride)) { return fextl::fmt::format("{}/fex-emu/", PortableInfo.InterpreterPath); } if (Global) { return GLOBAL_DATA_DIRECTORY; } auto HomeDir = GetHomeDirectory(); const char* DataXDG = getenv("XDG_DATA_HOME"); const fextl::string LegacyDir = fextl::string {HomeDir} + "/.fex-emu/"; // If $HOME/.fex-emu exists, use that if (FHU::Filesystem::Exists(LegacyDir)) { return LegacyDir; } fextl::string DataDir {}; if (DataOverride) { // Data override will override the complete directory DataDir = DataOverride; } else { // use ~/.local/share if XDG_DATA_HOME is unset DataDir = DataXDG ? DataXDG : fmt::format("{}/.local/share", HomeDir); DataDir += "/fex-emu/"; } return DataDir; } fextl::string GetConfigDirectory(bool Global, const PortableInformation& PortableInfo) { const char* ConfigOverride = getenv("FEX_APP_CONFIG_LOCATION"); if (PortableInfo.IsPortable && Global) { return fextl::fmt::format("{}/../share/fex-emu/", PortableInfo.InterpreterPath); } else if (ConfigOverride && !Global) { fextl::string AppConfigStr = ConfigOverride; if (FHU::Filesystem::IsRelative(AppConfigStr)) { AppConfigStr = PortableInfo.InterpreterPath + AppConfigStr; } return AppConfigStr; } #ifdef FEX_STEAM_SUPPORT const char* SteamDataPath = getenv("STEAM_COMPAT_DATA_PATH"); if (SteamDataPath) { return fextl::fmt::format("{}/fex-emu/", SteamDataPath); } #endif fextl::string ConfigDir; if (Global) { return GLOBAL_DATA_DIRECTORY; } auto HomeDir = GetHomeDirectory(); const char* ConfigXDG = getenv("XDG_CONFIG_HOME"); const fextl::string LegacyDir = fextl::string {HomeDir} + "/.fex-emu/"; // If $HOME/.fex-emu exists, use that if (FHU::Filesystem::Exists(LegacyDir)) { return LegacyDir; } if (ConfigOverride) { // Config override will override the complete directory ConfigDir = ConfigOverride; } else { // use ~/.config if XDG_CONFIG_HOME is unset ConfigDir = ConfigXDG ? ConfigXDG : fmt::format("{}/.config", HomeDir); ConfigDir += "/fex-emu/"; } return ConfigDir; } fextl::string GetCacheDirectory() { const char* CacheOverride = getenv("FEX_APP_CACHE_LOCATION"); if (CacheOverride) { return CacheOverride; } #ifndef _WIN32 #ifdef FEX_STEAM_SUPPORT const char* SteamDataPath = getenv("STEAM_COMPAT_SHADER_PATH"); if (SteamDataPath) { return fextl::fmt::format("{}/fex-emu/", SteamDataPath); } #endif auto HomeDir = GetHomeDirectory(); const char* CacheXDG = getenv("XDG_CACHE_HOME"); return (CacheXDG ? fextl::string {CacheXDG} : (fextl::string {HomeDir} + "/.cache")) + "/fex-emu/"; #else const char* PrefixAppData = getenv("LOCALAPPDATA"); return PrefixAppData ? (fextl::string {PrefixAppData} + "\\fex-emu\\") : fextl::string {".\\"}; #endif } fextl::string GetConfigFileLocation(bool Global, const PortableInformation& PortableInfo) { return GetConfigDirectory(Global, PortableInfo) + "Config.json"; } void InitializeConfigs(const PortableInformation& PortableInfo) { FEXCore::Config::SetDataDirectory(GetDataDirectory(false, PortableInfo), false); FEXCore::Config::SetDataDirectory(GetDataDirectory(true, PortableInfo), true); FEXCore::Config::SetConfigDirectory(GetConfigDirectory(false, PortableInfo), false); FEXCore::Config::SetConfigDirectory(GetConfigDirectory(true, PortableInfo), true); FEXCore::Config::SetConfigFileLocation(GetConfigFileLocation(false, PortableInfo), false); FEXCore::Config::SetConfigFileLocation(GetConfigFileLocation(true, PortableInfo), true); FEXCore::Config::SetCacheDirectory(GetCacheDirectory()); } } // namespace FEX::Config