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:
@@ -0,0 +1,82 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
if(NOT UPDATE_CLIENT_SCAN_ROOT)
|
||||
message(FATAL_ERROR "UPDATE_CLIENT_SCAN_ROOT is required.")
|
||||
endif()
|
||||
|
||||
cmake_path(
|
||||
ABSOLUTE_PATH UPDATE_CLIENT_SCAN_ROOT
|
||||
NORMALIZE
|
||||
OUTPUT_VARIABLE _scan_root
|
||||
)
|
||||
if(NOT IS_DIRECTORY "${_scan_root}")
|
||||
message(FATAL_ERROR "Production source scan root does not exist: ${_scan_root}")
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE _all_files LIST_DIRECTORIES FALSE "${_scan_root}/*")
|
||||
set(_production_files)
|
||||
foreach(_candidate IN LISTS _all_files)
|
||||
cmake_path(CONVERT "${_candidate}" TO_CMAKE_PATH_LIST _normalized_candidate NORMALIZE)
|
||||
cmake_path(
|
||||
RELATIVE_PATH _normalized_candidate
|
||||
BASE_DIRECTORY "${_scan_root}"
|
||||
OUTPUT_VARIABLE _relative_candidate
|
||||
)
|
||||
string(TOLOWER "${_relative_candidate}" _candidate_lower)
|
||||
if(_candidate_lower MATCHES
|
||||
"(^|/)(translations|docs|out|build|generated|cmake-build[^/]*|\\.git)(/|$)")
|
||||
continue()
|
||||
endif()
|
||||
if(_candidate_lower STREQUAL "bootstrap/bootstrap.rc")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
cmake_path(GET _normalized_candidate FILENAME _file_name)
|
||||
cmake_path(GET _normalized_candidate EXTENSION _file_extension)
|
||||
string(TOLOWER "${_file_extension}" _file_extension)
|
||||
if(NOT _file_name STREQUAL "CMakeLists.txt"
|
||||
AND NOT _file_extension MATCHES
|
||||
"^\\.(c|cc|cpp|cxx|h|hh|hpp|cmake|ui|qrc|manifest|rc|json)$")
|
||||
continue()
|
||||
endif()
|
||||
list(APPEND _production_files "${_candidate}")
|
||||
endforeach()
|
||||
|
||||
set(_han_utf8_regex
|
||||
"(e3:[9ab][0-9a-f]:[89ab][0-9a-f]:"
|
||||
"|e[4-9]:[89ab][0-9a-f]:[89ab][0-9a-f]:"
|
||||
"|ef:a[4-9ab]:[89ab][0-9a-f]:"
|
||||
"|f0:a[0-9a-f]:[89ab][0-9a-f]:[89ab][0-9a-f]:"
|
||||
"|f0:b[01]:[89ab][0-9a-f]:[89ab][0-9a-f]:)"
|
||||
)
|
||||
string(JOIN "" _han_utf8_regex ${_han_utf8_regex})
|
||||
|
||||
set(_violations)
|
||||
foreach(_production_file IN LISTS _production_files)
|
||||
file(READ "${_production_file}" _file_hex HEX)
|
||||
string(TOLOWER "${_file_hex}" _file_hex)
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "\\1:" _file_bytes "${_file_hex}")
|
||||
if(_file_bytes MATCHES "${_han_utf8_regex}")
|
||||
cmake_path(
|
||||
RELATIVE_PATH _production_file
|
||||
BASE_DIRECTORY "${_scan_root}"
|
||||
OUTPUT_VARIABLE _relative_file
|
||||
)
|
||||
list(APPEND _violations "${_relative_file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(_violations)
|
||||
list(JOIN _violations "\n " _violation_list)
|
||||
message(FATAL_ERROR
|
||||
"Han characters are forbidden in update-client production sources:\n"
|
||||
" ${_violation_list}\n"
|
||||
"Move user-visible text to the Qt translation catalog or the approved "
|
||||
"Bootstrap STRINGTABLE resource."
|
||||
)
|
||||
endif()
|
||||
|
||||
list(LENGTH _production_files _production_file_count)
|
||||
message(STATUS
|
||||
"No-Han production source check passed (${_production_file_count} files)."
|
||||
)
|
||||
@@ -0,0 +1,329 @@
|
||||
include_guard(DIRECTORY)
|
||||
|
||||
function(_update_client_normalize_thirdparty_root)
|
||||
if(NOT UPDATE_CLIENT_THIRDPARTY_ROOT)
|
||||
message(FATAL_ERROR
|
||||
"UPDATE_CLIENT_THIRDPARTY_ROOT is empty. Set it to the project third-party "
|
||||
"root, normally <SimCAE source>/3rdparty."
|
||||
)
|
||||
endif()
|
||||
|
||||
cmake_path(
|
||||
ABSOLUTE_PATH UPDATE_CLIENT_THIRDPARTY_ROOT
|
||||
BASE_DIRECTORY "${UPDATE_CLIENT_SOURCE_DIR}"
|
||||
NORMALIZE
|
||||
OUTPUT_VARIABLE _absolute_root
|
||||
)
|
||||
if(NOT IS_DIRECTORY "${_absolute_root}")
|
||||
message(FATAL_ERROR
|
||||
"UPDATE_CLIENT_THIRDPARTY_ROOT does not exist: ${_absolute_root}\n"
|
||||
"Provision the project dependencies or pass the correct project-relative root."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(
|
||||
UPDATE_CLIENT_THIRDPARTY_ROOT
|
||||
"${_absolute_root}"
|
||||
CACHE PATH
|
||||
"Root directory for update-client third-party packages"
|
||||
FORCE
|
||||
)
|
||||
set(UPDATE_CLIENT_THIRDPARTY_ROOT "${_absolute_root}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_assert_path_under_root candidate_path description root_path)
|
||||
if(candidate_path MATCHES "^\\$<")
|
||||
message(FATAL_ERROR
|
||||
"Strict third-party validation cannot verify ${description}: ${candidate_path}"
|
||||
)
|
||||
endif()
|
||||
if(NOT EXISTS "${candidate_path}")
|
||||
message(FATAL_ERROR "${description} does not exist: ${candidate_path}")
|
||||
endif()
|
||||
|
||||
file(REAL_PATH "${root_path}" _real_root)
|
||||
file(REAL_PATH "${candidate_path}" _real_candidate)
|
||||
set(_root_for_prefix_check "${_real_root}")
|
||||
cmake_path(
|
||||
IS_PREFIX _root_for_prefix_check "${_real_candidate}"
|
||||
NORMALIZE
|
||||
_is_under_root
|
||||
)
|
||||
if(NOT _is_under_root)
|
||||
message(FATAL_ERROR
|
||||
"Strict third-party validation rejected ${description}:\n"
|
||||
" resolved path: ${_real_candidate}\n"
|
||||
" required root: ${_real_root}\n"
|
||||
"Provision OpenSSL under UPDATE_CLIENT_THIRDPARTY_ROOT or disable "
|
||||
"UPDATE_CLIENT_STRICT_THIRDPARTY only for local dependency diagnosis."
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_update_client_assert_openssl_target_under_root target_name root_path)
|
||||
set(_location_properties IMPORTED_LOCATION IMPORTED_IMPLIB)
|
||||
get_target_property(_imported_configurations "${target_name}" IMPORTED_CONFIGURATIONS)
|
||||
if(_imported_configurations AND NOT _imported_configurations MATCHES "-NOTFOUND$")
|
||||
foreach(_configuration IN LISTS _imported_configurations)
|
||||
string(TOUPPER "${_configuration}" _configuration_upper)
|
||||
list(APPEND _location_properties
|
||||
"IMPORTED_LOCATION_${_configuration_upper}"
|
||||
"IMPORTED_IMPLIB_${_configuration_upper}"
|
||||
)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
set(_has_library_location OFF)
|
||||
foreach(_property IN LISTS _location_properties)
|
||||
get_target_property(_locations "${target_name}" "${_property}")
|
||||
if(NOT _locations OR _locations MATCHES "-NOTFOUND$")
|
||||
continue()
|
||||
endif()
|
||||
foreach(_location IN LISTS _locations)
|
||||
_update_client_assert_path_under_root(
|
||||
"${_location}"
|
||||
"${target_name} ${_property}"
|
||||
"${root_path}"
|
||||
)
|
||||
set(_has_library_location ON)
|
||||
endforeach()
|
||||
endforeach()
|
||||
if(NOT _has_library_location)
|
||||
message(FATAL_ERROR
|
||||
"Strict third-party validation found no imported library location for ${target_name}."
|
||||
)
|
||||
endif()
|
||||
|
||||
get_target_property(_include_directories "${target_name}" INTERFACE_INCLUDE_DIRECTORIES)
|
||||
if(NOT _include_directories OR _include_directories MATCHES "-NOTFOUND$")
|
||||
message(FATAL_ERROR
|
||||
"Strict third-party validation found no imported include directory for ${target_name}."
|
||||
)
|
||||
endif()
|
||||
foreach(_include_directory IN LISTS _include_directories)
|
||||
if(_include_directory MATCHES "^\\$<BUILD_INTERFACE:(.*)>$")
|
||||
set(_include_directory "${CMAKE_MATCH_1}")
|
||||
elseif(_include_directory MATCHES "^\\$<INSTALL_INTERFACE:")
|
||||
continue()
|
||||
endif()
|
||||
_update_client_assert_path_under_root(
|
||||
"${_include_directory}"
|
||||
"${target_name} INTERFACE_INCLUDE_DIRECTORIES"
|
||||
"${root_path}"
|
||||
)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
function(_update_client_attach_openssl_runtime target_name runtime_dll root_path)
|
||||
_update_client_assert_path_under_root(
|
||||
"${runtime_dll}"
|
||||
"${target_name} runtime DLL"
|
||||
"${root_path}"
|
||||
)
|
||||
set_property(
|
||||
TARGET "${target_name}"
|
||||
PROPERTY UPDATE_CLIENT_RUNTIME_DLL "${runtime_dll}"
|
||||
)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_discover_openssl_runtime)
|
||||
if(NOT WIN32)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(_runtime_directory "${UPDATE_CLIENT_THIRDPARTY_ROOT}/OpenSSL/bin")
|
||||
if(NOT IS_DIRECTORY "${_runtime_directory}")
|
||||
set(_runtime_directory "${UPDATE_CLIENT_THIRDPARTY_ROOT}/openssl/bin")
|
||||
endif()
|
||||
if(NOT IS_DIRECTORY "${_runtime_directory}")
|
||||
message(FATAL_ERROR
|
||||
"Windows OpenSSL runtime directory is missing under "
|
||||
"UPDATE_CLIENT_THIRDPARTY_ROOT: expected OpenSSL/bin. "
|
||||
"Release output requires vendored libssl and libcrypto DLLs."
|
||||
)
|
||||
endif()
|
||||
|
||||
file(GLOB _runtime_candidates CONFIGURE_DEPENDS "${_runtime_directory}/*.dll")
|
||||
set(_ssl_runtime_candidates)
|
||||
set(_crypto_runtime_candidates)
|
||||
foreach(_runtime_candidate IN LISTS _runtime_candidates)
|
||||
cmake_path(GET _runtime_candidate FILENAME _runtime_name)
|
||||
string(TOLOWER "${_runtime_name}" _runtime_name_lower)
|
||||
if(_runtime_name_lower MATCHES "^libssl.*\\.dll$")
|
||||
list(APPEND _ssl_runtime_candidates "${_runtime_candidate}")
|
||||
elseif(_runtime_name_lower MATCHES "^libcrypto.*\\.dll$")
|
||||
list(APPEND _crypto_runtime_candidates "${_runtime_candidate}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
list(LENGTH _ssl_runtime_candidates _ssl_runtime_count)
|
||||
list(LENGTH _crypto_runtime_candidates _crypto_runtime_count)
|
||||
if(NOT _ssl_runtime_count EQUAL 1 OR NOT _crypto_runtime_count EQUAL 1)
|
||||
message(FATAL_ERROR
|
||||
"Windows Release runtime deployment requires exactly one libssl*.dll and "
|
||||
"one libcrypto*.dll in ${_runtime_directory}. Found SSL=${_ssl_runtime_count}, "
|
||||
"Crypto=${_crypto_runtime_count}. Keep only the runtime matching the imported "
|
||||
"OpenSSL package and target architecture."
|
||||
)
|
||||
endif()
|
||||
|
||||
list(GET _ssl_runtime_candidates 0 _ssl_runtime)
|
||||
list(GET _crypto_runtime_candidates 0 _crypto_runtime)
|
||||
_update_client_attach_openssl_runtime(
|
||||
OpenSSL::SSL "${_ssl_runtime}" "${UPDATE_CLIENT_THIRDPARTY_ROOT}"
|
||||
)
|
||||
_update_client_attach_openssl_runtime(
|
||||
OpenSSL::Crypto "${_crypto_runtime}" "${UPDATE_CLIENT_THIRDPARTY_ROOT}"
|
||||
)
|
||||
message(STATUS "update-client OpenSSL SSL runtime: ${_ssl_runtime}")
|
||||
message(STATUS "update-client OpenSSL Crypto runtime: ${_crypto_runtime}")
|
||||
endfunction()
|
||||
|
||||
function(update_client_find_dependencies)
|
||||
_update_client_normalize_thirdparty_root()
|
||||
|
||||
set(_package_prefixes
|
||||
"${UPDATE_CLIENT_THIRDPARTY_ROOT}"
|
||||
"${UPDATE_CLIENT_THIRDPARTY_ROOT}/OpenSSL"
|
||||
"${UPDATE_CLIENT_THIRDPARTY_ROOT}/openssl"
|
||||
)
|
||||
foreach(_prefix IN LISTS _package_prefixes)
|
||||
if(IS_DIRECTORY "${_prefix}")
|
||||
list(PREPEND CMAKE_PREFIX_PATH "${_prefix}")
|
||||
endif()
|
||||
endforeach()
|
||||
list(REMOVE_DUPLICATES CMAKE_PREFIX_PATH)
|
||||
set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH}" PARENT_SCOPE)
|
||||
|
||||
find_package(Qt5 5.15 CONFIG QUIET COMPONENTS Core Gui Network Widgets)
|
||||
if(NOT Qt5_FOUND)
|
||||
message(FATAL_ERROR
|
||||
"Qt 5.15 or newer was not found through its CONFIG package.\n"
|
||||
"The parent project should discover Qt first and pass Qt5_DIR. For a "
|
||||
"standalone build, set Qt5_DIR to the directory containing Qt5Config.cmake "
|
||||
"or add the Qt installation prefix to CMAKE_PREFIX_PATH."
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT OPENSSL_ROOT_DIR)
|
||||
if(IS_DIRECTORY "${UPDATE_CLIENT_THIRDPARTY_ROOT}/OpenSSL")
|
||||
set(OPENSSL_ROOT_DIR "${UPDATE_CLIENT_THIRDPARTY_ROOT}/OpenSSL")
|
||||
elseif(IS_DIRECTORY "${UPDATE_CLIENT_THIRDPARTY_ROOT}/openssl")
|
||||
set(OPENSSL_ROOT_DIR "${UPDATE_CLIENT_THIRDPARTY_ROOT}/openssl")
|
||||
endif()
|
||||
endif()
|
||||
find_package(OpenSSL QUIET COMPONENTS SSL Crypto)
|
||||
if(NOT OpenSSL_FOUND OR NOT TARGET OpenSSL::SSL OR NOT TARGET OpenSSL::Crypto)
|
||||
message(FATAL_ERROR
|
||||
"OpenSSL with SSL and Crypto imported targets was not found.\n"
|
||||
"Provision OpenSSL under ${UPDATE_CLIENT_THIRDPARTY_ROOT}/OpenSSL or set "
|
||||
"OPENSSL_ROOT_DIR to an OpenSSL package inside UPDATE_CLIENT_THIRDPARTY_ROOT. "
|
||||
"Do not pass raw include or library directories."
|
||||
)
|
||||
endif()
|
||||
|
||||
if(UPDATE_CLIENT_STRICT_THIRDPARTY)
|
||||
_update_client_assert_openssl_target_under_root(
|
||||
OpenSSL::SSL "${UPDATE_CLIENT_THIRDPARTY_ROOT}"
|
||||
)
|
||||
_update_client_assert_openssl_target_under_root(
|
||||
OpenSSL::Crypto "${UPDATE_CLIENT_THIRDPARTY_ROOT}"
|
||||
)
|
||||
endif()
|
||||
_update_client_discover_openssl_runtime()
|
||||
|
||||
if(WIN32 AND UPDATE_CLIENT_DEPLOY_QT_RUNTIME)
|
||||
if(NOT TARGET Qt5::qmake)
|
||||
message(FATAL_ERROR
|
||||
"Qt5::qmake is unavailable, so windeployqt cannot be located. "
|
||||
"Use a complete Qt 5 CONFIG package or disable "
|
||||
"UPDATE_CLIENT_DEPLOY_QT_RUNTIME when deployment is handled by the caller."
|
||||
)
|
||||
endif()
|
||||
get_target_property(_qmake_executable Qt5::qmake IMPORTED_LOCATION)
|
||||
cmake_path(GET _qmake_executable PARENT_PATH _qt_binary_dir)
|
||||
find_program(
|
||||
UPDATE_CLIENT_WINDEPLOYQT_EXECUTABLE
|
||||
NAMES windeployqt
|
||||
HINTS "${_qt_binary_dir}"
|
||||
NO_DEFAULT_PATH
|
||||
DOC "Qt deployment tool used by update-client"
|
||||
)
|
||||
if(NOT UPDATE_CLIENT_WINDEPLOYQT_EXECUTABLE)
|
||||
message(FATAL_ERROR
|
||||
"windeployqt was not found next to ${_qmake_executable}. "
|
||||
"Install the Qt deployment tools or configure with "
|
||||
"-DUPDATE_CLIENT_DEPLOY_QT_RUNTIME=OFF when the caller deploys Qt."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(update_client_deploy_openssl_runtime target_name)
|
||||
if(NOT TARGET "${target_name}")
|
||||
message(FATAL_ERROR "Unknown update-client target: ${target_name}")
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
return()
|
||||
endif()
|
||||
|
||||
foreach(_openssl_target IN ITEMS OpenSSL::SSL OpenSSL::Crypto)
|
||||
get_target_property(
|
||||
_runtime_dll "${_openssl_target}" UPDATE_CLIENT_RUNTIME_DLL
|
||||
)
|
||||
if(NOT _runtime_dll OR _runtime_dll MATCHES "-NOTFOUND$")
|
||||
message(FATAL_ERROR
|
||||
"${_openssl_target} has no validated UPDATE_CLIENT_RUNTIME_DLL property."
|
||||
)
|
||||
endif()
|
||||
cmake_path(GET _runtime_dll FILENAME _runtime_name)
|
||||
add_custom_command(
|
||||
TARGET "${target_name}"
|
||||
POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${_runtime_dll}"
|
||||
"$<TARGET_FILE_DIR:${target_name}>/${_runtime_name}"
|
||||
COMMENT "Deploying ${_runtime_name} for ${target_name}"
|
||||
VERBATIM
|
||||
)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
function(update_client_install_openssl_runtime destination)
|
||||
if(NOT WIN32)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(_runtime_dlls)
|
||||
foreach(_openssl_target IN ITEMS OpenSSL::SSL OpenSSL::Crypto)
|
||||
get_target_property(
|
||||
_runtime_dll "${_openssl_target}" UPDATE_CLIENT_RUNTIME_DLL
|
||||
)
|
||||
if(NOT _runtime_dll OR _runtime_dll MATCHES "-NOTFOUND$")
|
||||
message(FATAL_ERROR
|
||||
"${_openssl_target} has no validated runtime DLL for installation."
|
||||
)
|
||||
endif()
|
||||
list(APPEND _runtime_dlls "${_runtime_dll}")
|
||||
endforeach()
|
||||
install(FILES ${_runtime_dlls} DESTINATION "${destination}")
|
||||
endfunction()
|
||||
|
||||
function(update_client_enable_qt_deployment target_name)
|
||||
if(NOT TARGET "${target_name}")
|
||||
message(FATAL_ERROR "Unknown update-client target: ${target_name}")
|
||||
endif()
|
||||
if(NOT WIN32 OR NOT UPDATE_CLIENT_DEPLOY_QT_RUNTIME)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
TARGET "${target_name}"
|
||||
POST_BUILD
|
||||
COMMAND "${UPDATE_CLIENT_WINDEPLOYQT_EXECUTABLE}"
|
||||
"$<IF:$<CONFIG:Debug>,--debug,--release>"
|
||||
"$<TARGET_FILE:${target_name}>"
|
||||
COMMENT "Deploying Qt runtime for ${target_name}"
|
||||
VERBATIM
|
||||
)
|
||||
endfunction()
|
||||
@@ -0,0 +1,889 @@
|
||||
include_guard(DIRECTORY)
|
||||
|
||||
function(_update_client_decode_utf8 codepoints_variable widths_variable input_value)
|
||||
string(HEX "${input_value}" _hex)
|
||||
string(LENGTH "${_hex}" _hex_length)
|
||||
set(_codepoints)
|
||||
set(_widths)
|
||||
set(_offset 0)
|
||||
while(_offset LESS _hex_length)
|
||||
string(SUBSTRING "${_hex}" ${_offset} 2 _first_hex)
|
||||
math(EXPR _first "0x${_first_hex}")
|
||||
if(_first LESS 128)
|
||||
set(_width 1)
|
||||
set(_codepoint ${_first})
|
||||
elseif(_first LESS 224)
|
||||
math(EXPR _second_offset "${_offset} + 2")
|
||||
string(SUBSTRING "${_hex}" ${_second_offset} 2 _second_hex)
|
||||
math(EXPR _second "0x${_second_hex}")
|
||||
math(EXPR _codepoint "((${_first} & 31) << 6) | (${_second} & 63)")
|
||||
set(_width 2)
|
||||
elseif(_first LESS 240)
|
||||
math(EXPR _second_offset "${_offset} + 2")
|
||||
math(EXPR _third_offset "${_offset} + 4")
|
||||
string(SUBSTRING "${_hex}" ${_second_offset} 2 _second_hex)
|
||||
string(SUBSTRING "${_hex}" ${_third_offset} 2 _third_hex)
|
||||
math(EXPR _second "0x${_second_hex}")
|
||||
math(EXPR _third "0x${_third_hex}")
|
||||
math(EXPR _codepoint
|
||||
"((${_first} & 15) << 12) | ((${_second} & 63) << 6) | (${_third} & 63)"
|
||||
)
|
||||
set(_width 3)
|
||||
else()
|
||||
math(EXPR _second_offset "${_offset} + 2")
|
||||
math(EXPR _third_offset "${_offset} + 4")
|
||||
math(EXPR _fourth_offset "${_offset} + 6")
|
||||
string(SUBSTRING "${_hex}" ${_second_offset} 2 _second_hex)
|
||||
string(SUBSTRING "${_hex}" ${_third_offset} 2 _third_hex)
|
||||
string(SUBSTRING "${_hex}" ${_fourth_offset} 2 _fourth_hex)
|
||||
math(EXPR _second "0x${_second_hex}")
|
||||
math(EXPR _third "0x${_third_hex}")
|
||||
math(EXPR _fourth "0x${_fourth_hex}")
|
||||
string(CONCAT _expression
|
||||
"((${_first} & 7) << 18) | ((${_second} & 63) << 12) | "
|
||||
"((${_third} & 63) << 6) | (${_fourth} & 63)"
|
||||
)
|
||||
math(EXPR _codepoint "${_expression}")
|
||||
set(_width 4)
|
||||
endif()
|
||||
list(APPEND _codepoints "${_codepoint}")
|
||||
list(APPEND _widths "${_width}")
|
||||
math(EXPR _offset "${_offset} + (${_width} * 2)")
|
||||
endwhile()
|
||||
set(${codepoints_variable} "${_codepoints}" PARENT_SCOPE)
|
||||
set(${widths_variable} "${_widths}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_codepoint_is_qt_space output_variable codepoint)
|
||||
if((codepoint GREATER_EQUAL 9 AND codepoint LESS_EQUAL 13)
|
||||
OR codepoint EQUAL 32
|
||||
OR codepoint EQUAL 160
|
||||
OR codepoint EQUAL 5760
|
||||
OR (codepoint GREATER_EQUAL 8192 AND codepoint LESS_EQUAL 8202)
|
||||
OR codepoint EQUAL 8232
|
||||
OR codepoint EQUAL 8233
|
||||
OR codepoint EQUAL 8239
|
||||
OR codepoint EQUAL 8287
|
||||
OR codepoint EQUAL 12288)
|
||||
set(${output_variable} TRUE PARENT_SCOPE)
|
||||
else()
|
||||
set(${output_variable} FALSE PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_update_client_qt_trim output_variable input_value)
|
||||
_update_client_decode_utf8(_codepoints _widths "${input_value}")
|
||||
list(LENGTH _codepoints _codepoint_count)
|
||||
set(_first_non_space_byte -1)
|
||||
set(_last_non_space_end_byte -1)
|
||||
set(_byte_offset 0)
|
||||
if(_codepoint_count GREATER 0)
|
||||
math(EXPR _last_index "${_codepoint_count} - 1")
|
||||
foreach(_index RANGE 0 ${_last_index})
|
||||
list(GET _codepoints ${_index} _codepoint)
|
||||
list(GET _widths ${_index} _width)
|
||||
_update_client_codepoint_is_qt_space(_is_space "${_codepoint}")
|
||||
if(NOT _is_space)
|
||||
if(_first_non_space_byte EQUAL -1)
|
||||
set(_first_non_space_byte ${_byte_offset})
|
||||
endif()
|
||||
math(EXPR _last_non_space_end_byte "${_byte_offset} + ${_width}")
|
||||
endif()
|
||||
math(EXPR _byte_offset "${_byte_offset} + ${_width}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(_first_non_space_byte EQUAL -1)
|
||||
set(_trimmed "")
|
||||
else()
|
||||
math(EXPR _trimmed_length
|
||||
"${_last_non_space_end_byte} - ${_first_non_space_byte}"
|
||||
)
|
||||
string(SUBSTRING "${input_value}" ${_first_non_space_byte} ${_trimmed_length} _trimmed)
|
||||
endif()
|
||||
set(${output_variable} "${_trimmed}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_string_properties
|
||||
control_variable space_variable utf16_length_variable input_value)
|
||||
_update_client_decode_utf8(_codepoints _widths "${input_value}")
|
||||
set(_has_control FALSE)
|
||||
set(_has_space FALSE)
|
||||
set(_utf16_length 0)
|
||||
foreach(_codepoint IN LISTS _codepoints)
|
||||
if(_codepoint LESS_EQUAL 31
|
||||
OR (_codepoint GREATER_EQUAL 127 AND _codepoint LESS_EQUAL 159))
|
||||
set(_has_control TRUE)
|
||||
endif()
|
||||
_update_client_codepoint_is_qt_space(_is_space "${_codepoint}")
|
||||
if(_is_space)
|
||||
set(_has_space TRUE)
|
||||
endif()
|
||||
if(_codepoint GREATER 65535)
|
||||
math(EXPR _utf16_length "${_utf16_length} + 2")
|
||||
else()
|
||||
math(EXPR _utf16_length "${_utf16_length} + 1")
|
||||
endif()
|
||||
endforeach()
|
||||
set(${control_variable} ${_has_control} PARENT_SCOPE)
|
||||
set(${space_variable} ${_has_space} PARENT_SCOPE)
|
||||
set(${utf16_length_variable} ${_utf16_length} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_require_json_string json_text field_name)
|
||||
string(JSON _field_type ERROR_VARIABLE _field_error TYPE "${json_text}" "${field_name}")
|
||||
if(NOT _field_error STREQUAL "NOTFOUND")
|
||||
message(FATAL_ERROR
|
||||
"Product config is missing required field '${field_name}': ${_field_error}"
|
||||
)
|
||||
endif()
|
||||
if(NOT _field_type STREQUAL "STRING")
|
||||
message(FATAL_ERROR
|
||||
"Product config field '${field_name}' must be a string, got ${_field_type}."
|
||||
)
|
||||
endif()
|
||||
|
||||
string(JSON _field_value GET "${json_text}" "${field_name}")
|
||||
_update_client_qt_trim(_trimmed_field_value "${_field_value}")
|
||||
if(_trimmed_field_value STREQUAL "")
|
||||
message(FATAL_ERROR "Product config field '${field_name}' must not be empty.")
|
||||
endif()
|
||||
|
||||
set("UPDATE_CLIENT_CONFIG_${field_name}" "${_field_value}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_require_positive_integer json_text field_name)
|
||||
string(JSON _field_type ERROR_VARIABLE _field_error TYPE "${json_text}" "${field_name}")
|
||||
if(NOT _field_error STREQUAL "NOTFOUND")
|
||||
message(FATAL_ERROR
|
||||
"Product config is missing required field '${field_name}': ${_field_error}"
|
||||
)
|
||||
endif()
|
||||
if(NOT _field_type STREQUAL "STRING" AND NOT _field_type STREQUAL "NUMBER")
|
||||
message(FATAL_ERROR
|
||||
"Product config field '${field_name}' must be a string or number, got ${_field_type}."
|
||||
)
|
||||
endif()
|
||||
string(JSON _field_value GET "${json_text}" "${field_name}")
|
||||
if(NOT _field_value MATCHES "^[1-9][0-9]*$")
|
||||
message(FATAL_ERROR
|
||||
"Product config field '${field_name}' must contain a positive integer."
|
||||
)
|
||||
endif()
|
||||
string(LENGTH "${_field_value}" _field_length)
|
||||
if(_field_length GREATER 10
|
||||
OR (_field_length EQUAL 10 AND _field_value STRGREATER "2147483647"))
|
||||
message(FATAL_ERROR
|
||||
"Product config field '${field_name}' exceeds the supported integer range."
|
||||
)
|
||||
endif()
|
||||
set("UPDATE_CLIENT_CONFIG_${field_name}" "${_field_value}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_prepare_qdir_path
|
||||
relative_variable clean_variable absolute_variable input_value)
|
||||
_update_client_qt_trim(_relative "${input_value}")
|
||||
string(REPLACE "\\" "/" _relative "${_relative}")
|
||||
set(_path_for_cmake "${_relative}")
|
||||
cmake_path(IS_ABSOLUTE _path_for_cmake _is_absolute)
|
||||
set(_clean "${_relative}")
|
||||
cmake_path(NORMAL_PATH _clean)
|
||||
if(NOT _clean STREQUAL "/")
|
||||
string(REGEX REPLACE "/+$" "" _clean "${_clean}")
|
||||
endif()
|
||||
if(_clean STREQUAL "")
|
||||
set(_clean ".")
|
||||
endif()
|
||||
set(${relative_variable} "${_relative}" PARENT_SCOPE)
|
||||
set(${clean_variable} "${_clean}" PARENT_SCOPE)
|
||||
set(${absolute_variable} ${_is_absolute} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_validate_install_root output_variable input_value validation_context)
|
||||
_update_client_prepare_qdir_path(_relative _clean _is_absolute "${input_value}")
|
||||
if(_is_absolute OR _relative MATCHES ":" OR NOT _clean STREQUAL _relative)
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: field 'install_root' must be a normalized relative path."
|
||||
)
|
||||
endif()
|
||||
set(${output_variable} "${_relative}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_validate_strict_relative_path field_name input_value validation_context)
|
||||
_update_client_prepare_qdir_path(_relative _clean _is_absolute "${input_value}")
|
||||
if(_relative STREQUAL ""
|
||||
OR _is_absolute
|
||||
OR _relative MATCHES ":"
|
||||
OR NOT _clean STREQUAL _relative
|
||||
OR _relative MATCHES "(^|/)\\.\\.?(/|$)")
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: field '${field_name}' must be a safe relative path."
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_update_client_split_leading_parents
|
||||
count_variable remainder_variable input_value)
|
||||
set(_remaining "${input_value}")
|
||||
set(_count 0)
|
||||
if(_remaining STREQUAL ".")
|
||||
set(_remaining "")
|
||||
else()
|
||||
while(_remaining MATCHES "^\\.\\./")
|
||||
math(EXPR _count "${_count} + 1")
|
||||
string(SUBSTRING "${_remaining}" 3 -1 _remaining)
|
||||
endwhile()
|
||||
if(_remaining STREQUAL "..")
|
||||
math(EXPR _count "${_count} + 1")
|
||||
set(_remaining "")
|
||||
endif()
|
||||
endif()
|
||||
set(${count_variable} ${_count} PARENT_SCOPE)
|
||||
set(${remainder_variable} "${_remaining}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_validate_main_executable input_value install_root validation_context)
|
||||
_update_client_prepare_qdir_path(_relative _clean _is_absolute "${input_value}")
|
||||
if(_relative STREQUAL ""
|
||||
OR _is_absolute
|
||||
OR _relative MATCHES ":"
|
||||
OR NOT _clean STREQUAL _relative
|
||||
OR _relative STREQUAL "."
|
||||
OR _relative STREQUAL "..")
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: field 'main_executable' must stay within install_root."
|
||||
)
|
||||
endif()
|
||||
|
||||
_update_client_split_leading_parents(
|
||||
_install_parent_count _install_remainder "${install_root}"
|
||||
)
|
||||
_update_client_split_leading_parents(
|
||||
_main_parent_count _main_remainder "${_relative}"
|
||||
)
|
||||
set(_is_within_root FALSE)
|
||||
if(_install_remainder STREQUAL "")
|
||||
if(_main_parent_count LESS_EQUAL _install_parent_count)
|
||||
set(_is_within_root TRUE)
|
||||
endif()
|
||||
elseif(_main_parent_count EQUAL _install_parent_count)
|
||||
if(_main_remainder STREQUAL _install_remainder)
|
||||
set(_is_within_root TRUE)
|
||||
else()
|
||||
string(FIND "${_main_remainder}" "${_install_remainder}/" _root_prefix_index)
|
||||
if(_root_prefix_index EQUAL 0)
|
||||
set(_is_within_root TRUE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
if(NOT _is_within_root)
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: field 'main_executable' must stay within install_root."
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_update_client_count_ipv6_groups count_variable valid_variable input_value)
|
||||
set(_remaining "${input_value}")
|
||||
set(_count 0)
|
||||
set(_valid TRUE)
|
||||
while(NOT _remaining STREQUAL "")
|
||||
string(FIND "${_remaining}" ":" _separator_index)
|
||||
if(_separator_index EQUAL -1)
|
||||
set(_group "${_remaining}")
|
||||
set(_remaining "")
|
||||
else()
|
||||
string(SUBSTRING "${_remaining}" 0 ${_separator_index} _group)
|
||||
math(EXPR _next_index "${_separator_index} + 1")
|
||||
string(SUBSTRING "${_remaining}" ${_next_index} -1 _remaining)
|
||||
endif()
|
||||
string(LENGTH "${_group}" _group_length)
|
||||
if(_group STREQUAL ""
|
||||
OR _group_length GREATER 4
|
||||
OR NOT _group MATCHES "^[0-9A-Fa-f]+$")
|
||||
set(_valid FALSE)
|
||||
break()
|
||||
endif()
|
||||
math(EXPR _count "${_count} + 1")
|
||||
endwhile()
|
||||
set(${count_variable} ${_count} PARENT_SCOPE)
|
||||
set(${valid_variable} ${_valid} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_is_valid_ipv6 output_variable input_value)
|
||||
set(_valid FALSE)
|
||||
if(input_value MATCHES "^[0-9A-Fa-f:]+$")
|
||||
string(FIND "${input_value}" "::" _compression_index)
|
||||
if(_compression_index EQUAL -1)
|
||||
_update_client_count_ipv6_groups(_group_count _groups_valid "${input_value}")
|
||||
if(_groups_valid AND _group_count EQUAL 8)
|
||||
set(_valid TRUE)
|
||||
endif()
|
||||
else()
|
||||
string(SUBSTRING "${input_value}" 0 ${_compression_index} _left)
|
||||
math(EXPR _right_index "${_compression_index} + 2")
|
||||
string(SUBSTRING "${input_value}" ${_right_index} -1 _right)
|
||||
string(FIND "${_right}" "::" _second_compression_index)
|
||||
if(NOT _left MATCHES ":$"
|
||||
AND NOT _right MATCHES "^:"
|
||||
AND _second_compression_index EQUAL -1)
|
||||
_update_client_count_ipv6_groups(_left_count _left_valid "${_left}")
|
||||
_update_client_count_ipv6_groups(_right_count _right_valid "${_right}")
|
||||
math(EXPR _explicit_group_count "${_left_count} + ${_right_count}")
|
||||
if(_left_valid AND _right_valid AND _explicit_group_count LESS 8)
|
||||
set(_valid TRUE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
set(${output_variable} ${_valid} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_is_valid_url_port output_variable input_value)
|
||||
set(_valid FALSE)
|
||||
if(input_value MATCHES "^[0-9]+$")
|
||||
string(REGEX REPLACE "^0+" "" _significant "${input_value}")
|
||||
if(_significant STREQUAL "")
|
||||
set(_significant 0)
|
||||
endif()
|
||||
string(LENGTH "${_significant}" _length)
|
||||
if(_length LESS 6
|
||||
AND NOT (_length EQUAL 5 AND _significant STRGREATER "65535"))
|
||||
set(_valid TRUE)
|
||||
endif()
|
||||
endif()
|
||||
set(${output_variable} ${_valid} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_validate_http_url input_value validation_context)
|
||||
set(_valid TRUE)
|
||||
if(NOT input_value MATCHES "^[Hh][Tt][Tt][Pp]([Ss])?://")
|
||||
set(_valid FALSE)
|
||||
else()
|
||||
string(REGEX REPLACE
|
||||
"^[Hh][Tt][Tt][Pp]([Ss])?://" "" _url_remainder "${input_value}"
|
||||
)
|
||||
string(REGEX MATCH "^[^/?#]*" _authority "${_url_remainder}")
|
||||
if(_authority STREQUAL "")
|
||||
set(_valid FALSE)
|
||||
else()
|
||||
_update_client_string_properties(
|
||||
_url_has_control _unused_space _unused_length "${input_value}"
|
||||
)
|
||||
_update_client_string_properties(
|
||||
_unused_control _authority_has_space _unused_length "${_authority}"
|
||||
)
|
||||
if(_url_has_control OR _authority_has_space)
|
||||
set(_valid FALSE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(_valid)
|
||||
string(REGEX REPLACE "^.*@" "" _host_port "${_authority}")
|
||||
if(_host_port MATCHES "^\\[")
|
||||
if(NOT _host_port MATCHES "^\\[([^]]+)\\](.*)$")
|
||||
set(_valid FALSE)
|
||||
else()
|
||||
set(_host "${CMAKE_MATCH_1}")
|
||||
set(_port_suffix "${CMAKE_MATCH_2}")
|
||||
_update_client_is_valid_ipv6(_host_valid "${_host}")
|
||||
if(NOT _host_valid)
|
||||
set(_valid FALSE)
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
if(_host_port MATCHES "[\\[\\]]")
|
||||
set(_valid FALSE)
|
||||
else()
|
||||
string(REGEX REPLACE "[^:]" "" _colons "${_host_port}")
|
||||
string(LENGTH "${_colons}" _colon_count)
|
||||
if(_colon_count GREATER 1)
|
||||
set(_valid FALSE)
|
||||
elseif(_colon_count EQUAL 1)
|
||||
string(FIND "${_host_port}" ":" _colon_index)
|
||||
string(SUBSTRING "${_host_port}" 0 ${_colon_index} _host)
|
||||
math(EXPR _port_index "${_colon_index} + 1")
|
||||
string(SUBSTRING "${_host_port}" ${_port_index} -1 _port)
|
||||
set(_port_suffix ":${_port}")
|
||||
else()
|
||||
set(_host "${_host_port}")
|
||||
set(_port_suffix "")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(_valid AND _host STREQUAL "")
|
||||
set(_valid FALSE)
|
||||
endif()
|
||||
if(_valid AND NOT _port_suffix STREQUAL "")
|
||||
if(NOT _port_suffix MATCHES "^:(.+)$")
|
||||
set(_valid FALSE)
|
||||
else()
|
||||
_update_client_is_valid_url_port(_port_valid "${CMAKE_MATCH_1}")
|
||||
if(NOT _port_valid)
|
||||
set(_valid FALSE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
if(_valid)
|
||||
_update_client_decode_utf8(_host_codepoints _unused_widths "${_host}")
|
||||
foreach(_codepoint IN LISTS _host_codepoints)
|
||||
if(_codepoint EQUAL 34
|
||||
OR _codepoint EQUAL 35
|
||||
OR _codepoint EQUAL 47
|
||||
OR _codepoint EQUAL 58
|
||||
OR _codepoint EQUAL 60
|
||||
OR _codepoint EQUAL 62
|
||||
OR _codepoint EQUAL 63
|
||||
OR _codepoint EQUAL 64
|
||||
OR _codepoint EQUAL 91
|
||||
OR _codepoint EQUAL 92
|
||||
OR _codepoint EQUAL 93
|
||||
OR _codepoint EQUAL 94
|
||||
OR _codepoint EQUAL 96
|
||||
OR _codepoint EQUAL 123
|
||||
OR _codepoint EQUAL 124
|
||||
OR _codepoint EQUAL 125)
|
||||
set(_valid FALSE)
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(NOT _valid)
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: field 'api_base_url' must be a valid HTTP or HTTPS URL with a non-empty host."
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_update_client_is_valid_uuid output_variable input_value)
|
||||
set(_uuid "${input_value}")
|
||||
string(TOLOWER "${_uuid}" _uuid_lower)
|
||||
if(_uuid_lower MATCHES "^urn:uuid:")
|
||||
string(SUBSTRING "${_uuid}" 9 -1 _uuid)
|
||||
endif()
|
||||
if(_uuid MATCHES "^\\{.*\\}$")
|
||||
string(LENGTH "${_uuid}" _uuid_length)
|
||||
if(_uuid_length GREATER_EQUAL 2)
|
||||
math(EXPR _inner_length "${_uuid_length} - 2")
|
||||
string(SUBSTRING "${_uuid}" 1 ${_inner_length} _uuid)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(_valid FALSE)
|
||||
if(_uuid STREQUAL "")
|
||||
set(_valid TRUE)
|
||||
else()
|
||||
string(LENGTH "${_uuid}" _uuid_length)
|
||||
if(_uuid_length EQUAL 36 AND _uuid MATCHES "^[0-9A-Fa-f-]+$")
|
||||
string(SUBSTRING "${_uuid}" 8 1 _hyphen_1)
|
||||
string(SUBSTRING "${_uuid}" 13 1 _hyphen_2)
|
||||
string(SUBSTRING "${_uuid}" 18 1 _hyphen_3)
|
||||
string(SUBSTRING "${_uuid}" 23 1 _hyphen_4)
|
||||
string(REPLACE "-" "" _uuid_digits "${_uuid}")
|
||||
string(LENGTH "${_uuid_digits}" _digits_length)
|
||||
string(TOLOWER "${_uuid_digits}" _uuid_digits)
|
||||
if(_hyphen_1 STREQUAL "-"
|
||||
AND _hyphen_2 STREQUAL "-"
|
||||
AND _hyphen_3 STREQUAL "-"
|
||||
AND _hyphen_4 STREQUAL "-"
|
||||
AND _digits_length EQUAL 32
|
||||
AND NOT _uuid_digits STREQUAL "00000000000000000000000000000000")
|
||||
set(_valid TRUE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
set(${output_variable} ${_valid} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_validate_product_config json_text validation_context)
|
||||
string(JSON _root_type ERROR_VARIABLE _json_error TYPE "${json_text}")
|
||||
if(NOT _json_error STREQUAL "NOTFOUND")
|
||||
message(FATAL_ERROR "Invalid JSON in ${validation_context}: ${_json_error}")
|
||||
endif()
|
||||
if(NOT _root_type STREQUAL "OBJECT")
|
||||
message(FATAL_ERROR "Invalid ${validation_context}: root must be a JSON object.")
|
||||
endif()
|
||||
|
||||
string(JSON _schema_type ERROR_VARIABLE _schema_error TYPE "${json_text}" schema_version)
|
||||
if(NOT _schema_error STREQUAL "NOTFOUND" OR NOT _schema_type STREQUAL "NUMBER")
|
||||
message(FATAL_ERROR "Invalid ${validation_context}: schema_version must be the number 1.")
|
||||
endif()
|
||||
string(JSON _schema_version GET "${json_text}" schema_version)
|
||||
if(NOT _schema_version EQUAL 1)
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: unsupported schema_version ${_schema_version}."
|
||||
)
|
||||
endif()
|
||||
|
||||
foreach(_required_field IN ITEMS
|
||||
app_id
|
||||
app_name
|
||||
channel
|
||||
launch_token
|
||||
api_base_url
|
||||
client_token
|
||||
temp_folder
|
||||
install_root
|
||||
main_executable
|
||||
launcher_executable
|
||||
updater_executable
|
||||
bootstrap_executable
|
||||
platform
|
||||
arch)
|
||||
_update_client_require_json_string("${json_text}" "${_required_field}")
|
||||
endforeach()
|
||||
foreach(_numeric_field IN ITEMS
|
||||
client_protocol request_timeout_ms health_check_timeout_ms)
|
||||
_update_client_require_positive_integer("${json_text}" "${_numeric_field}")
|
||||
endforeach()
|
||||
|
||||
foreach(_identifier_field IN ITEMS app_id channel platform arch)
|
||||
set(_identifier_value "${UPDATE_CLIENT_CONFIG_${_identifier_field}}")
|
||||
if(NOT _identifier_value MATCHES "^[A-Za-z0-9._-]+$")
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: field '${_identifier_field}' contains unsupported characters."
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
_update_client_validate_http_url(
|
||||
"${UPDATE_CLIENT_CONFIG_api_base_url}" "${validation_context}"
|
||||
)
|
||||
_update_client_validate_install_root(
|
||||
_install_root "${UPDATE_CLIENT_CONFIG_install_root}" "${validation_context}"
|
||||
)
|
||||
_update_client_validate_main_executable(
|
||||
"${UPDATE_CLIENT_CONFIG_main_executable}" "${_install_root}" "${validation_context}"
|
||||
)
|
||||
foreach(_path_field IN ITEMS
|
||||
temp_folder launcher_executable updater_executable bootstrap_executable)
|
||||
_update_client_validate_strict_relative_path(
|
||||
"${_path_field}" "${UPDATE_CLIENT_CONFIG_${_path_field}}" "${validation_context}"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
foreach(_mutable_field IN ITEMS current_version license_key device_id installation_id)
|
||||
string(JSON _root_mutable_type ERROR_VARIABLE _root_mutable_error
|
||||
TYPE "${json_text}" "${_mutable_field}")
|
||||
if(_root_mutable_error STREQUAL "NOTFOUND")
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: mutable field '${_mutable_field}' must be under initial_state."
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
string(JSON _initial_state_type ERROR_VARIABLE _initial_state_error
|
||||
TYPE "${json_text}" initial_state)
|
||||
if(NOT _initial_state_error STREQUAL "NOTFOUND"
|
||||
OR NOT _initial_state_type STREQUAL "OBJECT")
|
||||
message(FATAL_ERROR "Invalid ${validation_context}: initial_state must be an object.")
|
||||
endif()
|
||||
set(_allowed_initial_state current_version license_key device_id installation_id)
|
||||
set(_has_current_version FALSE)
|
||||
string(JSON _initial_state_length LENGTH "${json_text}" initial_state)
|
||||
if(_initial_state_length GREATER 0)
|
||||
math(EXPR _initial_state_last "${_initial_state_length} - 1")
|
||||
foreach(_index RANGE 0 ${_initial_state_last})
|
||||
string(JSON _initial_key MEMBER "${json_text}" initial_state ${_index})
|
||||
list(FIND _allowed_initial_state "${_initial_key}" _allowed_index)
|
||||
string(JSON _initial_type TYPE "${json_text}" initial_state "${_initial_key}")
|
||||
if(_allowed_index EQUAL -1 OR NOT _initial_type STREQUAL "STRING")
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: initial_state.${_initial_key} is unsupported or is not a string."
|
||||
)
|
||||
endif()
|
||||
|
||||
string(JSON _initial_value GET "${json_text}" initial_state "${_initial_key}")
|
||||
_update_client_qt_trim(_initial_value "${_initial_value}")
|
||||
if(_initial_key STREQUAL "current_version")
|
||||
set(_has_current_version TRUE)
|
||||
string(LENGTH "${_initial_value}" _version_length)
|
||||
if(_version_length GREATER 128
|
||||
OR NOT _initial_value MATCHES "^[0-9A-Za-z][0-9A-Za-z._+-]*$")
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: initial_state.current_version has an invalid value."
|
||||
)
|
||||
endif()
|
||||
elseif(_initial_key STREQUAL "installation_id")
|
||||
_update_client_is_valid_uuid(_uuid_valid "${_initial_value}")
|
||||
if(NOT _uuid_valid)
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: initial_state.installation_id must be empty or a non-null UUID."
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
_update_client_string_properties(
|
||||
_has_control _unused_space _value_length "${_initial_value}"
|
||||
)
|
||||
if(_initial_key STREQUAL "device_id")
|
||||
set(_maximum_length 256)
|
||||
else()
|
||||
set(_maximum_length 4096)
|
||||
endif()
|
||||
if(_has_control OR _value_length GREATER _maximum_length)
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: initial_state.${_initial_key} contains a control character or exceeds ${_maximum_length} UTF-16 code units."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
if(NOT _has_current_version)
|
||||
message(FATAL_ERROR
|
||||
"Invalid ${validation_context}: initial_state.current_version is required."
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(_update_client_xml_escape output_variable input_value)
|
||||
set(_escaped "${input_value}")
|
||||
string(REPLACE "&" "&" _escaped "${_escaped}")
|
||||
string(REPLACE "<" "<" _escaped "${_escaped}")
|
||||
string(REPLACE ">" ">" _escaped "${_escaped}")
|
||||
set(${output_variable} "${_escaped}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(_update_client_json_quote output_variable input_value)
|
||||
set(_escaped "${input_value}")
|
||||
string(REPLACE "\\" "\\\\" _escaped "${_escaped}")
|
||||
string(REPLACE "\"" "\\\"" _escaped "${_escaped}")
|
||||
string(REPLACE "\r" "\\r" _escaped "${_escaped}")
|
||||
string(REPLACE "\n" "\\n" _escaped "${_escaped}")
|
||||
string(REPLACE "\t" "\\t" _escaped "${_escaped}")
|
||||
set(${output_variable} "\"${_escaped}\"" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(update_client_select_product_config output_variable)
|
||||
set(_primary_config "${UPDATE_CLIENT_SOURCE_DIR}/config/config.json")
|
||||
set(_fallback_config "${UPDATE_CLIENT_SOURCE_DIR}/config/app_config.example.json")
|
||||
file(GLOB _product_config_candidates CONFIGURE_DEPENDS
|
||||
"${UPDATE_CLIENT_SOURCE_DIR}/config/config.json"
|
||||
"${UPDATE_CLIENT_SOURCE_DIR}/config/app_config.example.json")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${_primary_config}"
|
||||
"${_fallback_config}"
|
||||
)
|
||||
|
||||
if(UPDATE_CLIENT_PRODUCT_CONFIG_FILE)
|
||||
cmake_path(
|
||||
ABSOLUTE_PATH UPDATE_CLIENT_PRODUCT_CONFIG_FILE
|
||||
BASE_DIRECTORY "${UPDATE_CLIENT_SOURCE_DIR}"
|
||||
NORMALIZE
|
||||
OUTPUT_VARIABLE _selected_config
|
||||
)
|
||||
if(NOT EXISTS "${_selected_config}" OR IS_DIRECTORY "${_selected_config}")
|
||||
message(FATAL_ERROR
|
||||
"UPDATE_CLIENT_PRODUCT_CONFIG_FILE does not exist or is not a file: "
|
||||
"${_selected_config}"
|
||||
)
|
||||
endif()
|
||||
set(_selection_reason "caller-provided UPDATE_CLIENT_PRODUCT_CONFIG_FILE")
|
||||
elseif(EXISTS "${_primary_config}")
|
||||
set(_selected_config "${_primary_config}")
|
||||
set(_selection_reason "project config/config.json")
|
||||
elseif(EXISTS "${_fallback_config}")
|
||||
set(_selected_config "${_fallback_config}")
|
||||
set(_selection_reason "fallback config/app_config.example.json")
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"No update-client product config was found. Create "
|
||||
"${_primary_config} or restore ${_fallback_config}."
|
||||
)
|
||||
endif()
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_selected_config}")
|
||||
set(UPDATE_CLIENT_SELECTED_PRODUCT_CONFIG_FILE
|
||||
"${_selected_config}" CACHE INTERNAL "Selected update-client product config" FORCE)
|
||||
|
||||
file(READ "${_selected_config}" _config_json)
|
||||
_update_client_validate_product_config(
|
||||
"${_config_json}" "selected product config ${_selected_config}"
|
||||
)
|
||||
|
||||
message(STATUS "update-client product config source: ${_selected_config}")
|
||||
message(STATUS "update-client product config selection: ${_selection_reason}")
|
||||
set(${output_variable} "${_selected_config}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(update_client_generate_normalized_product_config output_variable source_file)
|
||||
file(READ "${source_file}" _source_json)
|
||||
string(JSON _normalized_json GET "${_source_json}")
|
||||
|
||||
string(JSON _source_launch_token GET "${_source_json}" launch_token)
|
||||
set(_effective_launch_token "${UPDATE_CLIENT_LAUNCH_TOKEN}")
|
||||
if(_effective_launch_token STREQUAL ""
|
||||
AND _source_launch_token MATCHES "(ChangeMe|CHANGE_ME|YOUR_[A-Z_]+)")
|
||||
if(NOT UPDATE_CLIENT_GENERATED_LAUNCH_TOKEN)
|
||||
string(RANDOM LENGTH 64
|
||||
ALPHABET "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
_generated_launch_token)
|
||||
set(UPDATE_CLIENT_GENERATED_LAUNCH_TOKEN "${_generated_launch_token}"
|
||||
CACHE INTERNAL "Generated update-client launch token" FORCE)
|
||||
endif()
|
||||
set(_effective_launch_token "${UPDATE_CLIENT_GENERATED_LAUNCH_TOKEN}")
|
||||
message(STATUS "Generated a build-local launch token for the example product config")
|
||||
endif()
|
||||
if(NOT _effective_launch_token STREQUAL "")
|
||||
_update_client_json_quote(_launch_token_json "${_effective_launch_token}")
|
||||
string(JSON _normalized_json SET "${_normalized_json}"
|
||||
launch_token "${_launch_token_json}")
|
||||
endif()
|
||||
|
||||
foreach(_override_variable IN ITEMS
|
||||
UPDATE_CLIENT_MAIN_EXECUTABLE
|
||||
UPDATE_CLIENT_LAUNCHER_EXECUTABLE
|
||||
UPDATE_CLIENT_UPDATER_EXECUTABLE
|
||||
UPDATE_CLIENT_BOOTSTRAP_EXECUTABLE
|
||||
UPDATE_CLIENT_PLATFORM
|
||||
UPDATE_CLIENT_ARCH)
|
||||
if("${${_override_variable}}" STREQUAL "")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
string(REGEX REPLACE "^UPDATE_CLIENT_" "" _json_field "${_override_variable}")
|
||||
string(TOLOWER "${_json_field}" _json_field)
|
||||
_update_client_json_quote(_json_value "${${_override_variable}}")
|
||||
string(JSON _normalized_json SET "${_normalized_json}" "${_json_field}" "${_json_value}")
|
||||
message(STATUS
|
||||
"update-client product config override: ${_json_field}=${${_override_variable}}"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
_update_client_validate_product_config(
|
||||
"${_normalized_json}" "normalized product config after build overrides"
|
||||
)
|
||||
|
||||
set(_generated_dir "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
file(MAKE_DIRECTORY "${_generated_dir}")
|
||||
set(_normalized_file "${_generated_dir}/product-config.json")
|
||||
file(WRITE "${_normalized_file}" "${_normalized_json}\n")
|
||||
message(STATUS "update-client normalized product config: ${_normalized_file}")
|
||||
set(${output_variable} "${_normalized_file}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(update_client_create_embedded_resources target_name product_config_file)
|
||||
if(TARGET "${target_name}")
|
||||
message(FATAL_ERROR "Embedded resource target already exists: ${target_name}")
|
||||
endif()
|
||||
|
||||
set(_generated_dir "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
file(MAKE_DIRECTORY "${_generated_dir}")
|
||||
|
||||
cmake_path(CONVERT "${product_config_file}" TO_CMAKE_PATH_LIST _config_qrc_path NORMALIZE)
|
||||
_update_client_xml_escape(_config_qrc_path "${_config_qrc_path}")
|
||||
set(_resource_entries
|
||||
" <file alias=\"product-config.json\">${_config_qrc_path}</file>\n"
|
||||
)
|
||||
|
||||
set(_public_key "${UPDATE_CLIENT_SOURCE_DIR}/config/manifest_public_key.pem")
|
||||
if(NOT EXISTS "${_public_key}")
|
||||
message(FATAL_ERROR
|
||||
"The manifest verification public key is missing: ${_public_key}"
|
||||
)
|
||||
endif()
|
||||
cmake_path(CONVERT "${_public_key}" TO_CMAKE_PATH_LIST _public_key_qrc_path NORMALIZE)
|
||||
_update_client_xml_escape(_public_key_qrc_path "${_public_key_qrc_path}")
|
||||
string(APPEND _resource_entries
|
||||
" <file alias=\"manifest-public-key.pem\">${_public_key_qrc_path}</file>\n"
|
||||
)
|
||||
|
||||
set(_translation_catalog
|
||||
"${UPDATE_CLIENT_SOURCE_DIR}/translations/update-client_zh_CN.ts"
|
||||
)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_translation_catalog}")
|
||||
set(_generated_inputs)
|
||||
if(EXISTS "${_translation_catalog}")
|
||||
find_package(Qt5 5.15 CONFIG REQUIRED COMPONENTS LinguistTools)
|
||||
if(NOT TARGET Qt5::lrelease)
|
||||
message(FATAL_ERROR
|
||||
"Qt5::lrelease is required to compile update-client translations."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(_translation_qm "${_generated_dir}/update-client_zh_CN.qm")
|
||||
add_custom_command(
|
||||
OUTPUT "${_translation_qm}"
|
||||
COMMAND Qt5::lrelease "${_translation_catalog}" -qm "${_translation_qm}"
|
||||
DEPENDS "${_translation_catalog}" Qt5::lrelease
|
||||
COMMENT "Compiling update-client Chinese translation catalog"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND _generated_inputs "${_translation_qm}")
|
||||
cmake_path(CONVERT "${_translation_qm}" TO_CMAKE_PATH_LIST _translation_qrc_path NORMALIZE)
|
||||
_update_client_xml_escape(_translation_qrc_path "${_translation_qrc_path}")
|
||||
string(APPEND _resource_entries
|
||||
" <file alias=\"i18n/update-client_zh_CN.qm\">${_translation_qrc_path}</file>\n"
|
||||
)
|
||||
elseif(UPDATE_CLIENT_REQUIRE_TRANSLATIONS)
|
||||
message(FATAL_ERROR
|
||||
"Required translation catalog is missing: ${_translation_catalog}"
|
||||
)
|
||||
else()
|
||||
message(STATUS
|
||||
"Translation catalog is not present yet; embedded i18n will be enabled when "
|
||||
"translations/update-client_zh_CN.ts is added."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(_resource_file "${_generated_dir}/update_client_embedded.qrc")
|
||||
string(CONCAT _resource_content
|
||||
"<RCC>\n"
|
||||
" <qresource prefix=\"/simcae/update-client\">\n"
|
||||
"${_resource_entries}"
|
||||
" </qresource>\n"
|
||||
"</RCC>\n"
|
||||
)
|
||||
file(CONFIGURE
|
||||
OUTPUT "${_resource_file}"
|
||||
CONTENT "${_resource_content}"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
if(NOT TARGET Qt5::rcc)
|
||||
message(FATAL_ERROR "Qt5::rcc is required to compile update-client resources.")
|
||||
endif()
|
||||
set(_resource_source "${_generated_dir}/qrc_update_client_embedded.cpp")
|
||||
add_custom_command(
|
||||
OUTPUT "${_resource_source}"
|
||||
COMMAND Qt5::rcc
|
||||
--name update_client_embedded
|
||||
--output "${_resource_source}"
|
||||
"${_resource_file}"
|
||||
DEPENDS
|
||||
"${_resource_file}"
|
||||
"${product_config_file}"
|
||||
"${_public_key}"
|
||||
${_generated_inputs}
|
||||
Qt5::rcc
|
||||
COMMENT "Compiling update-client embedded resources"
|
||||
VERBATIM
|
||||
)
|
||||
add_library("${target_name}" OBJECT "${_resource_source}" ${_generated_inputs})
|
||||
target_link_libraries("${target_name}" PRIVATE Qt5::Core)
|
||||
set_target_properties("${target_name}" PROPERTIES
|
||||
AUTOMOC OFF
|
||||
AUTOUIC OFF
|
||||
AUTORCC OFF
|
||||
)
|
||||
|
||||
set(UPDATE_CLIENT_PRODUCT_CONFIG_RESOURCE
|
||||
":/simcae/update-client/product-config.json"
|
||||
PARENT_SCOPE
|
||||
)
|
||||
set(UPDATE_CLIENT_TRANSLATION_RESOURCE
|
||||
":/simcae/update-client/i18n/update-client_zh_CN.qm"
|
||||
PARENT_SCOPE
|
||||
)
|
||||
endfunction()
|
||||
|
||||
function(update_client_link_embedded_resources target_name)
|
||||
if(NOT TARGET "${target_name}")
|
||||
message(FATAL_ERROR "Unknown update-client target: ${target_name}")
|
||||
endif()
|
||||
if(NOT TARGET UpdateClientEmbeddedResources)
|
||||
message(FATAL_ERROR "UpdateClientEmbeddedResources has not been created.")
|
||||
endif()
|
||||
target_link_libraries("${target_name}" PRIVATE UpdateClientEmbeddedResources)
|
||||
endfunction()
|
||||
Reference in New Issue
Block a user