#include #include #include #include #include #include #include namespace crossdesk { 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; } 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_unsigned()) { const auto parsed = patch_value.get(); if (parsed > static_cast(std::numeric_limits::max())) { return false; } *patch = static_cast(parsed); return true; } 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; } } // 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, "", -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; } std::optional ParseVersionInfo(const nlohmann::json& json) { if (!json.is_object()) { return std::nullopt; } const auto read_string = [&](const char* key) -> std::string { const auto it = json.find(key); return it != json.end() && it->is_string() ? it->get() : std::string{}; }; VersionInfo info; info.version = read_string("latest_version"); if (info.version.empty()) { info.version = read_string("version"); } if (!info.version.empty() && (info.version.front() == 'v' || info.version.front() == 'V')) { info.version.erase(0, 1); } // Invalid/missing version data is a failed check, never "up to date". const auto numeric_end = FindNumericEnd(info.version, 0); if (numeric_end == 0 || info.version[numeric_end - 1] == '.' || (numeric_end < info.version.size() && (numeric_end + 1 == info.version.size() || (info.version[numeric_end] != '-' && info.version[numeric_end] != '+')))) { return std::nullopt; } std::istringstream parts(info.version.substr(0, numeric_end)); std::string part; int number = 0; while (std::getline(parts, part, '.')) { if (!TryParseNonNegativeInt(part, &number)) { return std::nullopt; } } info.release_name = read_string("releaseName"); info.release_notes = read_string("releaseNotes"); info.release_date = read_string("releaseDate"); ReadPatchField(json, &info.patch); return info; } std::optional ParseVersionInfoJSON(std::string_view json) { try { return ParseVersionInfo(nlohmann::json::parse(json)); } catch (const nlohmann::json::exception&) { return std::nullopt; } } bool IsValidAppVersion(const std::string& version) { return ParseVersionInfo(nlohmann::json{{"version", version}}).has_value(); } std::optional CheckPlatformAppUpdate(const std::string& current, std::string_view json, const std::string& platform) { if (!IsValidAppVersion(current)) return std::nullopt; try { const auto manifest = nlohmann::json::parse(json); const auto latest = ParseVersionInfo(manifest.at("downloads").at(platform)); if (!latest) return std::nullopt; return AvailableAppUpdate(current, *latest).value_or(""); } catch (const nlohmann::json::exception&) { return std::nullopt; } } std::optional AvailableAppUpdate(const std::string& current, const VersionInfo& latest) { if (!IsValidAppVersion(latest.version) || (IsValidAppVersion(current) && !IsNewerVersionWithMetadata(current, latest.version, latest.release_date, latest.patch))) { return std::nullopt; } // Include the effective patch even when it exists only in release metadata. // Build dates and hashes do not participate in release ordering. const auto parsed = ParseVersion(latest.version); std::string label = ExtractNumericPart(latest.version); const int patch = latest.patch >= 0 ? latest.patch : parsed.patch; if (patch > 0) label += "-" + std::to_string(patch); return label; } } // namespace crossdesk