Files
FEX-Emu--FEX/Source/Tools/FEXRootFSFetcher/Main.cpp
T

1306 lines
39 KiB
C++

// SPDX-License-Identifier: MIT
#include <FEXCore/fextl/fmt.h>
#include <FEXCore/fextl/string.h>
#include "Common/cpp-optparse/OptionParser.h"
#include "Common/JSONPool.h"
#include "XXFileHash.h"
#include "Common/ArgumentLoader.h"
#include "Common/Config.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <unistd.h>
#include <optional>
#include <sstream>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
#include <tiny-json.h>
namespace ArgOptions {
bool AssumeYes = false;
enum class CompressedImageOption {
OPTION_ASK,
OPTION_EXTRACT,
OPTION_ASIS,
};
CompressedImageOption CompressedUsageOption {CompressedImageOption::OPTION_ASK};
enum class ListQueryOption {
OPTION_ASK,
OPTION_FIRST,
};
ListQueryOption DistroListOption {ListQueryOption::OPTION_ASK};
fextl::vector<fextl::string> RemainingArgs;
std::string DistroName {};
std::string DistroVersion {};
enum class UIOverrideOption {
Default,
TTY,
Zenity,
};
UIOverrideOption UIOption {UIOverrideOption::Default};
void ParseArguments(int argc, char** argv) {
optparse::OptionParser Parser = optparse::OptionParser().description("Tool for fetching RootFS from FEXServers").add_help_option(true);
Parser.add_option("-y", "--assume-yes").action("store_true").help("Assume yes to prompts");
Parser.add_option("-x", "--extract").action("store_true").help("Extract compressed image");
Parser.add_option("-a", "--as-is").action("store_true").help("Use compressed image as-is");
Parser.add_option("--distro-name").help("Which distro name to select");
Parser.add_option("--distro-version").help("Which distro version to select");
Parser.add_option("--distro-list-first").action("store_true").help("When presented the distro-list option, automatically select the first distro if there isn't an exact match.");
Parser.add_option("--force-ui").choices({"default", "tty", "zenity"}).set_default("default").help("Override which UI to use for selection");
optparse::Values Options = Parser.parse_args(argc, argv);
if (Options.is_set_by_user("assume_yes")) {
AssumeYes = Options.get("assume_yes");
}
if (Options.is_set_by_user("extract")) {
CompressedUsageOption = CompressedImageOption::OPTION_EXTRACT;
}
if (Options.is_set_by_user("as_is")) {
CompressedUsageOption = CompressedImageOption::OPTION_ASIS;
}
if (Options.is_set_by_user("distro_list_first")) {
DistroListOption = ListQueryOption::OPTION_FIRST;
}
if (Options.is_set_by_user("distro_name")) {
DistroName = Options["distro_name"];
}
if (Options.is_set_by_user("distro_version")) {
DistroVersion = Options["distro_version"];
}
if (Options.is_set_by_user("force_ui")) {
const auto& Option = Options["force_ui"];
if (Option == "tty") {
UIOption = UIOverrideOption::TTY;
} else if (Option == "zenity") {
UIOption = UIOverrideOption::Zenity;
}
}
RemainingArgs = Parser.args();
}
} // namespace ArgOptions
namespace Exec {
int32_t ExecAndWaitForResponse(const char* path, char* const* args) {
pid_t pid = fork();
if (pid == 0) {
execvp(path, args);
_exit(-1);
} else {
int32_t Status {};
waitpid(pid, &Status, 0);
if (WIFEXITED(Status)) {
return (int8_t)WEXITSTATUS(Status);
}
}
return -1;
}
int32_t ExecAndWaitForResponseRedirect(const char* path, char* const* args, int stdoutRedirect = -2, int stderrRedirect = -2) {
pid_t pid = fork();
if (pid == 0) {
if (stdoutRedirect == -1) {
close(STDOUT_FILENO);
} else if (stdoutRedirect == -2) {
// Do nothing
} else {
if (stdoutRedirect != STDOUT_FILENO) {
close(STDOUT_FILENO);
}
dup2(stdoutRedirect, STDOUT_FILENO);
}
if (stderrRedirect == -1) {
close(STDERR_FILENO);
} else if (stderrRedirect == -2) {
// Do nothing
} else {
if (stderrRedirect != STDOUT_FILENO) {
close(STDERR_FILENO);
}
dup2(stderrRedirect, STDERR_FILENO);
}
execvp(path, args);
_exit(-1);
} else {
int32_t Status {};
while (waitpid(pid, &Status, 0) == -1 && errno == EINTR)
;
if (WIFEXITED(Status)) {
return (int8_t)WEXITSTATUS(Status);
}
}
return -1;
}
std::string ExecAndWaitForResponseText(const char* path, char* const* args) {
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
close(fd[0]); // Close read side
// Redirect stdout to pipe
dup2(fd[1], STDOUT_FILENO);
// Close stderr
close(STDERR_FILENO);
// We can now close the pipe since the duplications take care of the rest
close(fd[1]);
execvp(path, args);
_exit(-1);
} else {
close(fd[1]); // Close write side
// Nothing larger than this
char Buffer[1024] {};
std::string Output {};
// Read the pipe until it closes
while (size_t Size = read(fd[0], Buffer, sizeof(Buffer))) {
Output += std::string_view(Buffer, Size);
}
int32_t Status {};
while (waitpid(pid, &Status, 0) == -1 && errno == EINTR)
;
if (WIFEXITED(Status)) {
// Return what we've read
close(fd[0]);
return Output;
}
}
return {};
}
} // namespace Exec
namespace WorkingAppsTester {
static bool Has_Curl {false};
static bool Has_Squashfuse {false};
static bool Has_Unsquashfs {false};
static bool Has_Zenity {false};
// EroFS specific
static bool Has_EroFSFuse {false};
static bool Has_EroFSFsck {false};
void CheckCurl() {
// Check if curl exists on the host
const std::array<const char*, 3> ExecveArgs = {
"curl",
"-V",
nullptr,
};
int32_t Result = Exec::ExecAndWaitForResponseRedirect(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()), -1, -1);
Has_Curl = Result != -1;
}
void CheckSquashfuse() {
const std::array<const char*, 3> ExecveArgs = {
"squashfuse",
"--help",
nullptr,
};
int32_t Result = Exec::ExecAndWaitForResponseRedirect(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()), -1, -1);
Has_Squashfuse = Result != -1;
}
void CheckUnsquashfs() {
const std::array<const char*, 3> ExecveArgs = {
"unsquashfs",
"--help",
nullptr,
};
int fd = ::syscall(SYS_memfd_create, "stdout", 0);
int32_t Result = Exec::ExecAndWaitForResponseRedirect(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()), fd, fd);
Has_Unsquashfs = Result != -1;
if (Has_Unsquashfs) {
// Seek back to the start
lseek(fd, 0, SEEK_SET);
// Unsquashfs needs to support zstd
// Scan its output to find the zstd compressor
FILE* fp = fdopen(fd, "r");
char* Line {nullptr};
size_t Len;
bool ReadingDecompressors = false;
bool SupportsZSTD = false;
while (getline(&Line, &Len, fp) != -1) {
if (!ReadingDecompressors) {
if (strstr(Line, "Decompressors available")) {
ReadingDecompressors = true;
}
} else {
if (strstr(Line, "zstd")) {
SupportsZSTD = true;
}
}
}
free(Line);
fclose(fp);
// Disable unsquashfs if it doesn't support ZSTD
if (!SupportsZSTD) {
Has_Unsquashfs = false;
}
}
close(fd);
}
void CheckZenity() {
// Check if zenity exists on the host
std::array<const char*, 3> ExecveArgs = {
"zenity",
"-h",
nullptr,
};
int32_t Result = Exec::ExecAndWaitForResponseRedirect(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()), -1, -1);
Has_Zenity = Result != -1;
}
// EroFS specific tests
void CheckEroFSFuse() {
std::array<const char*, 3> ExecveArgs = {
"erofsfuse",
"--help",
nullptr,
};
int32_t Result = Exec::ExecAndWaitForResponseRedirect(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()), -1, -1);
Has_EroFSFuse = Result != -1;
}
void CheckEroFSFsck() {
std::array<const char*, 3> ExecveArgs = {
"fsck.erofs",
"-V",
nullptr,
};
int32_t Result = Exec::ExecAndWaitForResponseRedirect(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()), -1, -1);
Has_EroFSFsck = Result != -1;
}
void Init() {
CheckCurl();
CheckSquashfuse();
CheckUnsquashfs();
CheckZenity();
CheckEroFSFuse();
CheckEroFSFsck();
}
} // namespace WorkingAppsTester
namespace DistroQuery {
struct DistroInfo {
std::string DistroName;
std::string DistroVersion;
bool RollingRelease;
bool Unknown;
};
DistroInfo GetDistroInfo() {
// Detect these files in order
//
// /etc/lsb-release
// eg:
// DISTRIB_ID=Ubuntu
// DISTRIB_RELEASE=21.10
// DISTRIB_CODENAME=impish
// DISTRIB_DESCRIPTION="Ubuntu 21.10"
//
// /etc/os-release
// eg:
// PRETTY_NAME="Ubuntu 21.10"
// NAME="Ubuntu"
// VERSION_ID="21.10"
// VERSION="21.10 (Impish Indri)"
// VERSION_CODENAME=impish
// ID=ubuntu
// ID_LIKE=debian
// HOME_URL="https://www.ubuntu.com/"
// SUPPORT_URL="https://help.ubuntu.com/"
// BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
// PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
// UBUNTU_CODENAME=impish
//
// /etc/debian_version
// eg:
// 11.0
//
// uname -r
// eg:
// 5.13.0-22-generic
DistroInfo Info {};
uint32_t FoundCount {};
if (std::filesystem::exists("/etc/lsb-release")) {
std::fstream File("/etc/lsb-release", std::fstream::in);
std::string Line;
while (std::getline(File, Line)) {
if (File.eof() || FoundCount == 2) {
break;
}
std::stringstream ss(Line);
std::string Key, Value;
std::getline(ss, Key, '=');
std::getline(ss, Value, '=');
if (Key == "DISTRIB_ID") {
auto ToLower = [](auto Str) {
std::transform(Str.begin(), Str.end(), Str.begin(), [](unsigned char c) { return std::tolower(c); });
return Str;
};
Info.DistroName = ToLower(Value);
++FoundCount;
} else if (Key == "DISTRIB_RELEASE") {
Info.DistroVersion = std::move(Value);
++FoundCount;
}
}
}
if (FoundCount == 2) {
Info.Unknown = false;
if (Info.DistroName == "arch") {
Info.RollingRelease = true;
}
return Info;
}
FoundCount = 0;
if (std::filesystem::exists("/etc/os-release")) {
std::fstream File("/etc/os-release", std::fstream::in);
std::string Line;
while (std::getline(File, Line)) {
if (File.eof() || FoundCount == 2) {
break;
}
std::stringstream ss(Line);
std::string Key, Value;
std::getline(ss, Key, '=');
std::getline(ss, Value, '=');
if (Key == "ID") {
Info.DistroName = std::move(Value);
++FoundCount;
} else if (Key == "VERSION_ID") {
// Ubuntu provides VERSION_ID
// Strip the two quotes from the VERSION_ID
Value = Value.substr(1, Value.size() - 2);
Info.DistroVersion = std::move(Value);
++FoundCount;
} else if (Key == "IMAGE_VERSION") {
// Arch provides IMAGE_VERSION
Info.DistroVersion = std::move(Value);
++FoundCount;
}
}
}
if (FoundCount == 2) {
Info.Unknown = false;
if (Info.DistroName == "arch") {
Info.RollingRelease = true;
}
return Info;
}
FoundCount = 0;
if (std::filesystem::exists("/etc/debian_version")) {
std::fstream File("/etc/debian_version", std::fstream::in);
std::string Line;
Info.DistroName = "debian";
++FoundCount;
while (std::getline(File, Line)) {
Info.DistroVersion = Line;
++FoundCount;
}
}
if (FoundCount == 2) {
Info.Unknown = false;
return Info;
}
Info.DistroName = "Unknown";
Info.DistroVersion = {};
Info.Unknown = true;
return Info;
}
} // namespace DistroQuery
namespace WebFileFetcher {
struct FileTargets {
// These two are for matching version checks
std::string DistroMatch;
std::string VersionMatch;
// This is a human readable name
std::string DistroName;
// This is the URL
fextl::string URL;
// This is the hash of the file
std::string Hash;
// FileType
enum class FileType {
TYPE_UNKNOWN,
TYPE_SQUASHFS,
TYPE_EROFS,
};
FileType Type;
};
const static std::string DownloadURL = "https://rootfs.fex-emu.gg/RootFS_links.json";
std::string DownloadToString(const std::string& URL) {
std::array<const char*, 3> ExecveArgs = {
"curl",
URL.c_str(),
nullptr,
};
return Exec::ExecAndWaitForResponseText(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()));
}
bool DownloadToPath(const fextl::string& URL, const fextl::string& Path) {
auto filename = URL.substr(URL.find_last_of('/') + 1);
auto PathName = Path + filename;
std::array<const char*, 5> ExecveArgs = {
"curl", URL.c_str(), "-o", PathName.c_str(), nullptr,
};
return Exec::ExecAndWaitForResponse(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data())) == 0;
}
bool DownloadToPathWithZenityProgress(const fextl::string& URL, const fextl::string& Path) {
auto filename = URL.substr(URL.find_last_of('/') + 1);
auto PathName = Path + filename;
// -# for progress bar
// -o for output file
// -f for silent fail
std::string CurlPipe = fmt::format("curl -C - -#f {} -o {} 2>&1", URL, PathName);
const std::string StdBuf = "stdbuf -oL tr '\\r' '\\n'";
const std::string SedBuf = "sed -u 's/[^0-9]*\\([0-9]*\\).*/\\1/'";
// zenity --auto-close can't be used since `curl -C` for whatever reason prints 100% at the start.
// Making zenity vanish immediately
const std::string ZenityBuf = "zenity --time-remaining --progress --no-cancel --title 'Downloading'";
std::string BigArgs = fmt::format("{} | {} | {} | {}", CurlPipe, StdBuf, SedBuf, ZenityBuf);
std::array<const char*, 4> ExecveArgs = {
"/bin/sh",
"-c",
BigArgs.c_str(),
nullptr,
};
return Exec::ExecAndWaitForResponse(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data())) == 0;
}
std::optional<std::vector<FileTargets>> GetRootFSLinks() {
// Decode the filetargets
std::string Data = DownloadToString(DownloadURL);
if (Data.empty()) {
return std::nullopt;
}
FEX::JSON::JsonAllocator Pool {};
const json_t* json = FEX::JSON::CreateJSON(Data, Pool);
if (!json) {
fmt::print(stderr, "Failed to parse JSON from RootFSLinks file '{}' - invalid JSON format", Data);
std::abort();
}
const json_t* RootList = json_getProperty(json, "v1");
if (!RootList) {
fprintf(stderr, "Couldn't get root list");
return {};
}
std::vector<FileTargets> Targets;
for (const json_t* RootItem = json_getChild(RootList); RootItem != nullptr; RootItem = json_getSibling(RootItem)) {
FileTargets Target {};
Target.DistroName = json_getName(RootItem);
for (const json_t* DataItem = json_getChild(RootItem); DataItem != nullptr; DataItem = json_getSibling(DataItem)) {
auto DataName = std::string_view {json_getName(DataItem)};
if (DataName == "DistroMatch") {
Target.DistroMatch = json_getValue(DataItem);
} else if (DataName == "DistroVersion") {
Target.VersionMatch = json_getValue(DataItem);
} else if (DataName == "URL") {
Target.URL = json_getValue(DataItem);
} else if (DataName == "Hash") {
Target.Hash = json_getValue(DataItem);
} else if (DataName == "Type") {
auto DataValue = std::string_view {json_getValue(DataItem)};
if (DataValue == "squashfs") {
Target.Type = FileTargets::FileType::TYPE_SQUASHFS;
} else if (DataValue == "erofs") {
Target.Type = FileTargets::FileType::TYPE_EROFS;
} else {
Target.Type = FileTargets::FileType::TYPE_UNKNOWN;
}
}
}
bool SupportsSquashFS = WorkingAppsTester::Has_Squashfuse || WorkingAppsTester::Has_Unsquashfs;
bool SupportsEroFS = WorkingAppsTester::Has_EroFSFuse;
if ((Target.Type == FileTargets::FileType::TYPE_SQUASHFS && SupportsSquashFS) ||
(Target.Type == FileTargets::FileType::TYPE_EROFS && SupportsEroFS)) {
// If we don't understand the type, then we can't use this.
// Additionally if the type is erofs but the user doesn't have erofsfuse, then we can't use this
Targets.emplace_back(Target);
}
}
return Targets;
}
} // namespace WebFileFetcher
namespace Zenity {
bool ExecWithQuestion(const fextl::string& Question) {
fextl::string TextArg = "--text=" + Question;
const char* Args[] = {
"zenity",
"--question",
TextArg.c_str(),
nullptr,
};
int32_t Result = Exec::ExecAndWaitForResponse(Args[0], const_cast<char* const*>(Args));
// 0 on Yes, 1 on No
return Result == 0;
}
void ExecWithInfo(const fextl::string& Text) {
fextl::string TextArg = "--text=" + Text;
const char* Args[] = {
"zenity",
"--info",
TextArg.c_str(),
nullptr,
};
Exec::ExecAndWaitForResponse(Args[0], const_cast<char* const*>(Args));
}
bool AskForConfirmation(const fextl::string& Question) {
return ArgOptions::AssumeYes || ExecWithQuestion(Question);
}
int32_t AskForConfirmationList(const fextl::string& Text, const std::span<const fextl::string> Arguments) {
fextl::string TextArg = "--text=" + Text;
std::vector<const char*> ExecveArgs = {
"zenity", "--list", TextArg.c_str(), "--hide-header", "--column=Index", "--column=Text", "--hide-column=1",
};
std::vector<fextl::string> NumberArgs;
for (size_t i = 0; i < Arguments.size(); ++i) {
NumberArgs.emplace_back(std::to_string(i));
}
for (size_t i = 0; i < Arguments.size(); ++i) {
const auto& Arg = Arguments[i];
ExecveArgs.emplace_back(NumberArgs[i].c_str());
ExecveArgs.emplace_back(Arg.c_str());
}
ExecveArgs.emplace_back(nullptr);
auto Result = Exec::ExecAndWaitForResponseText(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()));
if (Result.empty()) {
return -1;
}
return std::stoi(Result);
}
int32_t AskForComplexConfirmationList(const std::string& Text, const std::span<const std::string> Arguments) {
std::string TextArg = "--text=" + Text;
std::vector<const char*> ExecveArgs = {
"zenity",
"--list",
TextArg.c_str(),
};
for (auto& Arg : Arguments) {
ExecveArgs.emplace_back(Arg.c_str());
}
ExecveArgs.emplace_back(nullptr);
auto Result = Exec::ExecAndWaitForResponseText(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data()));
if (Result.empty()) {
return -1;
}
return std::stoi(Result);
}
int32_t AskForDistroSelection(DistroQuery::DistroInfo& Info, const std::span<const WebFileFetcher::FileTargets> Targets) {
// Search for an exact match
int32_t DistroIndex = -1;
if (!Info.Unknown) {
for (size_t i = 0; i < Targets.size(); ++i) {
const auto& Target = Targets[i];
bool ExactMatch = Target.DistroMatch == Info.DistroName && (Info.RollingRelease || Target.VersionMatch == Info.DistroVersion);
if (ExactMatch) {
fextl::string Question = fextl::fmt::format("Found exact match for distro '{}'. Do you want to select this image?", Target.DistroName);
if (ExecWithQuestion(Question)) {
DistroIndex = i;
break;
}
}
}
}
if (DistroIndex != -1) {
return DistroIndex;
}
if (ArgOptions::DistroListOption == ArgOptions::ListQueryOption::OPTION_FIRST) {
// Return the first option if not an exact match.
return 0;
}
std::vector<std::string> Args;
Args.emplace_back("--column=Index");
Args.emplace_back("--column=Distro");
Args.emplace_back("--hide-column=1");
for (size_t i = 0; i < Targets.size(); ++i) {
const auto& Target = Targets[i];
Args.emplace_back(std::to_string(i));
Args.emplace_back(Target.DistroName);
}
std::string Text = "RootFS list selection";
return AskForComplexConfirmationList(Text, Args);
}
bool ValidateCheckExists(const WebFileFetcher::FileTargets& Target) {
fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/";
auto filename = Target.URL.substr(Target.URL.find_last_of('/') + 1);
auto PathName = RootFS + filename;
uint64_t ExpectedHash = std::stoul(Target.Hash, nullptr, 16);
std::error_code ec;
if (std::filesystem::exists(PathName, ec)) {
const std::array<const fextl::string, 2> Args {
"Overwrite",
"Validate",
};
fextl::string Text = filename + " already exists. What do you want to do?";
int Result = AskForConfirmationList(Text, Args);
if (Result == -1) {
return false;
}
auto Res = XXFileHash::HashFile(PathName);
if (Result == 0) {
if (Res.value_or(0) == ExpectedHash) {
fextl::string Text = fextl::fmt::format("{} matches expected hash. Skipping download", filename);
ExecWithInfo(Text);
return false;
}
} else if (Result == 1) {
if (Res.value_or(0) != ExpectedHash) {
return AskForConfirmation("RootFS doesn't match hash!\nDo you want to redownload?");
} else {
fextl::string Text = fextl::fmt::format("{} matches expected hash", filename);
ExecWithInfo(Text);
return false;
}
}
}
return true;
}
bool ValidateDownloadSelection(const WebFileFetcher::FileTargets& Target) {
fextl::string Text = fextl::fmt::format("Selected Rootfs: {}\n", Target.DistroName);
Text += fmt::format("\tURL: {}\n", Target.URL);
Text += fmt::format("Are you sure that you want to download this image");
if (AskForConfirmation(Text)) {
fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/";
std::error_code ec {};
if (!std::filesystem::exists(RootFS, ec)) {
// Doesn't exist, create the the folder as a user convenience
if (!std::filesystem::create_directories(RootFS, ec)) {
// Well I guess we failed
Text = fmt::format("Couldn't create {} path for storing RootFS", RootFS);
ExecWithInfo(Text);
return false;
}
}
if (!WebFileFetcher::DownloadToPathWithZenityProgress(Target.URL, RootFS)) {
return false;
}
return true;
}
return false;
}
} // namespace Zenity
namespace TTY {
bool AskForConfirmation(const fextl::string& Question) {
if (ArgOptions::AssumeYes) {
return true;
}
auto ToLowerInPlace = [](auto& Str) {
std::transform(Str.begin(), Str.end(), Str.begin(), [](unsigned char c) { return std::tolower(c); });
};
std::cout << Question << std::endl;
std::cout << "Response {y,yes,1} or {n,no,0}" << std::endl;
std::string Response;
std::cin >> Response;
ToLowerInPlace(Response);
if (Response == "y" || Response == "yes" || Response == "1") {
return true;
} else if (Response == "n" || Response == "no" || Response == "0") {
return false;
} else {
std::cout << "Unknown response. Assuming no" << std::endl;
return false;
}
}
void ExecWithInfo(const fextl::string& Text) {
std::cout << Text << std::endl;
}
int32_t AskForConfirmationList(const fextl::string& Text, std::span<const fextl::string> List) {
fmt::print("{}\n", Text);
fmt::print("Options:\n");
fmt::print("\t0: Cancel\n");
for (size_t i = 0; i < List.size(); ++i) {
fmt::print("\t{}: {}\n", i + 1, List[i]);
}
fmt::print("\t\nResponse {{1-{}}} or 0 to cancel\n", List.size());
fextl::string Response;
std::cin >> Response;
int32_t ResponseInt = std::stol(Response.data(), nullptr, 0);
if (ResponseInt == 0) {
return -1;
} else if (ResponseInt >= 1 && (ResponseInt - 1) < List.size()) {
return ResponseInt - 1;
} else {
std::cout << "Unknown response. Assuming cancel" << std::endl;
return -1;
}
}
int32_t AskForDistroSelection(DistroQuery::DistroInfo& Info, const std::span<const WebFileFetcher::FileTargets> Targets) {
// Search for an exact match
int32_t DistroIndex = -1;
if (!Info.Unknown) {
for (size_t i = 0; i < Targets.size(); ++i) {
const auto& Target = Targets[i];
bool ExactMatch = Target.DistroMatch == Info.DistroName && Target.VersionMatch == Info.DistroVersion;
if (ExactMatch) {
fextl::string Question = fextl::fmt::format("Found exact match for distro '{}'. Do you want to select this image?", Target.DistroName);
if (AskForConfirmation(Question)) {
DistroIndex = i;
break;
}
}
}
}
if (DistroIndex != -1) {
return DistroIndex;
}
if (ArgOptions::DistroListOption == ArgOptions::ListQueryOption::OPTION_FIRST) {
// Return the first option if not an exact match.
return 0;
}
std::vector<fextl::string> Args;
for (size_t i = 0; i < Targets.size(); ++i) {
const auto& Target = Targets[i];
Args.emplace_back(Target.DistroName);
}
fextl::string Text = "RootFS list selection";
return AskForConfirmationList(Text, Args);
}
bool ValidateCheckExists(const WebFileFetcher::FileTargets& Target) {
fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/";
auto filename = Target.URL.substr(Target.URL.find_last_of('/') + 1);
auto PathName = RootFS + filename;
uint64_t ExpectedHash = std::stoul(Target.Hash, nullptr, 16);
std::error_code ec;
if (std::filesystem::exists(PathName, ec)) {
const std::array<fextl::string, 2> Args {
"Overwrite",
"Validate",
};
fextl::string Text = filename + " already exists. What do you want to do?";
int Result = AskForConfirmationList(Text, Args);
if (Result == -1) {
return false;
}
fmt::print("Validating RootFS hash...\n");
auto Res = XXFileHash::HashFile(PathName);
if (Result == 0) {
if (Res.value_or(0) == ExpectedHash) {
fmt::print("{} matches expected hash. Skipping downloading\n", filename);
return false;
}
} else if (Result == 1) {
if (Res.value_or(0) != ExpectedHash) {
fmt::print("RootFS doesn't match hash!\n");
return AskForConfirmation("Do you want to redownload?");
} else {
fmt::print("{} matches expected hash\n", filename);
return false;
}
}
}
return true;
}
bool ValidateDownloadSelection(const WebFileFetcher::FileTargets& Target) {
fmt::print("Selected Rootfs: {}\n", Target.DistroName);
fmt::print("\tURL: {}\n", Target.URL);
if (AskForConfirmation("Are you sure that you want to download this image")) {
fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/";
std::error_code ec {};
if (!std::filesystem::exists(RootFS, ec)) {
// Doesn't exist, create the the folder as a user convenience
if (!std::filesystem::create_directories(RootFS, ec)) {
// Well I guess we failed
fmt::print("Couldn't create {} path for storing RootFS\n", RootFS);
return false;
}
}
auto DoDownload = [&Target, &RootFS]() -> bool {
if (!WebFileFetcher::DownloadToPath(Target.URL, RootFS)) {
fmt::print("Couldn't download RootFS\n");
return false;
}
return true;
};
while (DoDownload() == false) {
if (AskForConfirmation("Curl RootFS download failed. Do you want to retry?")) {
// Loop to retry
} else {
return false;
}
}
// Got here then we passed
return true;
}
return false;
}
} // namespace TTY
namespace {
std::function<bool(const fextl::string& Question)> _AskForConfirmation;
std::function<void(const fextl::string& Text)> _ExecWithInfo;
std::function<int32_t(const fextl::string& Text, const std::span<const fextl::string> List)> _AskForConfirmationList;
std::function<int32_t(DistroQuery::DistroInfo& Info, const std::span<const WebFileFetcher::FileTargets> Targets)> _AskForDistroSelection;
std::function<bool(const WebFileFetcher::FileTargets& Target)> _ValidateCheckExists;
std::function<bool(const WebFileFetcher::FileTargets& Target)> _ValidateDownloadSelection;
void CheckTTY() {
bool IsTTY {};
if (ArgOptions::UIOption == ArgOptions::UIOverrideOption::Default) {
IsTTY = isatty(STDOUT_FILENO);
} else {
IsTTY = ArgOptions::UIOption == ArgOptions::UIOverrideOption::TTY;
}
if (!WorkingAppsTester::Has_Zenity) {
// Force TTY if zenity isn't installed.
if (ArgOptions::UIOption == ArgOptions::UIOverrideOption::Zenity) {
fmt::print("Zenity isn't executable. Falling back to TTY mode\n");
}
IsTTY = true;
}
if (IsTTY) {
_AskForConfirmation = TTY::AskForConfirmation;
_ExecWithInfo = TTY::ExecWithInfo;
_AskForConfirmationList = TTY::AskForConfirmationList;
_AskForDistroSelection = TTY::AskForDistroSelection;
_ValidateCheckExists = TTY::ValidateCheckExists;
_ValidateDownloadSelection = TTY::ValidateDownloadSelection;
} else {
_AskForConfirmation = Zenity::AskForConfirmation;
_ExecWithInfo = Zenity::ExecWithInfo;
_AskForConfirmationList = Zenity::AskForConfirmationList;
_AskForDistroSelection = Zenity::AskForDistroSelection;
_ValidateCheckExists = Zenity::ValidateCheckExists;
_ValidateDownloadSelection = Zenity::ValidateDownloadSelection;
}
}
bool AskForConfirmation(const fextl::string& Question) {
return _AskForConfirmation(Question);
}
void ExecWithInfo(const fextl::string& Text) {
_ExecWithInfo(Text);
}
int32_t AskForConfirmationList(const fextl::string& Text, const std::span<const fextl::string> Arguments) {
return _AskForConfirmationList(Text, Arguments);
}
int32_t AskForDistroSelection(const std::span<const WebFileFetcher::FileTargets> Targets) {
auto Info = DistroQuery::GetDistroInfo();
if (!ArgOptions::DistroName.empty()) {
Info.DistroName = ArgOptions::DistroName;
}
if (!ArgOptions::DistroVersion.empty()) {
Info.DistroVersion = ArgOptions::DistroVersion;
}
return _AskForDistroSelection(Info, Targets);
}
bool ValidateCheckExists(const WebFileFetcher::FileTargets& Target) {
return _ValidateCheckExists(Target);
}
bool ValidateDownloadSelection(const WebFileFetcher::FileTargets& Target) {
return _ValidateDownloadSelection(Target);
}
} // namespace
namespace ConfigSetter {
void SetRootFSAsDefault(const fextl::string& RootFS) {
fextl::string Filename = FEXCore::Config::GetConfigFileLocation();
auto LoadedConfig = FEX::Config::CreateMainLayer(&Filename);
LoadedConfig->Load();
LoadedConfig->Set(FEXCore::Config::ConfigOption::CONFIG_ROOTFS, RootFS);
FEX::Config::SaveLayerToJSON(Filename, LoadedConfig.get());
}
} // namespace ConfigSetter
namespace UnSquash {
bool UnsquashRootFS(const fextl::string& Path, const fextl::string& RootFS, const fextl::string& FolderName) {
auto TargetFolder = Path + FolderName;
std::error_code ec;
if (std::filesystem::exists(TargetFolder, ec)) {
fextl::string Question = "Target folder \"" + FolderName + "\" already exists. Overwrite?";
if (AskForConfirmation(Question)) {
if (std::filesystem::remove_all(TargetFolder, ec) == ~0ULL) {
ExecWithInfo("Couldn't remove previous directory. Won't extract.");
return false;
}
} else {
return false;
}
}
const std::array<const char*, 6> ExecveArgs = {
"unsquashfs", "-f", "-d", TargetFolder.c_str(), RootFS.c_str(), nullptr,
};
return Exec::ExecAndWaitForResponse(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data())) == 0;
}
bool ExtractEroFS(const fextl::string& Path, const fextl::string& RootFS, const fextl::string& FolderName) {
auto TargetFolder = Path + FolderName;
std::error_code ec;
if (std::filesystem::exists(TargetFolder, ec)) {
fextl::string Question = "Target folder \"" + FolderName + "\" already exists. Overwrite?";
if (AskForConfirmation(Question)) {
if (std::filesystem::remove_all(TargetFolder, ec) == ~0ULL) {
ExecWithInfo("Couldn't remove previous directory. Won't extract.");
return false;
}
} else {
return false;
}
}
ExecWithInfo("Extracting Erofs. This might take a few minutes.");
const auto ExtractOption = fmt::format("--extract={}", TargetFolder);
const std::array<const char*, 4> ExecveArgs = {
"fsck.erofs",
ExtractOption.c_str(),
RootFS.c_str(),
nullptr,
};
return Exec::ExecAndWaitForResponse(ExecveArgs[0], const_cast<char* const*>(ExecveArgs.data())) == 0;
}
} // namespace UnSquash
int main(int argc, char** argv, char** const envp) {
auto ArgsLoader = fextl::make_unique<FEX::ArgLoader::ArgLoader>(FEX::ArgLoader::ArgLoader::LoadType::WITHOUT_FEXLOADER_PARSER, argc, argv);
FEX::Config::LoadConfig(std::move(ArgsLoader), {}, envp);
// Reload the meta layer
FEXCore::Config::ReloadMetaLayer();
ArgOptions::ParseArguments(argc, argv);
WorkingAppsTester::Init();
CheckTTY();
if (ArgOptions::RemainingArgs.size()) {
auto Res = XXFileHash::HashFile(ArgOptions::RemainingArgs[0]);
if (Res.has_value()) {
fmt::print("{} has hash: {:x}\n", ArgOptions::RemainingArgs[0], Res.value());
} else {
fmt::print("Couldn't generate hash for {}\n", ArgOptions::RemainingArgs[0]);
}
return 0;
}
// Check if curl exists on the host
if (!WorkingAppsTester::Has_Curl) {
ExecWithInfo("curl is required to use this tool. Please install curl before using.");
return -1;
}
if (!WorkingAppsTester::Has_Squashfuse && !WorkingAppsTester::Has_Unsquashfs && !WorkingAppsTester::Has_EroFSFuse) {
// We need at least one tool to mount or extract image files
ExecWithInfo("squashfuse, unsquashfs, or erofsfuse is required to use this tool. Please install one before using.");
return -1;
}
FEX_CONFIG_OPT(LDPath, ROOTFS);
std::error_code ec;
fextl::string Question {};
if (LDPath().empty() || std::filesystem::exists(LDPath(), ec) == false) {
Question = "RootFS not found. Do you want to try and download one?";
} else {
Question = "RootFS is already in use. Do you want to check the download list?";
}
if (AskForConfirmation(Question)) {
auto TargetReturn = WebFileFetcher::GetRootFSLinks();
if (!TargetReturn.has_value()) {
ExecWithInfo("Couldn't download rootfs list from the server. Try again in a minute or report on the fex-emu issue tracker.");
return -1;
}
auto Targets = TargetReturn.value();
if (Targets.empty()) {
ExecWithInfo("Couldn't parse rootfs definition URL.");
return -1;
}
int32_t DistroIndex = AskForDistroSelection(Targets);
if (DistroIndex != -1) {
const auto& Target = Targets[DistroIndex];
fextl::string RootFS = FEXCore::Config::GetDataDirectory() + "RootFS/";
auto filename = Target.URL.substr(Target.URL.find_last_of('/') + 1);
auto PathName = RootFS + filename;
if (!ValidateCheckExists(Target)) {
// Keep going
} else {
auto ValidateDownload = [&Target, &PathName]() -> std::pair<int32_t, bool> {
std::error_code ec;
if (ValidateDownloadSelection(Target)) {
uint64_t ExpectedHash = std::stoul(Target.Hash, nullptr, 16);
if (std::filesystem::exists(PathName, ec)) {
auto Res = XXFileHash::HashFile(PathName);
if (Res.value_or(0) != ExpectedHash) {
fextl::string Text = fextl::fmt::format("Couldn't hash the rootfs or hash didn't match\n");
Text += fmt::format("Hash {:x} != Expected Hash {:x}\n", Res.value_or(0), ExpectedHash);
ExecWithInfo(Text);
return std::make_pair(-1, true);
}
} else {
ExecWithInfo("Correctly downloaded RootFS but doesn't exist?");
return std::make_pair(-1, false);
}
} else {
ExecWithInfo("Couldn't download rootfs for some reason.");
return std::make_pair(-1, false);
}
return std::make_pair(0, false);
};
std::pair<int32_t, bool> Result {};
while ((Result = ValidateDownload()).second == true && Result.first == -1) {
if (AskForConfirmation("Do you want to try downloading the RootFS again?")) {
// Continue the loop
} else {
// Didn't want to retry, just exit now
return Result.first;
}
}
// Early exit on other errors
if (Result.first == -1 && Result.second == false) {
return Result.first;
}
}
struct ExtractStrings {
const char* ExtractOrAsIs;
const char* AsIsSinceMounterNonFunctional;
const char* AsIsSinceExtractorNonFunctional;
const char* AsIsSinceNothingWorks;
};
ArgOptions::CompressedImageOption UseImageAs {ArgOptions::CompressedUsageOption};
bool HasExtractor {};
bool HasMounter {};
std::function<bool(const fextl::string& Path, const fextl::string& RootFS, const fextl::string& FolderName)> ExtractHelper;
ExtractStrings ExtractingStrings;
if (Target.Type == WebFileFetcher::FileTargets::FileType::TYPE_SQUASHFS) {
HasExtractor = WorkingAppsTester::Has_Unsquashfs;
HasMounter = WorkingAppsTester::Has_Squashfuse;
ExtractHelper = UnSquash::UnsquashRootFS;
ExtractingStrings = {
"Do you wish to extract the squashfs file or use it as-is?",
"Squashfuse doesn't work. Do you wish to extract the squashfs file?",
"Unsquashfs doesn't work. Do you want to use the squashfs file as-is?",
"Unsquashfs and squashfuse isn't working. Leaving rootfs as-is",
};
} else if (Target.Type == WebFileFetcher::FileTargets::FileType::TYPE_EROFS) {
HasExtractor = WorkingAppsTester::Has_EroFSFsck;
HasMounter = WorkingAppsTester::Has_EroFSFuse;
ExtractHelper = UnSquash::ExtractEroFS;
ExtractingStrings = {
"Do you wish to extract the erofs file or use it as-is?",
"erofsfuse doesn't work. Do you wish to extract the erofs file?",
"Extracting erofs doesn't work. Do you want to use the erofs file as-is?",
"Extracting erofs and erofsfuse isn't working. Leaving rootfs as-is",
};
}
int32_t Result {};
std::vector<fextl::string> Args = {
"Extract",
"As-Is",
};
if (UseImageAs == ArgOptions::CompressedImageOption::OPTION_ASK) {
if (HasExtractor) {
if (HasMounter) {
Result = AskForConfirmationList(ExtractingStrings.ExtractOrAsIs, Args);
if (Result == 0) {
UseImageAs = ArgOptions::CompressedImageOption::OPTION_EXTRACT;
} else if (Result == 1) {
UseImageAs = ArgOptions::CompressedImageOption::OPTION_ASIS;
}
} else {
Args.pop_back();
Result = AskForConfirmationList(ExtractingStrings.AsIsSinceMounterNonFunctional, Args);
if (Result == 0) {
UseImageAs = ArgOptions::CompressedImageOption::OPTION_EXTRACT;
}
}
} else {
if (HasMounter) {
Args.erase(Args.begin());
Result = AskForConfirmationList(ExtractingStrings.AsIsSinceExtractorNonFunctional, Args);
if (Result == 0) {
// We removed an argument, Just change "As-Is" from 0 to 1 for later logic to work
UseImageAs = ArgOptions::CompressedImageOption::OPTION_ASIS;
}
} else {
Args.erase(Args.begin());
ExecWithInfo(ExtractingStrings.AsIsSinceNothingWorks);
UseImageAs = ArgOptions::CompressedImageOption::OPTION_ASIS;
}
}
}
if (UseImageAs == ArgOptions::CompressedImageOption::OPTION_EXTRACT) {
auto FolderName = filename.substr(0, filename.find_last_of('.'));
if (ExtractHelper(RootFS, PathName, FolderName)) {
// Remove the image file suffix since we extracted to that.
filename = std::move(FolderName);
}
}
if (AskForConfirmation("Do you wish to set this RootFS as default?")) {
ConfigSetter::SetRootFSAsDefault(filename);
fextl::string Text = fextl::fmt::format("{} set as default RootFS\n", filename);
ExecWithInfo(Text);
}
}
}
return 0;
}