// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include "common/scm_rev.h" #include "net.h" #include "common/logging.h" #include "common/httplib.h" #ifdef YUZU_BUNDLED_OPENSSL #include #endif #define QT_TR_NOOP(x) x namespace Common::Net { std::vector Release::GetPlatformAssets() const { // TODO(crueter): Need better handling for this as a whole. #ifdef NIGHTLY_BUILD std::vector result; boost::algorithm::split(result, tag, boost::is_any_of(".")); if (result.size() != 2) return {}; const auto ref = result.at(1); #else const auto ref = tag; #endif std::vector found_assets; // FIXME: This is mildly inefficient. // Finds assets based on a hierarchy of regex search strings. const auto find_asset = [&found_assets, ref, this](const std::string& name, const std::vector& suffixes) { for (const std::string& asset : assets) { for (const auto& suffix : suffixes) { if (asset.ends_with(suffix)) { const std::string_view asset_sv = asset; const size_t pos = asset_sv.find_last_of('/'); const std::string_view filename = (pos != std::string_view::npos) ? asset_sv.substr(pos + 1) : asset_sv; found_assets.emplace_back(Asset{ .name = name, .url = host, .path = asset, .filename = std::string{filename}, }); return; } } } }; #ifdef _WIN32 #ifdef ARCHITECTURE_x86_64 #ifdef _MSC_VER find_asset("Standard", {"amd64-msvc-standard.exe", "amd64-msvc-standard.zip"}); #else // _MSC_VER find_asset("Standard", {BUILD_ID "-gcc-standard.exe", BUILD_ID "-gcc-standard.zip"}); find_asset("PGO", {BUILD_ID "-clang-pgo.exe", BUILD_ID "-clang-pgo.zip"}); #endif // _MSC_VER #elif defined(ARCHITECTURE_arm64) find_asset("Standard", {"arm64-clang-standard.exe", "arm64-clang-standard.zip"}); find_asset("PGO", {"arm64-clang-pgo.exe", "arm64-clang-pgo.zip"}); #endif // ARCHITECTURE_arm64 #elif defined(__APPLE__) #ifdef ARCHITECTURE_arm64 find_asset("Standard", {".dmg", ".tar.gz"}); #endif // ARCHITECTURE_arm64 #elif defined(__ANDROID__) #ifdef ARCHITECTURE_x86_64 find_asset("Standard", {"chromeos.apk"}); #elif defined(ARCHITECTURE_arm64) #ifdef YUZU_LEGACY find_asset("Standard", {"legacy.apk"}); #elif defined(GENSHIN_SPOOF) find_asset("Standard", {"optimized.apk"}); #else find_asset("Standard", {"standard.apk"}); #endif // GENSHIN_SPOOF #endif // ARCHITECTURE_arm64 #endif // __APPLE__ return found_assets; } static inline u64 ParseIsoTimestamp(const std::string& iso) { if (iso.empty()) return 0; std::string buf = iso; if (buf.back() == 'Z') buf.pop_back(); std::tm tm{}; std::istringstream ss(buf); ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); if (ss.fail()) return 0; #ifdef _WIN32 return static_cast(_mkgmtime(&tm)); #else return static_cast(timegm(&tm)); #endif } std::optional Release::FromJson(const nlohmann::json& json, const std::string& host, const std::string& repo) { Release rel; if (!json.is_object()) return std::nullopt; rel.tag = json.value("tag_name", std::string{}); if (rel.tag.empty()) return std::nullopt; rel.title = json.value("name", rel.tag); rel.id = json.value("id", std::hash{}(rel.title)); rel.published = ParseIsoTimestamp(json.value("published_at", std::string{})); rel.prerelease = json.value("prerelease", false); auto body = json.value("body", rel.title); boost::replace_all(body, "\\r", ""); boost::replace_all(body, "\\n", "\n"); rel.body = body; rel.host = host; const auto release_base = fmt::format("{}/{}/releases", Common::g_build_auto_update_website, repo); const auto fallback_html = fmt::format("{}/tag/{}", release_base, rel.tag); rel.html_url = json.value("html_url", fallback_html); // This is our own "fake" API. if (json.contains("base")) { const auto base = json.value("base", fmt::format("https://{}", Common::g_build_auto_update_api)); rel.base_download_url = fmt::format("{}/{}", base, rel.tag); // Assets are easy :) rel.assets = json.value("assets", std::vector{}); } else { const auto base_download_url = fmt::format("/{}/releases/download/{}", repo, rel.tag); rel.base_download_url = base_download_url; // assets are a bit more complex here. :( std::vector assets; const nlohmann::json& arr = json["assets"]; for (const auto &obj : arr) { const auto url = obj.value("browser_download_url", std::string{}); assets.emplace_back(url); } rel.assets = assets; } return rel; } std::optional Release::FromJson(const std::string_view& json, const std::string& host, const std::string& repo) { try { return FromJson(nlohmann::json::parse(json), host, repo); } catch (std::exception& e) { LOG_WARNING(Common, "Failed to parse JSON: {}", e.what()); } return {}; } std::vector Release::ListFromJson(const nlohmann::json& json, const std::string& host, const std::string& repo) { if (!json.is_array()) return {}; std::vector releases; for (const auto& obj : json) { auto rel = Release::FromJson(obj, host, repo); if (rel) releases.emplace_back(rel.value()); } return releases; } std::vector Release::ListFromJson(const std::string_view& json, const std::string& host, const std::string& repo) { try { return ListFromJson(nlohmann::json::parse(json), host, repo); } catch (std::exception& e) { LOG_WARNING(Common, "Failed to parse JSON: {}", e.what()); } return {}; } std::optional MakeRequest(const std::string& url, const std::string& path) { try { constexpr std::size_t timeout_seconds = 15; std::unique_ptr client = std::make_unique(url); client->set_connection_timeout(timeout_seconds); client->set_read_timeout(timeout_seconds); client->set_write_timeout(timeout_seconds); #ifdef YUZU_BUNDLED_OPENSSL client->load_ca_cert_store(kCert, sizeof(kCert)); #endif if (client == nullptr) { LOG_ERROR(Common, "Invalid URL {}{}", url, path); return {}; } httplib::Request request{ .method = "GET", .path = path, }; client->set_follow_location(true); httplib::Result result = client->send(request); if (!result) { LOG_ERROR(Common, "GET to {}{} returned null", url, path); return {}; } const auto& response = result.value(); if (response.status >= 400) { LOG_ERROR(Common, "GET to {}{} returned error status code: {}", url, path, response.status); return {}; } if (!response.headers.contains("content-type")) { LOG_ERROR(Common, "GET to {}{} returned no content", url, path); return {}; } return response.body; } catch (std::exception& e) { LOG_ERROR(Common, "GET to {}{} failed during update check: {}", url, path, e.what()); return std::nullopt; } } std::vector GetReleases() { const auto body = GetReleasesBody(); if (!body) { LOG_WARNING(Common, "Failed to get stable releases"); return {}; } const std::string_view body_str = body.value(); const auto url = fmt::format("https://{}", Common::g_build_auto_update_stable_api); return Release::ListFromJson(body_str, url, Common::g_build_auto_update_stable_repo); } std::optional GetLatestRelease() { const auto releases_path = Common::g_build_auto_update_api_path; const auto url = fmt::format("https://{}", Common::g_build_auto_update_api); const auto body = MakeRequest(url, releases_path); if (!body) { LOG_WARNING(Common, "Failed to get latest release"); return std::nullopt; } const std::string_view body_str = body.value(); return Release::FromJson(body_str, url, Common::g_build_auto_update_repo); } std::optional GetReleasesBody() { const auto releases_path = fmt::format("/{}/{}/releases", Common::g_build_auto_update_stable_api_path, Common::g_build_auto_update_stable_repo); const auto url = fmt::format("https://{}", Common::g_build_auto_update_stable_api); return MakeRequest(url, releases_path); } } // namespace Common::Net