fe5aa5bbf0
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 删运行时编码设置。
890 lines
36 KiB
CMake
890 lines
36 KiB
CMake
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()
|