2026-07-14 01:37:06 +00:00
#include "ConfigHelper.h"
#include <QApplication>
#include <QByteArray>
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMessageBox>
#include <QSaveFile>
#include <QSettings>
2026-07-16 08:14:51 +00:00
#include <QStandardPaths>
2026-07-14 01:37:06 +00:00
#include <QStringList>
#include <QDebug>
#include <string>
#ifdef Q_OS_WIN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <shellapi.h>
#endif
namespace
{
const QString kElevatedConfigSetFlag = QStringLiteral ( "--simcae-config-set" );
const QString kElevatedFileWriteFlag = QStringLiteral ( "--simcae-file-write" );
const QString kElevatedFileRemoveFlag = QStringLiteral ( "--simcae-file-remove" );
const QString kConfigPathPrefix = QStringLiteral ( "--config-path-b64=" );
const QString kConfigKeyPrefix = QStringLiteral ( "--config-key-b64=" );
const QString kConfigValuePrefix = QStringLiteral ( "--config-value-b64=" );
const QString kFilePathPrefix = QStringLiteral ( "--file-path-b64=" );
const QString kFileDataPrefix = QStringLiteral ( "--file-data-b64=" );
const QString kRegistryOrganization = QStringLiteral ( "Marsco" );
const QString kRegistryApplication = QStringLiteral ( "UpdateClientSDK" );
const QString kRegistryInstallationsGroup = QStringLiteral ( "installations" );
const QString kRegistryConfigGroup = QStringLiteral ( "config" );
const QString kRegistryMetaGroup = QStringLiteral ( "_meta" );
const QString kRegistrySourceHashKey = QStringLiteral ( "source_sha256" );
const QString kRegistrySourcePathKey = QStringLiteral ( "source_path" );
const QString kRegistryRuntimeRootKey = QStringLiteral ( "runtime_root" );
const QString kRegistryImportedAtKey = QStringLiteral ( "imported_at_utc" );
2026-07-15 06:39:45 +00:00
const QString kEmbeddedServerConfigPath = QStringLiteral ( ":/simcae/server_config.json" );
const QString kApiBaseUrlKey = QStringLiteral ( "api_base_url" );
2026-07-14 01:37:06 +00:00
2026-07-16 08:14:51 +00:00
// 需要提权写入时,子进程参数统一用 Base64Url 编码。
// 这样可以避免 Windows 路径、中文、空格或换行在 ShellExecute 参数传递中被截断或误解析。
2026-07-14 01:37:06 +00:00
QString encodeArgument ( const QString & value )
{
return QString :: fromLatin1 ( value . toUtf8 (). toBase64 (
QByteArray :: Base64UrlEncoding | QByteArray :: OmitTrailingEquals ));
}
QString encodeBytes ( const QByteArray & value )
{
return QString :: fromLatin1 ( value . toBase64 (
QByteArray :: Base64UrlEncoding | QByteArray :: OmitTrailingEquals ));
}
QString decodeArgument ( const QString & value )
{
return QString :: fromUtf8 ( QByteArray :: fromBase64 ( value . toLatin1 (),
QByteArray :: Base64UrlEncoding | QByteArray :: OmitTrailingEquals ));
}
QByteArray decodeBytes ( const QString & value )
{
return QByteArray :: fromBase64 ( value . toLatin1 (),
QByteArray :: Base64UrlEncoding | QByteArray :: OmitTrailingEquals );
}
QString findArgumentValue ( const QStringList & arguments , const QString & prefix )
{
for ( const QString & argument : arguments )
if ( argument . startsWith ( prefix ))
return argument . mid ( prefix . size ());
return QString ();
}
bool writeBytesToFile ( const QString & path , const QByteArray & bytes , QString * errorMessage )
{
QDir dir ( QFileInfo ( path ). path ());
if ( ! dir . exists () && ! dir . mkpath ( "." ))
{
if ( errorMessage )
* errorMessage = QString ( "Cannot create directory: %1" ). arg ( dir . path ());
return false ;
}
QSaveFile output ( path );
if ( ! output . open ( QIODevice :: WriteOnly ))
{
if ( errorMessage )
* errorMessage = QString ( "Cannot open file for writing: %1" ). arg ( output . errorString ());
return false ;
}
if ( output . write ( bytes ) != bytes . size ())
{
if ( errorMessage )
* errorMessage = QString ( "Cannot write full file: %1" ). arg ( output . errorString ());
output . cancelWriting ();
return false ;
}
if ( ! output . commit ())
{
if ( errorMessage )
* errorMessage = QString ( "Cannot commit file: %1" ). arg ( output . errorString ());
return false ;
}
if ( errorMessage )
errorMessage -> clear ();
return true ;
}
bool removeFile ( const QString & path , QString * errorMessage )
{
if ( ! QFile :: exists ( path ))
{
if ( errorMessage )
errorMessage -> clear ();
return true ;
}
if ( QFile :: remove ( path ))
{
if ( errorMessage )
errorMessage -> clear ();
return true ;
}
if ( errorMessage )
* errorMessage = QString ( "Cannot remove file: %1" ). arg ( path );
return false ;
}
bool writeConfigValueToFile ( const QString & configPath , const QString & key ,
const QString & value , QString * errorMessage )
{
QFile input ( configPath );
QJsonObject config ;
if ( input . exists ())
{
if ( ! input . open ( QIODevice :: ReadOnly ))
{
if ( errorMessage )
* errorMessage = 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 ())
{
if ( errorMessage )
* errorMessage = QString ( "Invalid config JSON: %1" ). arg ( parseError . errorString ());
return false ;
}
config = document . object ();
}
config . insert ( key , value );
QDir dir ( QFileInfo ( configPath ). path ());
if ( ! dir . exists () && ! dir . mkpath ( "." ))
{
if ( errorMessage )
* errorMessage = QString ( "Cannot create config directory: %1" ). arg ( dir . path ());
return false ;
}
const QByteArray payload = QJsonDocument ( config ). toJson ( QJsonDocument :: Indented );
return writeBytesToFile ( configPath , payload , errorMessage );
}
2026-07-15 06:39:45 +00:00
bool isRegistryManagedConfigKey ( const QString & key )
{
2026-07-16 08:14:51 +00:00
// 服务端地址是编译期 qrc 配置,不进入注册表。
// 其他运行配置会在 Launcher 首次启动时导入注册表,之后以注册表为准。
2026-07-15 06:39:45 +00:00
return key != kApiBaseUrlKey ;
}
2026-07-16 08:14:51 +00:00
bool isPathInsideDirectory ( const QString & path , const QString & directory )
{
if ( path . isEmpty () || directory . isEmpty ())
return false ;
QString normalizedPath = QDir :: cleanPath ( QFileInfo ( path ). absoluteFilePath ());
QString normalizedDirectory = QDir :: cleanPath ( QFileInfo ( directory ). absoluteFilePath ());
#ifdef Q_OS_WIN
normalizedPath = normalizedPath . toLower ();
normalizedDirectory = normalizedDirectory . toLower ();
#endif
return normalizedPath == normalizedDirectory
|| normalizedPath . startsWith ( normalizedDirectory + QDir :: separator ());
}
bool isUserDataPath ( const QString & path )
{
return isPathInsideDirectory ( path , ConfigHelper :: instance (). dataRoot ());
}
2026-07-15 06:39:45 +00:00
QJsonObject registryManagedConfigObject ( const QJsonObject & source )
{
QJsonObject result ;
for ( auto it = source . constBegin (); it != source . constEnd (); ++ it )
{
if ( isRegistryManagedConfigKey ( it . key ()))
result . insert ( it . key (), it . value ());
}
return result ;
}
2026-07-14 01:37:06 +00:00
#ifdef Q_OS_WIN
QString windowsErrorMessage ( DWORD errorCode )
{
if ( errorCode == ERROR_CANCELLED )
return QCoreApplication :: translate ( "ConfigHelper" , "The user canceled the administrator permission confirmation." );
return QCoreApplication :: translate ( "ConfigHelper" , "Windows error %1" ). arg ( errorCode );
}
bool runElevatedSelfCommand ( const QStringList & arguments , const QString & targetPath ,
const QString & originalError , QString * errorMessage )
{
2026-07-16 08:14:51 +00:00
// 安装到 C:\Program Files 等目录时,普通用户不能直接修改配置或运行态文件。
// 这里不让主进程一直以管理员运行,而是在确实需要写入时临时拉起自身完成单次写入。
2026-07-14 01:37:06 +00:00
const QMessageBox :: StandardButton choice = QMessageBox :: question (
nullptr ,
QCoreApplication :: translate ( "ConfigHelper" , "Administrator Permission Required" ),
QCoreApplication :: translate ( "ConfigHelper" , "The current installation directory requires administrator permission to save configuration. \n\n Target file: %1 \n Reason: %2 \n\n Click OK, then choose Yes in the Windows permission confirmation dialog." )
. arg ( QDir :: toNativeSeparators ( targetPath ), originalError ),
QMessageBox :: Ok | QMessageBox :: Cancel ,
QMessageBox :: Ok );
if ( choice != QMessageBox :: Ok )
{
if ( errorMessage )
* errorMessage = QCoreApplication :: translate ( "ConfigHelper" , "The user canceled the administrator permission request." );
return false ;
}
const QString executable = QCoreApplication :: applicationFilePath ();
if ( executable . isEmpty () || ! QFileInfo :: exists ( executable ))
{
if ( errorMessage )
* errorMessage = QStringLiteral ( "Cannot locate current executable for elevated config write." );
return false ;
}
const QString parameters = arguments . join ( QLatin1Char ( ' ' ));
const QString workingDir = QFileInfo ( executable ). absolutePath ();
std :: wstring verb = L "runas" ;
std :: wstring file = executable . toStdWString ();
std :: wstring params = parameters . toStdWString ();
std :: wstring directory = workingDir . toStdWString ();
SHELLEXECUTEINFOW info {};
info . cbSize = sizeof ( info );
info . fMask = SEE_MASK_NOCLOSEPROCESS ;
info . lpVerb = verb . c_str ();
info . lpFile = file . c_str ();
info . lpParameters = params . c_str ();
info . lpDirectory = directory . c_str ();
info . nShow = SW_HIDE ;
if ( ! ShellExecuteExW ( & info ))
{
const DWORD err = GetLastError ();
if ( errorMessage )
* errorMessage = QStringLiteral ( "Cannot request administrator permission: %1" ). arg ( windowsErrorMessage ( err ));
return false ;
}
WaitForSingleObject ( info . hProcess , INFINITE );
DWORD exitCode = 1 ;
GetExitCodeProcess ( info . hProcess , & exitCode );
CloseHandle ( info . hProcess );
if ( exitCode != 0 )
{
if ( errorMessage )
* errorMessage = QStringLiteral ( "Elevated config write failed with exit code %1." ). arg ( exitCode );
return false ;
}
if ( errorMessage )
errorMessage -> clear ();
return true ;
}
bool writeConfigValueWithElevation ( const QString & configPath , const QString & key ,
const QString & value , const QString & originalError ,
QString * errorMessage )
{
const QStringList arguments {
kElevatedConfigSetFlag ,
kConfigPathPrefix + encodeArgument ( configPath ),
kConfigKeyPrefix + encodeArgument ( key ),
kConfigValuePrefix + encodeArgument ( value )
};
return runElevatedSelfCommand ( arguments , configPath , originalError , errorMessage );
}
bool writeBytesWithElevation ( const QString & path , const QByteArray & bytes ,
const QString & originalError , QString * errorMessage )
{
const QStringList arguments {
kElevatedFileWriteFlag ,
kFilePathPrefix + encodeArgument ( path ),
kFileDataPrefix + encodeBytes ( bytes )
};
return runElevatedSelfCommand ( arguments , path , originalError , errorMessage );
}
bool removeFileWithElevation ( const QString & path , const QString & originalError , QString * errorMessage )
{
const QStringList arguments {
kElevatedFileRemoveFlag ,
kFilePathPrefix + encodeArgument ( path )
};
return runElevatedSelfCommand ( arguments , path , originalError , errorMessage );
}
#endif
}
ConfigHelper & ConfigHelper :: instance ()
{
static ConfigHelper obj ;
return obj ;
}
2026-07-14 08:39:41 +00:00
QString ConfigHelper :: executableNameForCurrentPlatform ( const QString & configuredValue ,
const QString & fallbackBaseName )
{
QString name = configuredValue . trimmed ();
if ( name . isEmpty ())
name = fallbackBaseName . trimmed ();
#ifdef Q_OS_WIN
const QString fileName = QFileInfo ( name ). fileName ();
if ( ! fileName . endsWith ( QStringLiteral ( ".exe" ), Qt :: CaseInsensitive )
&& QFileInfo ( fileName ). suffix (). isEmpty ()) {
name += QStringLiteral ( ".exe" );
}
#else
if ( name . endsWith ( QStringLiteral ( ".exe" ), Qt :: CaseInsensitive ))
name . chop ( 4 );
#endif
return name ;
}
2026-07-14 01:37:06 +00:00
int ConfigHelper :: runElevatedWriteCommandIfRequested ()
{
const QStringList arguments = QCoreApplication :: arguments ();
if ( arguments . contains ( kElevatedConfigSetFlag ))
{
const QString configPath = decodeArgument ( findArgumentValue ( arguments , kConfigPathPrefix ));
const QString key = decodeArgument ( findArgumentValue ( arguments , kConfigKeyPrefix ));
const QString value = decodeArgument ( findArgumentValue ( arguments , kConfigValuePrefix ));
if ( configPath . isEmpty () || key . isEmpty ())
{
qWarning () << "Elevated config write arguments are incomplete." ;
return 2 ;
}
QString errorMessage ;
if ( ! writeConfigValueToFile ( configPath , key , value , & errorMessage ))
{
qWarning () << "Elevated config write failed:" << errorMessage ;
return 3 ;
}
return 0 ;
}
if ( arguments . contains ( kElevatedFileWriteFlag ))
{
const QString path = decodeArgument ( findArgumentValue ( arguments , kFilePathPrefix ));
const QByteArray bytes = decodeBytes ( findArgumentValue ( arguments , kFileDataPrefix ));
if ( path . isEmpty ())
{
qWarning () << "Elevated file write arguments are incomplete." ;
return 2 ;
}
QString errorMessage ;
if ( ! writeBytesToFile ( path , bytes , & errorMessage ))
{
qWarning () << "Elevated file write failed:" << errorMessage ;
return 3 ;
}
return 0 ;
}
if ( arguments . contains ( kElevatedFileRemoveFlag ))
{
const QString path = decodeArgument ( findArgumentValue ( arguments , kFilePathPrefix ));
if ( path . isEmpty ())
{
qWarning () << "Elevated file remove arguments are incomplete." ;
return 2 ;
}
QString errorMessage ;
if ( ! removeFile ( path , & errorMessage ))
{
qWarning () << "Elevated file remove failed:" << errorMessage ;
return 3 ;
}
return 0 ;
}
return - 1 ;
}
bool ConfigHelper :: writeFileWithElevationIfNeeded ( const QString & path , const QByteArray & data ,
QString * errorMessage )
{
QString localError ;
if ( writeBytesToFile ( path , data , & localError ))
{
if ( errorMessage )
errorMessage -> clear ();
return true ;
}
#ifdef Q_OS_WIN
2026-07-16 08:14:51 +00:00
if ( isUserDataPath ( path ))
{
if ( errorMessage )
* errorMessage = localError ;
return false ;
}
2026-07-14 01:37:06 +00:00
if ( data . size () > 24 * 1024 )
{
if ( errorMessage )
* errorMessage = QString ( "File is too large for elevated inline write: %1 bytes. Original error: %2" )
. arg ( data . size ()). arg ( localError );
return false ;
}
return writeBytesWithElevation ( path , data , localError , errorMessage );
#else
if ( errorMessage )
* errorMessage = localError ;
return false ;
#endif
}
bool ConfigHelper :: removeFileWithElevationIfNeeded ( const QString & path , QString * errorMessage )
{
QString localError ;
if ( removeFile ( path , & localError ))
{
if ( errorMessage )
errorMessage -> clear ();
return true ;
}
#ifdef Q_OS_WIN
2026-07-16 08:14:51 +00:00
if ( isUserDataPath ( path ))
{
if ( errorMessage )
* errorMessage = localError ;
return false ;
}
2026-07-14 01:37:06 +00:00
return removeFileWithElevation ( path , localError , errorMessage );
#else
if ( errorMessage )
* errorMessage = localError ;
return false ;
#endif
}
2026-07-10 02:45:13 +00:00
2026-07-14 01:37:06 +00:00
ConfigHelper :: ConfigHelper ()
{
m_configPath = QApplication :: applicationDirPath () + "/config/app_config.json" ;
m_registryInstallId = QString :: fromLatin1 ( QCryptographicHash :: hash (
QDir :: cleanPath ( QApplication :: applicationDirPath ()). toUtf8 (),
QCryptographicHash :: Sha256 ). toHex ());
migrateLegacyIniIfNeeded ();
syncRegistryFromConfigFileIfChanged ();
2026-07-15 06:39:45 +00:00
removeRegistryValue ( kApiBaseUrlKey );
2026-07-14 01:37:06 +00:00
qDebug () << "Loading app config path:" << m_configPath ;
qDebug () << "File exists?" << QFile :: exists ( m_configPath );
qDebug () << "Registry installation id:" << m_registryInstallId ;
}
2026-07-10 02:45:13 +00:00
QString ConfigHelper :: configPath () const
{
return m_configPath ;
}
QString ConfigHelper :: installRoot () const
{
QString relativeRoot = getValue ( "Runtime" , "install_root" ). trimmed ();
if ( relativeRoot . isEmpty ())
relativeRoot = "." ;
return QDir :: cleanPath ( QDir ( runtimeRoot ()). filePath ( relativeRoot ));
}
2026-07-16 08:14:51 +00:00
QString ConfigHelper :: runtimeRoot () const
{
return QDir :: cleanPath ( QApplication :: applicationDirPath ());
}
QString ConfigHelper :: dataRoot () const
{
QString base = QStandardPaths :: writableLocation ( QStandardPaths :: GenericDataLocation );
if ( base . isEmpty ())
base = QDir :: homePath ();
return QDir :: cleanPath ( QDir ( base ). filePath (
QStringLiteral ( "Marsco/UpdateClientSDK/installations/%1" ). arg ( m_registryInstallId )));
}
QString ConfigHelper :: dataConfigDir () const
{
return QDir ( dataRoot ()). filePath ( QStringLiteral ( "config" ));
}
QString ConfigHelper :: clientIdentityPath () const
{
return QDir ( dataConfigDir ()). filePath ( QStringLiteral ( "client_identity.dat" ));
}
QString ConfigHelper :: policyPath () const
{
return QDir ( dataConfigDir ()). filePath ( QStringLiteral ( "version_policy.dat" ));
}
QString ConfigHelper :: localStatePath () const
{
return QDir ( dataConfigDir ()). filePath ( QStringLiteral ( "local_state.json" ));
}
QString ConfigHelper :: updateRoot () const
{
return QDir ( dataRoot ()). filePath ( "update" );
}
2026-07-10 02:45:13 +00:00
QString ConfigHelper :: runtimeRelativePath () const
{
QString relative = QDir :: fromNativeSeparators ( QDir ( installRoot ()). relativeFilePath ( runtimeRoot ()));
relative = QDir :: cleanPath ( relative );
if ( relative == "." )
return QString ();
while ( relative . startsWith ( "./" ))
relative = relative . mid ( 2 );
return relative . trimmed ();
}
2026-07-14 01:37:06 +00:00
QString ConfigHelper :: lastError () const
{
return m_error ;
}
void ConfigHelper :: enterRegistryGroup ( QSettings & settings ) const
{
settings . beginGroup ( kRegistryInstallationsGroup );
settings . beginGroup ( m_registryInstallId );
}
bool ConfigHelper :: syncRegistryFromConfigFileIfChanged ()
{
QFile file ( m_configPath );
if ( ! file . exists ())
return true ;
if ( ! file . open ( QIODevice :: ReadOnly ))
{
m_error = QStringLiteral ( "Cannot open config for reading: %1" ). arg ( file . errorString ());
return false ;
}
QJsonParseError parseError ;
const QByteArray raw = file . readAll ();
const QJsonDocument document = QJsonDocument :: fromJson ( raw , & parseError );
if ( parseError . error != QJsonParseError :: NoError || ! document . isObject ())
{
m_error = QStringLiteral ( "Invalid config JSON: %1" ). arg ( parseError . errorString ());
return false ;
}
2026-07-15 06:39:45 +00:00
const QJsonObject config = registryManagedConfigObject ( document . object ());
const QByteArray normalizedConfig = QJsonDocument ( config ). toJson ( QJsonDocument :: Compact );
2026-07-14 01:37:06 +00:00
const QString sourceHash = QString :: fromLatin1 (
QCryptographicHash :: hash ( normalizedConfig , QCryptographicHash :: Sha256 ). toHex ());
QSettings settings ( QSettings :: NativeFormat , QSettings :: UserScope ,
kRegistryOrganization , kRegistryApplication );
enterRegistryGroup ( settings );
settings . beginGroup ( kRegistryMetaGroup );
const QString previousHash = settings . value ( kRegistrySourceHashKey ). toString ();
settings . endGroup ();
if ( previousHash == sourceHash )
return true ;
2026-07-14 08:39:41 +00:00
if ( config . isEmpty ())
{
settings . beginGroup ( kRegistryMetaGroup );
settings . setValue ( kRegistrySourceHashKey , sourceHash );
settings . setValue ( kRegistrySourcePathKey , QDir :: toNativeSeparators ( QFileInfo ( m_configPath ). absoluteFilePath ()));
settings . setValue ( kRegistryRuntimeRootKey , QDir :: toNativeSeparators ( QApplication :: applicationDirPath ()));
settings . setValue ( kRegistryImportedAtKey , QDateTime :: currentDateTimeUtc (). toString ( Qt :: ISODate ));
settings . endGroup ();
settings . sync ();
if ( settings . status () != QSettings :: NoError )
{
m_error = QStringLiteral ( "Cannot sync empty app_config.json marker to registry." );
return false ;
}
m_error . clear ();
qDebug () << "app_config.json is empty; keeping existing registry configuration." ;
return true ;
}
2026-07-14 01:37:06 +00:00
const bool configChangedAfterPreviousImport = ! previousHash . isEmpty ();
settings . beginGroup ( kRegistryConfigGroup );
settings . remove ( QString ());
for ( auto it = config . constBegin (); it != config . constEnd (); ++ it )
settings . setValue ( it . key (), it . value (). toVariant ());
settings . endGroup ();
settings . beginGroup ( kRegistryMetaGroup );
settings . setValue ( kRegistrySourceHashKey , sourceHash );
settings . setValue ( kRegistrySourcePathKey , QDir :: toNativeSeparators ( QFileInfo ( m_configPath ). absoluteFilePath ()));
settings . setValue ( kRegistryRuntimeRootKey , QDir :: toNativeSeparators ( QApplication :: applicationDirPath ()));
settings . setValue ( kRegistryImportedAtKey , QDateTime :: currentDateTimeUtc (). toString ( Qt :: ISODate ));
settings . endGroup ();
settings . sync ();
if ( settings . status () != QSettings :: NoError )
{
m_error = QStringLiteral ( "Cannot sync config to registry." );
return false ;
}
m_error . clear ();
qDebug () << "Synced app_config.json to registry installation id:" << m_registryInstallId ;
if ( configChangedAfterPreviousImport )
{
const QStringList staleFiles {
2026-07-16 08:14:51 +00:00
clientIdentityPath (),
policyPath (),
localStatePath ()
2026-07-14 01:37:06 +00:00
};
for ( const QString & staleFile : staleFiles )
{
QString removeError ;
2026-07-16 08:14:51 +00:00
if ( ! removeFile ( staleFile , & removeError ))
2026-07-14 01:37:06 +00:00
{
m_error = QStringLiteral ( "Cannot remove stale runtime file after config change: %1. %2" )
. arg ( staleFile , removeError );
return false ;
}
}
2026-07-14 08:39:41 +00:00
qDebug () << "Removed stale client identity, policy and local state after app_config.json changed." ;
}
if ( ! config . isEmpty () && ! sanitizeConfigFileAfterImport ( settings ))
{
qWarning () << "Cannot sanitize app_config.json after registry import:" << m_error ;
return true ;
}
return true ;
}
bool ConfigHelper :: sanitizeConfigFileAfterImport ( QSettings & settings )
{
const QJsonObject emptyConfig ;
const QByteArray normalizedEmptyConfig = QJsonDocument ( emptyConfig ). toJson ( QJsonDocument :: Compact );
const QByteArray emptyFileBytes = QJsonDocument ( emptyConfig ). toJson ( QJsonDocument :: Indented );
const QString sanitizedHash = QString :: fromLatin1 (
QCryptographicHash :: hash ( normalizedEmptyConfig , QCryptographicHash :: Sha256 ). toHex ());
QString writeError ;
2026-07-16 08:14:51 +00:00
if ( ! writeBytesToFile ( m_configPath , emptyFileBytes , & writeError ))
2026-07-14 08:39:41 +00:00
{
2026-07-16 08:14:51 +00:00
// 清空 app_config.json 只是为了减少明文配置暴露,不是启动必需步骤。
// 如果安装目录或文件只读,不再为了清空源配置弹 UAC;运行配置已经写入 HKCU 注册表。
m_error = QStringLiteral ( "Cannot clear app_config.json after registry import without elevation: %1" ). arg ( writeError );
2026-07-14 08:39:41 +00:00
return false ;
2026-07-14 01:37:06 +00:00
}
2026-07-14 08:39:41 +00:00
settings . beginGroup ( kRegistryMetaGroup );
settings . setValue ( kRegistrySourceHashKey , sanitizedHash );
settings . setValue ( kRegistrySourcePathKey , QDir :: toNativeSeparators ( QFileInfo ( m_configPath ). absoluteFilePath ()));
settings . setValue ( kRegistryRuntimeRootKey , QDir :: toNativeSeparators ( QApplication :: applicationDirPath ()));
settings . setValue ( kRegistryImportedAtKey , QDateTime :: currentDateTimeUtc (). toString ( Qt :: ISODate ));
settings . endGroup ();
settings . sync ();
if ( settings . status () != QSettings :: NoError )
{
m_error = QStringLiteral ( "Cannot update registry source hash after clearing app_config.json." );
return false ;
}
m_error . clear ();
qDebug () << "Cleared app_config.json after importing configuration to registry." ;
2026-07-14 01:37:06 +00:00
return true ;
}
2026-07-15 06:39:45 +00:00
bool ConfigHelper :: removeRegistryValue ( const QString & key ) const
{
QSettings settings ( QSettings :: NativeFormat , QSettings :: UserScope ,
kRegistryOrganization , kRegistryApplication );
enterRegistryGroup ( settings );
settings . beginGroup ( kRegistryConfigGroup );
settings . remove ( key );
settings . endGroup ();
settings . sync ();
return settings . status () == QSettings :: NoError ;
}
QString ConfigHelper :: readEmbeddedValue ( const QString & key ) const
{
if ( key != kApiBaseUrlKey )
return QString ();
QFile file ( kEmbeddedServerConfigPath );
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 (). trimmed ();
}
2026-07-14 01:37:06 +00:00
bool ConfigHelper :: readRegistryValue ( const QString & key , QString * value ) const
{
QSettings settings ( QSettings :: NativeFormat , QSettings :: UserScope ,
kRegistryOrganization , kRegistryApplication );
enterRegistryGroup ( settings );
settings . beginGroup ( kRegistryConfigGroup );
if ( ! settings . contains ( key ))
{
settings . endGroup ();
return false ;
}
if ( value )
* value = settings . value ( key ). toString ();
settings . endGroup ();
return true ;
}
bool ConfigHelper :: writeRegistryValue ( const QString & key , const QString & value )
{
QSettings settings ( QSettings :: NativeFormat , QSettings :: UserScope ,
kRegistryOrganization , kRegistryApplication );
enterRegistryGroup ( settings );
settings . beginGroup ( kRegistryConfigGroup );
settings . setValue ( key , value );
settings . endGroup ();
settings . sync ();
if ( settings . status () != QSettings :: NoError )
{
m_error = QStringLiteral ( "Cannot write config value to registry: %1" ). arg ( key );
return false ;
}
m_error . clear ();
return true ;
}
QString ConfigHelper :: readFileValue ( const QString & key ) const
{
2026-07-15 06:39:45 +00:00
if ( ! isRegistryManagedConfigKey ( key ))
return QString ();
2026-07-14 01:37:06 +00:00
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 ();
}
QString ConfigHelper :: getValue ( const QString & section , const QString & key ) const
{
Q_UNUSED ( section );
2026-07-15 06:39:45 +00:00
const QString embeddedValue = readEmbeddedValue ( key );
if ( ! embeddedValue . isEmpty ())
return embeddedValue ;
if ( ! isRegistryManagedConfigKey ( key ))
return QString ();
2026-07-14 01:37:06 +00:00
QString value ;
if ( readRegistryValue ( key , & value ))
return value ;
return readFileValue ( key );
}
bool ConfigHelper :: setValue ( const QString & section , const QString & key , const QString & value )
{
Q_UNUSED ( section );
m_error . clear ();
return writeRegistryValue ( key , value );
}
2026-07-10 02:45:13 +00:00
bool ConfigHelper :: migrateLegacyIniIfNeeded ()
{
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" , "." );
2026-07-14 08:39:41 +00:00
copyText ( "Runtime" , "main_executable" , ConfigHelper :: executableNameForCurrentPlatform ( QString (), "MainApp" ));
copyText ( "Runtime" , "launcher_executable" , ConfigHelper :: executableNameForCurrentPlatform ( QString (), "Launcher" ));
copyText ( "Runtime" , "updater_executable" , ConfigHelper :: executableNameForCurrentPlatform ( QString (), "Updater" ));
copyText ( "Runtime" , "bootstrap_executable" , ConfigHelper :: executableNameForCurrentPlatform ( QString (), "Bootstrap" ));
copyText ( "Runtime" , "health_check_timeout_ms" , "15000" );
#ifdef Q_OS_WIN
config . insert ( "platform" , "windows" );
#elif defined(Q_OS_LINUX)
config . insert ( "platform" , "linux" );
#else
config . insert ( "platform" , "unknown" );
#endif
config . insert ( "arch" , "x64" );
2026-07-10 02:45:13 +00:00
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 ;
}