feat(client): improve SDK packaging and registry config

This commit is contained in:
2026-07-14 01:37:06 +00:00
parent cb5819ab7c
commit 94b20bf2bb
38 changed files with 3040 additions and 3960 deletions
+32 -31
View File
@@ -1,8 +1,9 @@
#include "UpdaterLogic.h"
#include <QSet>
#include <QDebug>
#include <QFileInfo>
#include <QFile>
#include <QFileInfo>
#include <QFile>
#include <QCoreApplication>
#include <QEventLoop>
#include <QElapsedTimer>
#include <QThread>
@@ -341,45 +342,45 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
{
m_offlineError.clear();
QFile package(packagePath);
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = "无法打开离线更新包"; return false; }
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = "离线包格式标识无效"; return false; }
const QByteArray lengthBytes = package.read(8);
if (lengthBytes.size() != 8) { m_offlineError = "离线包头不完整"; return false; }
quint64 headerSize = 0;
for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast<unsigned char>(lengthBytes[i])) << (i * 8);
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = "离线包头长度无效"; return false; }
QJsonParseError error;
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error);
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = "离线包头 JSON 无效"; return false; }
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot open the offline update package"); return false; }
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package format identifier is invalid"); return false; }
const QByteArray lengthBytes = package.read(8);
if (lengthBytes.size() != 8) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header is incomplete"); return false; }
quint64 headerSize = 0;
for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast<unsigned char>(lengthBytes[i])) << (i * 8);
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header length is invalid"); return false; }
QJsonParseError error;
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error);
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header JSON is invalid"); return false; }
const QJsonObject wrapper = wrapperDoc.object();
const QByteArray packageText = wrapper.value("package_text").toString().toUtf8();
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
const QString publicKey = QApplication::applicationDirPath() + "/config/manifest_public_key.pem";
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = "离线包 RSA 签名无效"; return false; }
const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object();
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = "离线包签名元数据无效"; return false; }
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = "Manifest 摘要与包签名不一致"; return false; }
QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object();
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = "离线 Manifest 无效"; return false; }
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package RSA signature is invalid"); return false; }
const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object();
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package signature metadata is invalid"); return false; }
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The manifest digest does not match the package signature"); return false; }
QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object();
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest is invalid"); return false; }
manifest.insert("signature", wrapper.value("manifest_signature").toString());
m_manifest = manifest; m_manifestText = QString::fromUtf8(manifestText); m_fileItems.clear();
if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = "包信息与 Manifest 身份不一致"; return false; }
if (!verifyManifestSignature()) { m_offlineError = "离线 Manifest RSA 签名无效"; return false; }
if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Package information does not match manifest identity"); return false; }
if (!verifyManifestSignature()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest RSA signature is invalid"); return false; }
const qint64 payloadStart = 16 + qint64(headerSize);
const QJsonArray entries = packageMeta.value("files").toArray();
for (const QJsonValue& value : entries) {
const QJsonObject item = value.toObject(); const QString path = QDir::fromNativeSeparators(item.value("path").toString());
const qint64 offset = item.value("offset").toVariant().toLongLong(); const qint64 size = item.value("size").toVariant().toLongLong();
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = "离线包包含不安全路径或越界数据: " + path; return false; }
FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi);
if (stagingDir.isEmpty()) continue;
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = "无法准备离线文件: " + path; return false; }
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = "无法创建暂存文件: " + path; return false; }
QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size;
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = "离线文件读取失败: " + path; return false; } hash.addData(block); remaining -= block.size(); }
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = "离线文件 Hash 或写入失败: " + path; return false; }
}
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = "离线包文件数量与 Manifest 不一致"; return false; }
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package contains an unsafe path or out-of-range data: %1").arg(path); return false; }
FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi);
if (stagingDir.isEmpty()) continue;
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot prepare offline file: %1").arg(path); return false; }
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot create staged file: %1").arg(path); return false; }
QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size;
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Failed to read offline file: %1").arg(path); return false; } hash.addData(block); remaining -= block.size(); }
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Offline file hash verification or write failed: %1").arg(path); return false; }
}
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package file count does not match the manifest"); return false; }
return true;
}
@@ -514,7 +515,7 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
if (existingSize > 0 && httpStatus == 200)
{
// 服务端忽略 Range,当前文件是“旧片段 + 完整响应”,必须安全重下。
// The server ignored Range; the current file is "old fragment + full response" and must be redownloaded safely.
qDebug() << "Server ignored Range; restart full download:" << savePath;
QFile::remove(partPath);
}