#include "UpdateChecker.h" #include "Config.hpp" #include "../../../McpPlugin/json.hpp" #ifndef __ANDROID__ #include #else #include #include #endif #include #include #include #include #include #include #include #include #include namespace casioemu { namespace { constexpr const char* kApi = "https://api.github.com/repos/telecomadm1145/CasioEmuMsvc/releases?per_page=1"; constexpr const char* kReleaseBase = "https://github.com/telecomadm1145/CasioEmuMsvc/releases/tag/"; constexpr const char* kCachePath = "update-check.json"; constexpr auto kCacheLifetime = std::chrono::hours(1); constexpr std::size_t kMaxResponseSize = 1024 * 1024; size_t Write(void* p, size_t s, size_t n, void* u) { const auto bytes = s * n; auto& body = *static_cast(u); if (bytes > kMaxResponseSize || body.size() > kMaxResponseSize - bytes) return 0; body.append(static_cast(p), bytes); return bytes; } bool SetTrustedReleaseUrl(UpdateInfo& release) { constexpr std::string_view prefix = "stable-"; if (release.tag.size() < prefix.size() + 14 + 1 + 7 || release.tag.size() > prefix.size() + 14 + 1 + 40 || release.tag.compare(0, prefix.size(), prefix) != 0) return false; const std::size_t timestamp_begin = prefix.size(); const std::size_t separator = timestamp_begin + 14; for (std::size_t i = timestamp_begin; i < separator; ++i) if (release.tag[i] < '0' || release.tag[i] > '9') return false; if (release.tag[separator] != '-') return false; for (std::size_t i = separator + 1; i < release.tag.size(); ++i) { const char ch = release.tag[i]; if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'))) return false; } release.url = std::string(kReleaseBase) + release.tag; return true; } UpdateInfo EvaluateRelease(UpdateInfo release) { SDL_Log("[UpdateChecker] Local commit=%s ref=%s remote tag=%s", GIT_COMMIT_HASH, GIT_LATEST_TAG, release.tag.c_str()); if (release.tag.empty() || release.tag == GIT_LATEST_TAG || release.tag.find(GIT_COMMIT_HASH) != std::string::npos) { SDL_Log("[UpdateChecker] No update available"); return {}; } SDL_Log("[UpdateChecker] Update available: %s", release.tag.c_str()); return release; } std::string LocalDate(); std::optional LoadCachedRelease() { std::ifstream input{kCachePath, std::ios::binary}; if (!input) return std::nullopt; try { const auto cache = nlohmann::json::parse(input); const auto checked_at = cache.value("checked_at", std::int64_t{}); const auto now = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); if (checked_at <= 0 || now < checked_at || now - checked_at >= std::chrono::duration_cast(kCacheLifetime).count()) { SDL_Log("[UpdateChecker] Cached result expired"); return std::nullopt; } UpdateInfo release{cache.value("tag", ""), {}, cache.value("title", ""), cache.value("notes", "")}; if (!SetTrustedReleaseUrl(release)) { SDL_Log("[UpdateChecker] Ignoring cache with invalid release tag"); return std::nullopt; } SDL_Log("[UpdateChecker] Using cached release result: %s", release.tag.c_str()); if (!release.tag.empty() && cache.value("dismissed_tag", "") == release.tag && cache.value("dismissed_on", "") == LocalDate()) { SDL_Log("[UpdateChecker] Update reminder suppressed for today: %s", release.tag.c_str()); return UpdateInfo{}; } return release; } catch (const std::exception& e) { SDL_Log("[UpdateChecker] Ignoring invalid cache: %s", e.what()); return std::nullopt; } } void SaveCachedRelease(const UpdateInfo& release) { try { const auto checked_at = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); const nlohmann::json cache{{"checked_at", checked_at}, {"tag", release.tag}, {"title", release.title}, {"notes", release.notes}}; std::ofstream output{kCachePath, std::ios::binary | std::ios::trunc}; if (!output) throw std::runtime_error("cannot open cache file"); output << cache.dump(); if (!output) throw std::runtime_error("cannot write cache file"); SDL_Log("[UpdateChecker] Cached release result for 1 hour"); } catch (const std::exception& e) { SDL_Log("[UpdateChecker] Failed to save cache: %s", e.what()); } } std::string LocalDate() { const std::time_t now = std::time(nullptr); std::tm local{}; #ifdef _WIN32 localtime_s(&local, &now); #else localtime_r(&now, &local); #endif char value[11]{}; std::strftime(value, sizeof(value), "%Y-%m-%d", &local); return value; } void SaveDismissal(const std::string& tag) { try { std::ifstream input{kCachePath, std::ios::binary}; if (!input) throw std::runtime_error("cache file does not exist"); auto cache = nlohmann::json::parse(input); cache["dismissed_tag"] = tag; cache["dismissed_on"] = LocalDate(); std::ofstream output{kCachePath, std::ios::binary | std::ios::trunc}; if (!output) throw std::runtime_error("cannot open cache file"); output << cache.dump(); if (!output) throw std::runtime_error("cannot write cache file"); SDL_Log("[UpdateChecker] Suppressed %s for today", tag.c_str()); } catch (const std::exception& e) { SDL_Log("[UpdateChecker] Failed to save dismissal: %s", e.what()); } } UpdateInfo Check() { if (auto cached = LoadCachedRelease()) return EvaluateRelease(std::move(*cached)); std::string body; const char* api = kApi; SDL_Log("[UpdateChecker] Checking releases: %s", api); #ifdef __ANDROID__ auto* env = static_cast(SDL_AndroidGetJNIEnv()); if (!env) throw std::runtime_error("Android runtime unavailable"); jobject activity = static_cast(SDL_AndroidGetActivity()); jclass cls = env->GetObjectClass(activity); jmethodID method = env->GetStaticMethodID(cls, "checkUpdate", "(Ljava/lang/String;)[B"); if (!method) throw std::runtime_error("Android update API unavailable"); jstring url = env->NewStringUTF(api); auto bytes = static_cast(env->CallStaticObjectMethod(cls, method, url)); env->DeleteLocalRef(url); if (!bytes) throw std::runtime_error("release request failed"); const jsize len = env->GetArrayLength(bytes); body.assign(static_cast(len), '\0'); env->GetByteArrayRegion(bytes, 0, len, reinterpret_cast(body.data())); env->DeleteLocalRef(bytes); #else CURL* c = curl_easy_init(); if (!c) throw std::runtime_error("curl init failed"); curl_slist* h = nullptr; h = curl_slist_append(h, "Accept: application/vnd.github+json"); h = curl_slist_append(h, "User-Agent: CasioEmuMsvc-update-checker"); curl_easy_setopt(c, CURLOPT_URL, api); curl_easy_setopt(c, CURLOPT_HTTPHEADER, h); curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, 5000L); curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 10000L); curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, Write); curl_easy_setopt(c, CURLOPT_WRITEDATA, &body); curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); const auto code = curl_easy_perform(c); long status = 0; curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &status); SDL_Log("[UpdateChecker] HTTP result: curl=%d status=%ld bytes=%zu", static_cast(code), status, body.size()); curl_slist_free_all(h); curl_easy_cleanup(c); if (code != CURLE_OK || status < 200 || status >= 300) throw std::runtime_error("release request failed"); #endif const auto releases = nlohmann::json::parse(body); if (!releases.is_array() || releases.empty()) { SDL_Log("[UpdateChecker] No releases returned"); return {}; } const auto& json = releases.front(); const auto string_value = [&json](const char* name) { const auto it = json.find(name); return it != json.end() && it->is_string() ? it->get() : std::string{}; }; UpdateInfo release{string_value("tag_name"), {}, string_value("name"), string_value("body")}; if (!SetTrustedReleaseUrl(release)) { SDL_Log("[UpdateChecker] Ignoring release with invalid tag: %s", release.tag.c_str()); return {}; } SaveCachedRelease(release); return EvaluateRelease(std::move(release)); } } UpdateChecker::UpdateChecker() : state_(std::make_shared()) {} UpdateChecker::~UpdateChecker() = default; void UpdateChecker::Start() { if (started_.exchange(true)) return; auto state = state_; std::thread([state]() { UpdateInfo result; try { result = Check(); } catch (const std::exception& e) { SDL_Log("[UpdateChecker] Check failed: %s", e.what()); } catch (...) { SDL_Log("[UpdateChecker] Check failed: unknown error"); } { std::lock_guard lock(state->mutex); state->result = std::move(result); } state->ready.store(true, std::memory_order_release); }).detach(); } bool UpdateChecker::Ready() const { return state_->ready.load(std::memory_order_acquire); } UpdateInfo UpdateChecker::TakeResult() { std::lock_guard lock(state_->mutex); return state_->result; } void UpdateChecker::DismissForToday(const std::string& tag) const { SaveDismissal(tag); } }