// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Pure (no-I/O) core of the daemon updater: release-JSON parsing, platform/asset matching, the // markdown checksum-table parser, version normalization, and signature-asset selection. Split from // daemon_updater.cpp (the libcurl/miniz worker) so it can be unit-tested without curl/miniz. The // generic SHA-256 / ed25519 primitives are reused from the miner updater (xmrig_updater_core.cpp). #include "daemon_updater.h" #include #include #include #include using json = nlohmann::json; namespace dragonx { namespace util { namespace { std::string toLower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return s; } bool isHex64(const std::string& s) { if (s.size() != 64) return false; for (unsigned char c : s) if (!std::isxdigit(c)) return false; return true; } } // namespace namespace { // Fill a DaemonRelease from one Gitea release JSON object (ok=true iff it has a tag). DaemonRelease parseOneDaemonRelease(const json& j) { DaemonRelease r; if (!j.is_object()) return r; if (j.contains("tag_name") && j["tag_name"].is_string()) r.tag = j["tag_name"].get(); if (j.contains("name") && j["name"].is_string()) r.name = j["name"].get(); if (j.contains("body") && j["body"].is_string()) r.body = j["body"].get(); if (j.contains("prerelease") && j["prerelease"].is_boolean()) r.prerelease = j["prerelease"].get(); if (j.contains("published_at") && j["published_at"].is_string()) r.publishedAt = j["published_at"].get(); if (j.contains("assets") && j["assets"].is_array()) { for (const auto& a : j["assets"]) { if (!a.is_object()) continue; DaemonReleaseAsset asset; if (a.contains("name") && a["name"].is_string()) asset.name = a["name"].get(); if (a.contains("browser_download_url") && a["browser_download_url"].is_string()) asset.downloadUrl = a["browser_download_url"].get(); if (a.contains("size") && a["size"].is_number_integer()) asset.size = a["size"].get(); if (!asset.name.empty() && !asset.downloadUrl.empty()) r.assets.push_back(std::move(asset)); } } if (!r.tag.empty()) r.ok = true; return r; } } // namespace DaemonRelease parseDaemonRelease(const std::string& jsonStr) { DaemonRelease r; try { r = parseOneDaemonRelease(json::parse(jsonStr)); if (!r.ok) r.error = "release JSON has no tag_name"; } catch (const std::exception& e) { r.error = std::string("failed to parse release JSON: ") + e.what(); } return r; } std::vector parseDaemonReleaseList(const std::string& jsonStr) { std::vector out; try { const json j = json::parse(jsonStr); if (!j.is_array()) return out; for (const auto& e : j) { if (e.contains("draft") && e["draft"].is_boolean() && e["draft"].get()) continue; // skip drafts (not meant for end users) DaemonRelease r = parseOneDaemonRelease(e); if (r.ok) out.push_back(std::move(r)); } } catch (const std::exception&) { // malformed list -> empty (caller treats as "could not load releases") } return out; } std::string currentDaemonPlatformToken() { #if defined(_WIN32) return "win64"; #elif defined(__APPLE__) return "macos"; // single macOS archive (no arm/x86 split in the release naming) #elif defined(__linux__) #if defined(__aarch64__) return "linux-arm64"; // no arm64 build published yet -> resolves to Unavailable #else return "linux-amd64"; #endif #else return ""; #endif } int selectDaemonAsset(const DaemonRelease& release, const std::string& platformToken) { if (platformToken.empty()) return -1; const std::string needle = "-" + toLower(platformToken) + ".zip"; for (std::size_t i = 0; i < release.assets.size(); ++i) { const std::string n = toLower(release.assets[i].name); if (n.size() >= needle.size() && n.compare(n.size() - needle.size(), needle.size(), needle) == 0) return static_cast(i); } return -1; } std::map parseDaemonChecksums(const std::string& body) { // The release body publishes checksums as a markdown table: // | File | SHA-256 | // |------|---------| // | dragonx-1.0.2-linux-amd64.zip | `85f1dd…16` | // Per line: blank out the table/code delimiters ('|' and '`'), then find the 64-hex token (the // hash) and a token ending in ".zip" (the archive name). Header/separator/prose rows lack one // or the other and are skipped, so this is robust to surrounding text and column order. std::map out; std::istringstream in(body); std::string line; while (std::getline(in, line)) { for (char& c : line) if (c == '|' || c == '`') c = ' '; std::istringstream ls(line); std::string tok, hash, name; while (ls >> tok) { if (hash.empty() && isHex64(tok)) { hash = toLower(tok); continue; } if (name.empty()) { const std::string low = toLower(tok); // Key by the lowercased name so the lookup (also lowercased) is case-insensitive, // in case the markdown table and the JSON asset names differ in case. if (low.size() >= 4 && low.compare(low.size() - 4, 4, ".zip") == 0) name = low; } } if (!hash.empty() && !name.empty()) out[name] = hash; } return out; } std::vector daemonExtractBasenames(const std::string& platformToken) { if (platformToken.rfind("win", 0) == 0) return {"dragonxd.exe", "dragonx-cli.exe", "dragonx-tx.exe"}; return {"dragonxd", "dragonx-cli", "dragonx-tx"}; } int selectDaemonSignatureAsset(const DaemonRelease& release, const std::string& archiveName) { if (archiveName.empty()) return -1; for (std::size_t i = 0; i < release.assets.size(); ++i) { const std::string& n = release.assets[i].name; if (n == archiveName + ".sig" || n == archiveName + ".minisig") return static_cast(i); } return -1; } std::string daemonVersionCore(const std::string& version) { // Extract a leading "v?MAJOR.MINOR.PATCH" run, ignoring any "-" / build suffix. Hand- // rolled (no ) to stay light and dependency-free. const std::size_t start = version.find_first_of("0123456789"); if (start == std::string::npos) return version; std::size_t i = start; int dots = 0; for (; i < version.size(); ++i) { const char c = version[i]; if (c >= '0' && c <= '9') continue; if (c == '.' && dots < 2) { ++dots; continue; } break; } if (dots < 2) return version; // not a full N.N.N — leave as-is const bool hasV = start > 0 && (version[start - 1] == 'v' || version[start - 1] == 'V'); return (hasV ? "v" : "") + version.substr(start, i - start); } } // namespace util } // namespace dragonx