// SPDX-License-Identifier: MIT #include "Common/ArgumentLoader.h" #include "Common/Config.h" #include "Common/JSONPool.h" #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #include #endif #include #include namespace FEX::Config { namespace JSON { static void LoadJSonConfig(const fextl::string& Config, 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; } 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); 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); } } } // 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); explicit MainLoader(fextl::string ConfigFile); explicit MainLoader(FEXCore::Config::LayerType Type, std::string_view ConfigFile); void Load() override; private: fextl::string Config; }; class AppLoader final : public OptionMapper { public: explicit AppLoader(const fextl::string& Filename, FEXCore::Config::LayerType Type); void Load(); private: 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) : OptionMapper(Type) , Config {FEXCore::Config::GetConfigFileLocation(Type == FEXCore::Config::LayerType::LAYER_GLOBAL_MAIN)} {} MainLoader::MainLoader(fextl::string ConfigFile) : OptionMapper(FEXCore::Config::LayerType::LAYER_MAIN) , Config {std::move(ConfigFile)} {} MainLoader::MainLoader(FEXCore::Config::LayerType Type, std::string_view ConfigFile) : OptionMapper(Type) , Config {ConfigFile} {} void MainLoader::Load() { SetCurrentConfigFile(Config); JSON::LoadJSonConfig(Config, [this](const char* Name, const char* ConfigString) { MapNameToOption(Name, ConfigString); }); } AppLoader::AppLoader(const fextl::string& Filename, FEXCore::Config::LayerType Type) : OptionMapper(Type) { const bool Global = Type == FEXCore::Config::LayerType::LAYER_GLOBAL_STEAM_APP || Type == FEXCore::Config::LayerType::LAYER_GLOBAL_APP; Config = FEXCore::Config::GetApplicationConfig(Filename, Global); // Immediately load so we can reload the meta layer Load(); } void AppLoader::Load() { SetCurrentConfigFile(Config); JSON::LoadJSonConfig(Config, [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, *Value); } else { EnvMap.insert_or_assign(Key, Value_View); } } auto GetVar = [](EnvMapType& EnvMap, const std::string_view id) -> std::optional { if (EnvMap.find(id) != EnvMap.end()) { return EnvMap.at(id); } // 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) { if (File) { return fextl::make_unique(*File); } else { return fextl::make_unique(FEXCore::Config::LayerType::LAYER_MAIN); } } fextl::unique_ptr CreateUserOverrideLayer(std::string_view AppConfig) { return fextl::make_unique(FEXCore::Config::LayerType::LAYER_USER_OVERRIDE, AppConfig); } 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); } 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: `FEXInterpreter ...` // - Regular execveat with FD. FD is backed by application on disk. // execveat binfmt_misc args layout: `FEXInterpreter ...` // - Regular execveat with FD. FD points to file on disk that has been deleted. // execveat binfmt_misc args layout: `FEXInterpreter /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 to get the original application name. if (FHU::Symlinks::IsSymlink(Program)) { char Filename[PATH_MAX]; auto SymlinkPath = FHU::Symlinks::ResolveSymlink(Program, Filename); if (SymlinkPath.starts_with('/')) { // 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. return 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::unique_ptr ArgsLoader, 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()); 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)); } } if (ArgsLoader && ArgsLoader->GetLoadType() == FEX::ArgLoader::ArgLoader::LoadType::WITH_FEXLOADER_PARSER) { FEXCore::Config::AddLayer(std::move(ArgsLoader)); } 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)); } } FEXCore::Config::AddLayer(CreateEnvironmentLayer(envp)); FEXCore::Config::Load(); } #ifndef _WIN32 const char* FindUserHomeThroughUID() { auto passwd = getpwuid(geteuid()); if (passwd) { return passwd->pw_dir; } return nullptr; } const char* GetHomeDirectory() { const char* HomeDir = getenv("HOME"); // Try to get home directory from uid if (!HomeDir || !FHU::Filesystem::Exists(HomeDir)) { HomeDir = FindUserHomeThroughUID(); } // 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 const char* 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) { const char* DataOverride = getenv("FEX_APP_DATA_LOCATION"); if (PortableInfo.IsPortable && (Global || !DataOverride)) { return fextl::fmt::format("{}/fex-emu/", PortableInfo.InterpreterPath); } fextl::string DataDir {}; if (Global) { DataDir = GLOBAL_DATA_DIRECTORY; } else { const char* HomeDir = GetHomeDirectory(); const char* DataXDG = getenv("XDG_DATA_HOME"); if (DataOverride) { // Data override will override the complete directory DataDir = DataOverride; } else { DataDir = DataXDG ?: 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 || !ConfigOverride)) { return fextl::fmt::format("{}/fex-emu/", PortableInfo.InterpreterPath); } else if (PortableInfo.IsPortable && ConfigOverride && !Global) { fextl::string AppConfigStr = ConfigOverride; if (PortableInfo.IsPortable && FHU::Filesystem::IsRelative(AppConfigStr)) { AppConfigStr = PortableInfo.InterpreterPath + AppConfigStr; } return AppConfigStr; } fextl::string ConfigDir; if (Global) { ConfigDir = GLOBAL_DATA_DIRECTORY; } else { const char* HomeDir = GetHomeDirectory(); const char* ConfigXDG = getenv("XDG_CONFIG_HOME"); if (ConfigOverride) { // Config override completely overrides the config directory ConfigDir = ConfigOverride; } else { ConfigDir = ConfigXDG ? ConfigXDG : HomeDir; ConfigDir += "/.fex-emu/"; } // Ensure the folder structure is created for our configuration if (!FHU::Filesystem::Exists(ConfigDir) && !FHU::Filesystem::CreateDirectories(ConfigDir)) { // Let's go local in this case return "./"; } } return ConfigDir; } 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); } } // namespace FEX::Config