feat(hardening): 内嵌产品配置 + 系统级状态 + 全量 i18n + 依赖治理 + 资料清理
update-client-hardening 四工作流合入(构建/静态检查已过): - 配置:不可变产品策略经 UpdateClientResources.cmake 归一化校验后 内嵌 qrc(URL/相对路径/主程序必须落 install_root/版本/UUID 格式, CMake override 写入前复验);可变机器状态迁系统级原生存储; 运行目录不再落可编辑 app_config.json。 - i18n:生产源全英文(no-Han 检查 41/41),中文进 translations/ zh_CN 目录;翻译加载收敛 Common/TranslationHelper 唯一入口; Bootstrap 补资源文件。 - 依赖:Qt/OpenSSL/架构/Perl 机器路径改 imported targets + UpdateClientDependencies.cmake,3rdparty 由父工程契约供给。 - 清理:旧 README/SDK README/3 个重复 PowerShell 打包脚本/PDF 提取 文本删除,canonical README + 5 份结构化英文文档替代;Launcher requireAdministrator manifest;统一 MSVC /utf-8 删运行时编码设置。
This commit is contained in:
+565
-105
@@ -1,13 +1,162 @@
|
||||
#include "ConfigHelper.h"
|
||||
#include <QApplication>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSaveFile>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QDebug>
|
||||
#include <QUrl>
|
||||
#include <QUuid>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kProductConfigResource = ":/simcae/update-client/product-config.json";
|
||||
constexpr auto kSettingsOrganization = "SimCAE";
|
||||
constexpr auto kSettingsApplication = "UpdateClient";
|
||||
constexpr auto kMigrationMarker = "meta/legacy_state_migration_version";
|
||||
constexpr int kMigrationVersion = 1;
|
||||
|
||||
const QStringList& mutableKeys()
|
||||
{
|
||||
static const QStringList keys{
|
||||
QStringLiteral("current_version"),
|
||||
QStringLiteral("license_key"),
|
||||
QStringLiteral("device_id"),
|
||||
QStringLiteral("installation_id")
|
||||
};
|
||||
return keys;
|
||||
}
|
||||
|
||||
const QStringList& legacyMigratableKeys()
|
||||
{
|
||||
static const QStringList keys{
|
||||
QStringLiteral("license_key"),
|
||||
QStringLiteral("device_id"),
|
||||
QStringLiteral("installation_id")
|
||||
};
|
||||
return keys;
|
||||
}
|
||||
|
||||
QString settingsStatusText(QSettings::Status status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case QSettings::NoError:
|
||||
return QStringLiteral("no error");
|
||||
case QSettings::AccessError:
|
||||
return QStringLiteral("access denied");
|
||||
case QSettings::FormatError:
|
||||
return QStringLiteral("invalid settings format");
|
||||
}
|
||||
return QStringLiteral("unknown settings error");
|
||||
}
|
||||
|
||||
bool isSafeIdentifier(const QString& value)
|
||||
{
|
||||
static const QRegularExpression pattern(QStringLiteral("^[A-Za-z0-9._-]+$"));
|
||||
return pattern.match(value).hasMatch();
|
||||
}
|
||||
|
||||
bool isStrictRelativePath(const QString& value)
|
||||
{
|
||||
const QString relative = QDir::fromNativeSeparators(value.trimmed());
|
||||
if (relative.isEmpty()
|
||||
|| QDir::isAbsolutePath(relative)
|
||||
|| relative.contains(QLatin1Char(':'))
|
||||
|| QDir::cleanPath(relative) != relative)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList segments = relative.split(QLatin1Char('/'));
|
||||
for (const QString& segment : segments)
|
||||
{
|
||||
if (segment.isEmpty() || segment == QStringLiteral(".") || segment == QStringLiteral(".."))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isPathWithinRoot(const QString& rootPath, const QString& candidatePath)
|
||||
{
|
||||
const QString relative = QDir::fromNativeSeparators(
|
||||
QDir(rootPath).relativeFilePath(candidatePath));
|
||||
return !QDir::isAbsolutePath(relative)
|
||||
&& relative != QStringLiteral("..")
|
||||
&& !relative.startsWith(QStringLiteral("../"));
|
||||
}
|
||||
|
||||
bool isControlledMainExecutablePath(const QString& value, const QString& installRoot)
|
||||
{
|
||||
const QString relative = QDir::fromNativeSeparators(value.trimmed());
|
||||
if (relative.isEmpty()
|
||||
|| QDir::isAbsolutePath(relative)
|
||||
|| relative.contains(QLatin1Char(':'))
|
||||
|| QDir::cleanPath(relative) != relative
|
||||
|| relative == QStringLiteral(".")
|
||||
|| relative == QStringLiteral(".."))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString runtimeRoot = QDir::cleanPath(QCoreApplication::applicationDirPath());
|
||||
const QString approvedRoot = QDir::cleanPath(QDir(runtimeRoot).filePath(installRoot));
|
||||
const QString executablePath = QDir::cleanPath(QDir(runtimeRoot).filePath(relative));
|
||||
return isPathWithinRoot(approvedRoot, executablePath);
|
||||
}
|
||||
|
||||
bool isPositiveInteger(const QJsonValue& value)
|
||||
{
|
||||
if (!value.isString() && !value.isDouble())
|
||||
return false;
|
||||
|
||||
bool ok = false;
|
||||
const int number = value.toVariant().toString().toInt(&ok);
|
||||
return ok && number > 0;
|
||||
}
|
||||
|
||||
bool hasControlCharacter(const QString& value)
|
||||
{
|
||||
for (const QChar character : value)
|
||||
{
|
||||
if (character.category() == QChar::Other_Control)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool normalizeMutableStateValue(const QString& key, const QString& rawValue, QString* normalizedValue)
|
||||
{
|
||||
const QString value = rawValue.trimmed();
|
||||
bool valid = false;
|
||||
|
||||
if (key == QStringLiteral("current_version"))
|
||||
{
|
||||
static const QRegularExpression versionPattern(
|
||||
QStringLiteral("^[0-9A-Za-z][0-9A-Za-z._+-]{0,127}$"));
|
||||
valid = versionPattern.match(value).hasMatch();
|
||||
}
|
||||
else if (key == QStringLiteral("installation_id"))
|
||||
{
|
||||
valid = value.isEmpty() || !QUuid(value).isNull();
|
||||
}
|
||||
else if (key == QStringLiteral("device_id"))
|
||||
{
|
||||
valid = value.size() <= 256 && !hasControlCharacter(value);
|
||||
}
|
||||
else if (key == QStringLiteral("license_key"))
|
||||
{
|
||||
valid = value.size() <= 4096 && !hasControlCharacter(value);
|
||||
}
|
||||
|
||||
if (valid && normalizedValue)
|
||||
*normalizedValue = value;
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigHelper& ConfigHelper::instance()
|
||||
{
|
||||
@@ -16,43 +165,73 @@ ConfigHelper& ConfigHelper::instance()
|
||||
}
|
||||
|
||||
ConfigHelper::ConfigHelper()
|
||||
: m_productConfigPath(QString::fromLatin1(kProductConfigResource))
|
||||
{
|
||||
m_configPath = QApplication::applicationDirPath() + "/config/app_config.json";
|
||||
migrateLegacyIniIfNeeded();
|
||||
qDebug() << "Loading app config path:" << m_configPath;
|
||||
qDebug() << "File exists?" << QFile::exists(m_configPath);
|
||||
if (!loadProductConfig())
|
||||
{
|
||||
qCritical().noquote() << m_error;
|
||||
return;
|
||||
}
|
||||
|
||||
m_statePrefix = QStringLiteral("products/%1/%2/")
|
||||
.arg(m_productConfig.value(QStringLiteral("app_id")).toString(),
|
||||
m_productConfig.value(QStringLiteral("channel")).toString());
|
||||
|
||||
if (!migrateLegacyStateIfNeeded())
|
||||
qWarning().noquote() << m_error;
|
||||
if (!initializeStateDefaults())
|
||||
qWarning().noquote() << m_error;
|
||||
}
|
||||
|
||||
QString ConfigHelper::configPath() const
|
||||
{
|
||||
return m_configPath;
|
||||
return stateLocation();
|
||||
}
|
||||
|
||||
QString ConfigHelper::productConfigPath() const
|
||||
{
|
||||
return m_productConfigPath;
|
||||
}
|
||||
|
||||
QString ConfigHelper::stateLocation() const
|
||||
{
|
||||
QSettings settings(QSettings::NativeFormat, QSettings::SystemScope,
|
||||
QString::fromLatin1(kSettingsOrganization),
|
||||
QString::fromLatin1(kSettingsApplication));
|
||||
settings.setFallbacksEnabled(false);
|
||||
const QString nativeLocation = settings.fileName();
|
||||
if (!nativeLocation.isEmpty())
|
||||
return nativeLocation;
|
||||
return QStringLiteral("Native system settings: %1/%2")
|
||||
.arg(QString::fromLatin1(kSettingsOrganization),
|
||||
QString::fromLatin1(kSettingsApplication));
|
||||
}
|
||||
|
||||
QString ConfigHelper::installRoot() const
|
||||
{
|
||||
QString relativeRoot = getValue("Runtime", "install_root").trimmed();
|
||||
QString relativeRoot = getValue(QStringLiteral("Runtime"), QStringLiteral("install_root")).trimmed();
|
||||
if (relativeRoot.isEmpty())
|
||||
relativeRoot = ".";
|
||||
relativeRoot = QStringLiteral(".");
|
||||
return QDir::cleanPath(QDir(runtimeRoot()).filePath(relativeRoot));
|
||||
}
|
||||
|
||||
QString ConfigHelper::runtimeRoot() const
|
||||
{
|
||||
return QDir::cleanPath(QApplication::applicationDirPath());
|
||||
return QDir::cleanPath(QCoreApplication::applicationDirPath());
|
||||
}
|
||||
|
||||
QString ConfigHelper::updateRoot() const
|
||||
{
|
||||
return QDir(runtimeRoot()).filePath("update");
|
||||
return QDir(runtimeRoot()).filePath(QStringLiteral("update"));
|
||||
}
|
||||
|
||||
QString ConfigHelper::runtimeRelativePath() const
|
||||
{
|
||||
QString relative = QDir::fromNativeSeparators(QDir(installRoot()).relativeFilePath(runtimeRoot()));
|
||||
relative = QDir::cleanPath(relative);
|
||||
if (relative == ".")
|
||||
if (relative == QStringLiteral("."))
|
||||
return QString();
|
||||
while (relative.startsWith("./"))
|
||||
while (relative.startsWith(QStringLiteral("./")))
|
||||
relative = relative.mid(2);
|
||||
return relative.trimmed();
|
||||
}
|
||||
@@ -65,16 +244,9 @@ QString ConfigHelper::lastError() const
|
||||
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
QFile file(m_configPath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return QString();
|
||||
|
||||
QJsonParseError error;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !document.isObject())
|
||||
return QString();
|
||||
|
||||
return document.object().value(key).toVariant().toString();
|
||||
if (isMutableKey(key))
|
||||
return stateValue(key);
|
||||
return m_productConfig.value(key).toVariant().toString();
|
||||
}
|
||||
|
||||
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
|
||||
@@ -82,100 +254,388 @@ bool ConfigHelper::setValue(const QString& section, const QString& key, const QS
|
||||
Q_UNUSED(section);
|
||||
m_error.clear();
|
||||
|
||||
QFile input(m_configPath);
|
||||
QJsonObject config;
|
||||
if (input.exists())
|
||||
if (!isMutableKey(key))
|
||||
{
|
||||
if (!input.open(QIODevice::ReadOnly))
|
||||
{
|
||||
m_error = QString("Cannot open config for reading: %1").arg(input.errorString());
|
||||
return false;
|
||||
}
|
||||
QJsonParseError parseError;
|
||||
const QByteArray raw = input.readAll();
|
||||
input.close();
|
||||
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||
{
|
||||
m_error = QString("Invalid config JSON: %1").arg(parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
config = document.object();
|
||||
}
|
||||
|
||||
config.insert(key, value);
|
||||
QDir dir(QFileInfo(m_configPath).path());
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
{
|
||||
m_error = QString("Cannot create config directory: %1").arg(dir.path());
|
||||
m_error = QStringLiteral("Configuration key '%1' is immutable and cannot be changed at runtime.")
|
||||
.arg(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
QSaveFile output(m_configPath);
|
||||
if (!output.open(QIODevice::WriteOnly))
|
||||
if (m_statePrefix.isEmpty())
|
||||
{
|
||||
m_error = QString("Cannot open config for writing: %1").arg(output.errorString());
|
||||
m_error = QStringLiteral("Runtime state is unavailable because product configuration was not loaded.");
|
||||
return false;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented);
|
||||
if (output.write(payload) != payload.size())
|
||||
|
||||
QString normalizedValue;
|
||||
if (!normalizeMutableStateValue(key, value, &normalizedValue))
|
||||
{
|
||||
m_error = QString("Cannot write full config file: %1").arg(output.errorString());
|
||||
output.cancelWriting();
|
||||
m_error = QStringLiteral("Runtime state key '%1' has an invalid value.").arg(key);
|
||||
return false;
|
||||
}
|
||||
if (!output.commit())
|
||||
|
||||
return writeStateValue(key, normalizedValue);
|
||||
}
|
||||
|
||||
bool ConfigHelper::loadProductConfig()
|
||||
{
|
||||
QFile file(m_productConfigPath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
m_error = QString("Cannot commit config file: %1").arg(output.errorString());
|
||||
m_error = QStringLiteral("Cannot open embedded product configuration '%1': %2")
|
||||
.arg(m_productConfigPath, file.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||
{
|
||||
m_error = QStringLiteral("Embedded product configuration is invalid JSON at offset %1: %2")
|
||||
.arg(parseError.offset)
|
||||
.arg(parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject config = document.object();
|
||||
const QJsonValue schemaVersion = config.value(QStringLiteral("schema_version"));
|
||||
if (!schemaVersion.isDouble() || schemaVersion.toDouble(-1.0) != 1.0)
|
||||
{
|
||||
m_error = QStringLiteral("Embedded product configuration schema_version must be 1.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList requiredTextKeys{
|
||||
QStringLiteral("app_id"),
|
||||
QStringLiteral("app_name"),
|
||||
QStringLiteral("channel"),
|
||||
QStringLiteral("launch_token"),
|
||||
QStringLiteral("api_base_url"),
|
||||
QStringLiteral("client_token"),
|
||||
QStringLiteral("temp_folder"),
|
||||
QStringLiteral("install_root"),
|
||||
QStringLiteral("main_executable"),
|
||||
QStringLiteral("launcher_executable"),
|
||||
QStringLiteral("updater_executable"),
|
||||
QStringLiteral("bootstrap_executable"),
|
||||
QStringLiteral("platform"),
|
||||
QStringLiteral("arch")
|
||||
};
|
||||
for (const QString& key : requiredTextKeys)
|
||||
{
|
||||
const QJsonValue value = config.value(key);
|
||||
if (!value.isString() || value.toString().trimmed().isEmpty())
|
||||
{
|
||||
m_error = QStringLiteral("Embedded product configuration key '%1' must be a non-empty string.")
|
||||
.arg(key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString& key : mutableKeys())
|
||||
{
|
||||
if (config.contains(key))
|
||||
{
|
||||
m_error = QStringLiteral("Mutable key '%1' must not appear in the product configuration root.")
|
||||
.arg(key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSafeIdentifier(config.value(QStringLiteral("app_id")).toString())
|
||||
|| !isSafeIdentifier(config.value(QStringLiteral("channel")).toString()))
|
||||
{
|
||||
m_error = QStringLiteral("Product app_id and channel may contain only letters, digits, '.', '_' and '-'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QUrl apiUrl(config.value(QStringLiteral("api_base_url")).toString());
|
||||
if (!apiUrl.isValid()
|
||||
|| apiUrl.host().isEmpty()
|
||||
|| (apiUrl.scheme().compare(QStringLiteral("http"), Qt::CaseInsensitive) != 0
|
||||
&& apiUrl.scheme().compare(QStringLiteral("https"), Qt::CaseInsensitive) != 0))
|
||||
{
|
||||
m_error = QStringLiteral("Product api_base_url must be a valid HTTP or HTTPS URL.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList positiveIntegerKeys{
|
||||
QStringLiteral("client_protocol"),
|
||||
QStringLiteral("request_timeout_ms"),
|
||||
QStringLiteral("health_check_timeout_ms")
|
||||
};
|
||||
for (const QString& key : positiveIntegerKeys)
|
||||
{
|
||||
if (!isPositiveInteger(config.value(key)))
|
||||
{
|
||||
m_error = QStringLiteral("Embedded product configuration key '%1' must be a positive integer.")
|
||||
.arg(key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const QString installRoot = QDir::fromNativeSeparators(
|
||||
config.value(QStringLiteral("install_root")).toString().trimmed());
|
||||
if (QDir::isAbsolutePath(installRoot)
|
||||
|| installRoot.contains(QLatin1Char(':'))
|
||||
|| QDir::cleanPath(installRoot) != installRoot)
|
||||
{
|
||||
m_error = QStringLiteral("Product install_root must be a normalized relative path.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isSafeIdentifier(config.value(QStringLiteral("platform")).toString())
|
||||
|| !isSafeIdentifier(config.value(QStringLiteral("arch")).toString()))
|
||||
{
|
||||
m_error = QStringLiteral("Product platform and arch may contain only letters, digits, '.', '_' and '-'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isControlledMainExecutablePath(
|
||||
config.value(QStringLiteral("main_executable")).toString(), installRoot))
|
||||
{
|
||||
m_error = QStringLiteral(
|
||||
"Embedded product configuration key 'main_executable' must stay within install_root.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList restrictedRelativePaths{
|
||||
QStringLiteral("temp_folder"),
|
||||
QStringLiteral("launcher_executable"),
|
||||
QStringLiteral("updater_executable"),
|
||||
QStringLiteral("bootstrap_executable")
|
||||
};
|
||||
for (const QString& key : restrictedRelativePaths)
|
||||
{
|
||||
if (!isStrictRelativePath(config.value(key).toString()))
|
||||
{
|
||||
m_error = QStringLiteral("Embedded product configuration key '%1' must be a safe relative path.")
|
||||
.arg(key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonValue initialStateValue = config.value(QStringLiteral("initial_state"));
|
||||
if (!initialStateValue.isObject())
|
||||
{
|
||||
m_error = QStringLiteral("Embedded product configuration must contain an initial_state object.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject initialState = initialStateValue.toObject();
|
||||
QJsonObject normalizedInitialState;
|
||||
for (auto it = initialState.constBegin(); it != initialState.constEnd(); ++it)
|
||||
{
|
||||
if (!isMutableKey(it.key()) || !it.value().isString())
|
||||
{
|
||||
m_error = QStringLiteral("Initial state key '%1' is unsupported or is not a string.").arg(it.key());
|
||||
return false;
|
||||
}
|
||||
|
||||
QString normalizedValue;
|
||||
if (!normalizeMutableStateValue(it.key(), it.value().toString(), &normalizedValue))
|
||||
{
|
||||
m_error = QStringLiteral("Initial state key '%1' has an invalid value.").arg(it.key());
|
||||
return false;
|
||||
}
|
||||
normalizedInitialState.insert(it.key(), normalizedValue);
|
||||
}
|
||||
if (!normalizedInitialState.contains(QStringLiteral("current_version")))
|
||||
{
|
||||
m_error = QStringLiteral("Initial state current_version must be a non-empty string.");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_productConfig = config;
|
||||
m_productConfig.remove(QStringLiteral("initial_state"));
|
||||
m_initialState = normalizedInitialState;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigHelper::migrateLegacyStateIfNeeded()
|
||||
{
|
||||
QSettings settings(QSettings::NativeFormat, QSettings::SystemScope,
|
||||
QString::fromLatin1(kSettingsOrganization),
|
||||
QString::fromLatin1(kSettingsApplication));
|
||||
settings.setFallbacksEnabled(false);
|
||||
const QString markerKey = m_statePrefix + QString::fromLatin1(kMigrationMarker);
|
||||
if (settings.value(markerKey).toInt() >= kMigrationVersion)
|
||||
return true;
|
||||
|
||||
bool allStateKeysPresent = true;
|
||||
for (const QString& key : legacyMigratableKeys())
|
||||
allStateKeysPresent = allStateKeysPresent && settings.contains(stateStorageKey(key));
|
||||
|
||||
QJsonObject iniValues;
|
||||
QJsonObject jsonValues;
|
||||
const QString legacyIniPath = QDir(QCoreApplication::applicationDirPath())
|
||||
.filePath(QStringLiteral("client.ini"));
|
||||
if (!allStateKeysPresent && QFile::exists(legacyIniPath))
|
||||
{
|
||||
QSettings ini(legacyIniPath, QSettings::IniFormat);
|
||||
ini.setFallbacksEnabled(false);
|
||||
const auto importIniValue = [&](const QString& section, const QString& key) {
|
||||
const QString legacyKey = section + QLatin1Char('/') + key;
|
||||
if (ini.contains(legacyKey))
|
||||
iniValues.insert(key, ini.value(legacyKey).toString());
|
||||
};
|
||||
importIniValue(QStringLiteral("License"), QStringLiteral("license_key"));
|
||||
importIniValue(QStringLiteral("Update"), QStringLiteral("device_id"));
|
||||
importIniValue(QStringLiteral("Device"), QStringLiteral("installation_id"));
|
||||
if (ini.status() != QSettings::NoError)
|
||||
qWarning().noquote() << QStringLiteral("Cannot read legacy INI state '%1': %2")
|
||||
.arg(legacyIniPath, settingsStatusText(ini.status()));
|
||||
}
|
||||
|
||||
const QString legacyJsonPath = QDir(QCoreApplication::applicationDirPath())
|
||||
.filePath(QStringLiteral("config/app_config.json"));
|
||||
QFile legacyJson(legacyJsonPath);
|
||||
if (!allStateKeysPresent && legacyJson.exists())
|
||||
{
|
||||
if (!legacyJson.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qWarning().noquote() << QStringLiteral("Cannot read legacy JSON state '%1': %2")
|
||||
.arg(legacyJsonPath, legacyJson.errorString());
|
||||
}
|
||||
else
|
||||
{
|
||||
QJsonParseError legacyParseError;
|
||||
const QJsonDocument legacyDocument = QJsonDocument::fromJson(legacyJson.readAll(), &legacyParseError);
|
||||
if (legacyParseError.error != QJsonParseError::NoError || !legacyDocument.isObject())
|
||||
{
|
||||
qWarning().noquote() << QStringLiteral("Cannot parse legacy JSON state '%1' at offset %2: %3")
|
||||
.arg(legacyJsonPath)
|
||||
.arg(legacyParseError.offset)
|
||||
.arg(legacyParseError.errorString());
|
||||
}
|
||||
else
|
||||
{
|
||||
const QJsonObject legacy = legacyDocument.object();
|
||||
for (const QString& key : legacyMigratableKeys())
|
||||
{
|
||||
const QJsonValue value = legacy.value(key);
|
||||
if (value.isString())
|
||||
jsonValues.insert(key, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QStringList migratedKeys;
|
||||
for (const QString& key : legacyMigratableKeys())
|
||||
{
|
||||
const QString storageKey = stateStorageKey(key);
|
||||
if (settings.contains(storageKey))
|
||||
continue;
|
||||
|
||||
QString normalizedValue;
|
||||
bool hasValue = false;
|
||||
if (jsonValues.contains(key))
|
||||
{
|
||||
hasValue = normalizeMutableStateValue(key, jsonValues.value(key).toString(), &normalizedValue);
|
||||
if (!hasValue)
|
||||
qWarning().noquote() << QStringLiteral("Rejected invalid legacy JSON state key '%1' from '%2'.")
|
||||
.arg(key, legacyJsonPath);
|
||||
}
|
||||
if (!hasValue && iniValues.contains(key))
|
||||
{
|
||||
hasValue = normalizeMutableStateValue(key, iniValues.value(key).toString(), &normalizedValue);
|
||||
if (!hasValue)
|
||||
qWarning().noquote() << QStringLiteral("Rejected invalid legacy INI state key '%1' from '%2'.")
|
||||
.arg(key, legacyIniPath);
|
||||
}
|
||||
if (hasValue)
|
||||
{
|
||||
settings.setValue(storageKey, normalizedValue);
|
||||
migratedKeys.append(key);
|
||||
}
|
||||
}
|
||||
|
||||
settings.sync();
|
||||
if (settings.status() != QSettings::NoError)
|
||||
{
|
||||
m_error = QStringLiteral("Cannot persist legacy runtime state migration in system settings: %1")
|
||||
.arg(settingsStatusText(settings.status()));
|
||||
return false;
|
||||
}
|
||||
|
||||
settings.setValue(markerKey, kMigrationVersion);
|
||||
settings.sync();
|
||||
if (settings.status() != QSettings::NoError)
|
||||
{
|
||||
m_error = QStringLiteral("Cannot mark legacy runtime state migration complete in system settings: %1")
|
||||
.arg(settingsStatusText(settings.status()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!migratedKeys.isEmpty())
|
||||
qInfo().noquote() << QStringLiteral("Migrated legacy runtime state keys: %1")
|
||||
.arg(migratedKeys.join(QStringLiteral(", ")));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigHelper::initializeStateDefaults()
|
||||
{
|
||||
QSettings settings(QSettings::NativeFormat, QSettings::SystemScope,
|
||||
QString::fromLatin1(kSettingsOrganization),
|
||||
QString::fromLatin1(kSettingsApplication));
|
||||
settings.setFallbacksEnabled(false);
|
||||
|
||||
for (const QString& key : mutableKeys())
|
||||
{
|
||||
const QString storageKey = stateStorageKey(key);
|
||||
if (!settings.contains(storageKey) && m_initialState.contains(key))
|
||||
settings.setValue(storageKey, m_initialState.value(key).toString());
|
||||
}
|
||||
|
||||
settings.sync();
|
||||
if (settings.status() != QSettings::NoError)
|
||||
{
|
||||
m_error = QStringLiteral("Cannot initialize runtime state in system settings: %1")
|
||||
.arg(settingsStatusText(settings.status()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
QString ConfigHelper::stateStorageKey(const QString& key) const
|
||||
{
|
||||
if (QFile::exists(m_configPath))
|
||||
return true;
|
||||
|
||||
const QString legacyPath = QApplication::applicationDirPath() + "/client.ini";
|
||||
if (!QFile::exists(legacyPath))
|
||||
return false;
|
||||
|
||||
QSettings ini(legacyPath, QSettings::IniFormat);
|
||||
QJsonObject config;
|
||||
const auto copyText = [&](const QString& section, const QString& key, const QString& fallback = QString()) {
|
||||
const QString value = ini.value(section + "/" + key, fallback).toString();
|
||||
config.insert(key, value);
|
||||
};
|
||||
|
||||
copyText("App", "app_id");
|
||||
copyText("App", "app_name", "Marsco Demo App");
|
||||
copyText("App", "channel", "stable");
|
||||
copyText("App", "current_version", "1.0.0");
|
||||
copyText("App", "client_protocol", "3");
|
||||
copyText("App", "launch_token");
|
||||
copyText("License", "license_key");
|
||||
copyText("Server", "api_base_url");
|
||||
copyText("Server", "client_token");
|
||||
copyText("Update", "request_timeout_ms", "5000");
|
||||
copyText("Update", "temp_folder", "update_temp");
|
||||
copyText("Update", "device_id");
|
||||
copyText("Runtime", "install_root", ".");
|
||||
copyText("Runtime", "main_executable", "MainApp.exe");
|
||||
copyText("Runtime", "launcher_executable", "Launcher.exe");
|
||||
copyText("Runtime", "updater_executable", "Updater.exe");
|
||||
copyText("Runtime", "bootstrap_executable", "Bootstrap.exe");
|
||||
copyText("Runtime", "health_check_timeout_ms", "15000");
|
||||
config.insert("platform", "windows");
|
||||
config.insert("arch", "x64");
|
||||
|
||||
QDir().mkpath(QFileInfo(m_configPath).path());
|
||||
QSaveFile output(m_configPath);
|
||||
if (!output.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
output.write(QJsonDocument(config).toJson(QJsonDocument::Indented));
|
||||
const bool saved = output.commit();
|
||||
if (saved)
|
||||
qDebug() << "Migrated legacy client.ini to" << m_configPath;
|
||||
return saved;
|
||||
return m_statePrefix + QStringLiteral("state/") + key;
|
||||
}
|
||||
|
||||
QString ConfigHelper::stateValue(const QString& key) const
|
||||
{
|
||||
if (m_statePrefix.isEmpty())
|
||||
return QString();
|
||||
|
||||
QSettings settings(QSettings::NativeFormat, QSettings::SystemScope,
|
||||
QString::fromLatin1(kSettingsOrganization),
|
||||
QString::fromLatin1(kSettingsApplication));
|
||||
settings.setFallbacksEnabled(false);
|
||||
const QString storageKey = stateStorageKey(key);
|
||||
if (settings.contains(storageKey))
|
||||
return settings.value(storageKey).toString();
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool ConfigHelper::writeStateValue(const QString& key, const QString& value)
|
||||
{
|
||||
QSettings settings(QSettings::NativeFormat, QSettings::SystemScope,
|
||||
QString::fromLatin1(kSettingsOrganization),
|
||||
QString::fromLatin1(kSettingsApplication));
|
||||
settings.setFallbacksEnabled(false);
|
||||
settings.setValue(stateStorageKey(key), value);
|
||||
settings.sync();
|
||||
if (settings.status() != QSettings::NoError)
|
||||
{
|
||||
m_error = QStringLiteral("Cannot write runtime state key '%1' to system settings: %2")
|
||||
.arg(key, settingsStatusText(settings.status()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigHelper::isMutableKey(const QString& key)
|
||||
{
|
||||
return mutableKeys().contains(key);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user