/* * @Author: DI JUNKUN * @Date: 2025-11-11 * Copyright (c) 2025 by DI JUNKUN, All Rights Reserved. */ #include "version_checker.h" #include #include "rd_log.h" #include #include #include #include #include #include #include #include #include #include namespace crossdesk { static std::string latest_release_date_ = ""; static bool latest_patch_available_ = false; static int latest_patch_ = 0; std::vector SplitVersion(const std::string& ver); namespace { constexpr size_t kMaxInlinePatchDigits = 4; struct ParsedVersion { std::vector numbers; std::string date; bool has_patch = false; int patch = 0; }; bool IsDigit(char c) { return std::isdigit(static_cast(c)) != 0; } bool IsAlphaNumeric(char c) { return std::isalnum(static_cast(c)) != 0; } bool IsAllDigits(const std::string& value) { if (value.empty()) { return false; } for (char c : value) { if (!IsDigit(c)) { return false; } } return true; } bool TryParseNonNegativeInt(const std::string& value, int* result) { if (!IsAllDigits(value)) { return false; } try { const long long parsed = std::stoll(value); if (parsed > std::numeric_limits::max()) { return false; } *result = static_cast(parsed); return true; } catch (...) { return false; } } bool TryParseInlinePatch(const std::string& value, int* result) { if (value.size() > kMaxInlinePatchDigits) { return false; } return TryParseNonNegativeInt(value, result); } size_t FindNumericStart(const std::string& version) { size_t start = 0; while (start < version.size() && !IsDigit(version[start])) { start++; } return start; } size_t FindNumericEnd(const std::string& version, size_t start) { size_t end = start; while (end < version.size() && (IsDigit(version[end]) || version[end] == '.')) { end++; } return end; } bool HasDigitBoundary(const std::string& value, size_t pos, size_t len) { const bool before_ok = pos == 0 || !IsDigit(value[pos - 1]); const size_t end = pos + len; const bool after_ok = end >= value.size() || !IsDigit(value[end]); return before_ok && after_ok; } bool IsCompactDateAt(const std::string& value, size_t pos) { if (pos + 8 > value.size() || !HasDigitBoundary(value, pos, 8)) { return false; } for (size_t i = 0; i < 8; ++i) { if (!IsDigit(value[pos + i])) { return false; } } return true; } std::string CompactDateToIso(const std::string& compact_date) { return compact_date.substr(0, 4) + "-" + compact_date.substr(4, 2) + "-" + compact_date.substr(6, 2); } bool ExtractDateFromText(const std::string& value, std::string* date, size_t* date_end) { for (size_t i = 0; i < value.size(); ++i) { if (IsCompactDateAt(value, i)) { *date = CompactDateToIso(value.substr(i, 8)); *date_end = i + 8; return true; } } return false; } ParsedVersion ParseVersion(const std::string& version) { const size_t numeric_start = FindNumericStart(version); const size_t numeric_end = FindNumericEnd(version, numeric_start); ParsedVersion parsed; parsed.numbers = SplitVersion(version.substr(numeric_start, numeric_end - numeric_start)); const std::string suffix = version.substr(numeric_end); size_t pos = 0; while (pos < suffix.size()) { while (pos < suffix.size() && !IsAlphaNumeric(suffix[pos])) { pos++; } const size_t token_start = pos; while (pos < suffix.size() && IsAlphaNumeric(suffix[pos])) { pos++; } if (token_start == pos) { continue; } const std::string token = suffix.substr(token_start, pos - token_start); if (parsed.date.empty() && IsCompactDateAt(token, 0)) { parsed.date = CompactDateToIso(token); continue; } int patch = 0; if (!parsed.has_patch && TryParseInlinePatch(token, &patch)) { parsed.has_patch = true; parsed.patch = patch; } } return parsed; } int CompareNumericVersion(const std::vector& current, const std::vector& latest) { std::vector current_parts = current; std::vector latest_parts = latest; const size_t len = std::max(current_parts.size(), latest_parts.size()); current_parts.resize(len, 0); latest_parts.resize(len, 0); for (size_t i = 0; i < len; ++i) { if (latest_parts[i] > current_parts[i]) { return 1; } if (latest_parts[i] < current_parts[i]) { return -1; } } return 0; } void ResetLatestMetadata() { latest_release_date_ = ""; latest_patch_available_ = false; latest_patch_ = 0; } bool ReadPatchField(const nlohmann::json& json, int* patch) { if (!json.contains("patch")) { return false; } const auto& patch_value = json["patch"]; if (patch_value.is_number_integer()) { const long long parsed = patch_value.get(); if (parsed < 0 || parsed > std::numeric_limits::max()) { return false; } *patch = static_cast(parsed); return true; } if (patch_value.is_string()) { return TryParseNonNegativeInt(patch_value.get(), patch); } return false; } void LogHttpError(const httplib::Result& result) { LOG_WARN("Failed to fetch version.json: error={}, message={}", static_cast(result.error()), httplib::to_string(result.error())); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT LOG_WARN("version.json SSL error={}, OpenSSL error={}", result.ssl_error(), result.ssl_openssl_error()); #endif } #if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__) bool PathExists(const std::string& path) { if (path.empty()) { return false; } std::error_code ec; return std::filesystem::exists(path, ec); } std::string GetEnvPathIfExists(const char* key) { const char* value = std::getenv(key); if (!value) { return ""; } const std::string path = value; return PathExists(path) ? path : ""; } std::string FindFirstExistingPath( const std::vector& candidates) { for (const auto& candidate : candidates) { if (PathExists(candidate)) { return candidate; } } return ""; } void ConfigureLinuxCaCerts(httplib::Client* cli) { const std::string ca_file = [&]() { const std::string env_path = GetEnvPathIfExists("SSL_CERT_FILE"); if (!env_path.empty()) { return env_path; } return FindFirstExistingPath({ "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/cert.pem", "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", }); }(); const std::string ca_dir = [&]() { const std::string env_path = GetEnvPathIfExists("SSL_CERT_DIR"); if (!env_path.empty()) { return env_path; } return FindFirstExistingPath({ "/etc/ssl/certs", "/etc/pki/tls/certs", "/etc/openssl/certs", }); }(); if (ca_file.empty() && ca_dir.empty()) { LOG_WARN("No Linux CA bundle found for version.json request; relying on OpenSSL defaults"); return; } cli->set_ca_cert_path(ca_file, ca_dir); LOG_INFO("Configured version.json TLS CA bundle: file={}, dir={}", ca_file.empty() ? "" : ca_file, ca_dir.empty() ? "" : ca_dir); } #endif } // namespace std::string ExtractNumericPart(const std::string& ver) { const size_t start = FindNumericStart(ver); const size_t end = FindNumericEnd(ver, start); return ver.substr(start, end - start); } std::vector SplitVersion(const std::string& ver) { std::vector nums; std::istringstream ss(ver); std::string token; while (std::getline(ss, token, '.')) { try { nums.push_back(std::stoi(token)); } catch (...) { nums.push_back(0); } } return nums; } // extract date from version string (format: v1.2.3-20251113-abc // or 1.2.3-20251113-abc) std::string ExtractDateFromVersion(const std::string& version) { const size_t numeric_start = FindNumericStart(version); const size_t numeric_end = FindNumericEnd(version, numeric_start); const std::string suffix = version.substr(numeric_end); std::string date; size_t date_end = 0; if (ExtractDateFromText(suffix, &date, &date_end)) { return date; } return ""; } // compare two dates in YYYY-MM-DD format bool IsNewerDate(const std::string& date1, const std::string& date2) { if (date1.empty() || date2.empty()) return false; // simple string comparison works for ISO date format (YYYY-MM-DD) return date2 > date1; } bool IsNewerVersion(const std::string& current, const std::string& latest) { return IsNewerVersionWithMetadata( current, latest, latest_release_date_, latest_patch_available_ ? latest_patch_ : -1); } bool IsNewerVersionWithMetadata(const std::string& current, const std::string& latest, const std::string& latest_date, int latest_patch) { (void)latest_date; const ParsedVersion current_version = ParseVersion(current); const ParsedVersion latest_version = ParseVersion(latest); const int numeric_compare = CompareNumericVersion(current_version.numbers, latest_version.numbers); if (numeric_compare > 0) { return true; } if (numeric_compare < 0) { return false; } const bool metadata_has_patch = latest_patch >= 0; const bool latest_has_patch = metadata_has_patch || latest_version.has_patch; if (latest_has_patch || current_version.has_patch) { const int resolved_latest_patch = metadata_has_patch ? latest_patch : (latest_version.has_patch ? latest_version.patch : 0); const int resolved_current_patch = current_version.has_patch ? current_version.patch : 0; return resolved_latest_patch > resolved_current_patch; } return false; } nlohmann::json CheckUpdate() { httplib::Client cli("https://version.crossdesk.cn"); cli.set_connection_timeout(5); cli.set_read_timeout(5); cli.set_follow_location(true); #if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__) ConfigureLinuxCaCerts(&cli); #endif auto res = cli.Get("/version.json"); if (res) { if (res->status == 200) { try { auto j = nlohmann::json::parse(res->body); if (j.contains("releaseDate") && j["releaseDate"].is_string()) { latest_release_date_ = j["releaseDate"]; } else { latest_release_date_ = ""; } latest_patch_ = 0; latest_patch_available_ = ReadPatchField(j, &latest_patch_); LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}", j.value("latest_version", j.value("version", "")), j.value("releaseDate", ""), latest_patch_available_ ? latest_patch_ : -1); return j; } catch (const std::exception& e) { LOG_WARN("Failed to parse version.json: {}", e.what()); ResetLatestMetadata(); return nlohmann::json{}; } } else { LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status); ResetLatestMetadata(); return nlohmann::json{}; } } else { LogHttpError(res); ResetLatestMetadata(); return nlohmann::json{}; } } } // namespace crossdesk