pindo 5.15.3 → 5.15.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 931d648cd4154c3372f8be276a3f8c1ba343c1ee521997a5b2fc593ea5e4cb18
4
- data.tar.gz: 8859ad809b6006f3709b315b6eb830eecd670eb77d49c8109a504f82519acd8d
3
+ metadata.gz: ef2974c733368b841567503a425a32f359fe03154f73809574b2c1b0a1d2201b
4
+ data.tar.gz: ac37d8ed02529bc1c487696ef1e450965bc42e8914104bc381ca1486123d35da
5
5
  SHA512:
6
- metadata.gz: cf977c0431a53abcbcc6da091da61ccbbca6dc420c03507977aef574b4cb85f45ff5f628d021ac8ece839d2fd17362527b5f260d6e76b466fa30c95513fb05ae
7
- data.tar.gz: 2b91cb9a006234e86bbc5b66040bbc54ebf7a2c75d01acb8fa44524d7811e321de1de882f3ee84b498b19e1686be73b51f46f8df51bfeb5599214e2498fa680d
6
+ metadata.gz: 23730681434d87847ef4c5f43aee490d74cfcea7dd754fa9f1f59f6018b59df99296ba67ae808a5a7520865ffebac30b952d304495ce1a2222fc3b73b1828e41
7
+ data.tar.gz: feef72ac4cfcc0671458083fb6de016e1213ca37ef28be5ff820107bca499962e06ac3e26aaef10d470a52f0df52410085b74f4cc74a92aefcd6b1914dc6d5f4
@@ -1,9 +1,27 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  require "tty-spinner"
2
5
 
3
6
  module Pindo
4
7
 
5
8
  class Funlog
6
9
 
10
+ # 确保字符串使用 UTF-8 编码,防止编码错误导致任务中断
11
+ # @param str [String] 输入字符串
12
+ # @return [String] UTF-8 编码的字符串
13
+ def self.ensure_utf8(str)
14
+ return str if str.nil?
15
+ return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
16
+
17
+ # 尝试转换为 UTF-8
18
+ begin
19
+ str.encode(Encoding::UTF_8)
20
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
21
+ # 如果转换失败,尝试使用替换字符
22
+ str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
23
+ end
24
+ end
7
25
 
8
26
  def create_spinner(info_key:nil)
9
27
  # spinner = TTY::Spinner.new("[:spinner] :#{info_key}", format: :dots_2, error_mark: "❌", success_mark: "✅")
@@ -21,15 +39,24 @@ module Pindo
21
39
 
22
40
 
23
41
  def fancyinfo_start(*args)
24
- message = args.join(" ")
25
- spinner_log_handle.update(title:message)
26
- spinner_log_handle.auto_spin
42
+ message = Funlog.ensure_utf8(args.join(" "))
43
+
44
+ # 检查是否在任务上下文中
45
+ if Funlog.in_task_context?
46
+ # 并发模式:路由到输出管理器(只写日志文件)
47
+ Funlog.current_output_sink.log_info(Funlog.current_task_id, message)
48
+ else
49
+ # 串行模式:使用 spinner
50
+ spinner_log_handle.update(title:message)
51
+ spinner_log_handle.auto_spin
52
+ end
53
+
27
54
  @spinner_log
28
55
  end
29
56
 
30
57
 
31
58
  def fancyinfo_update(*args)
32
- message = args.join(" ")
59
+ message = Funlog.ensure_utf8(args.join(" "))
33
60
 
34
61
  # 检查是否在任务上下文中
35
62
  if Funlog.in_task_context?
@@ -45,14 +72,14 @@ module Pindo
45
72
  end
46
73
 
47
74
  def fancyinfo_success(*args)
48
- message = args.join(" ")
75
+ message = Funlog.ensure_utf8(args.join(" "))
49
76
  spinner_log_handle.update(title:message)
50
77
  spinner_log_handle.success
51
78
  @spinner_log =nil
52
79
  end
53
80
 
54
81
  def fancyinfo_error(*args)
55
- message = args.join(" ")
82
+ message = Funlog.ensure_utf8(args.join(" "))
56
83
  spinner_log_handle.update(title:message)
57
84
  spinner_log_handle.error
58
85
  @spinner_log =nil
@@ -61,7 +88,7 @@ module Pindo
61
88
  # 输出警告信息(使用spinner,黄色警告标记)
62
89
  # @param args [Array] 要输出的消息
63
90
  def fancyinfo_warning(*args)
64
- message = args.join(" ")
91
+ message = Funlog.ensure_utf8(args.join(" "))
65
92
  spinner_log_handle.update(title:message)
66
93
  spinner_log_handle.stop("\e[33m⚠\e[0m")
67
94
  @spinner_log = nil
@@ -70,7 +97,7 @@ module Pindo
70
97
  # 输出静态成功信息(不使用spinner,对应 fancyinfo_success)
71
98
  # @param args [Array] 要输出的消息
72
99
  def info(*args)
73
- message = args.join(" ")
100
+ message = Funlog.ensure_utf8(args.join(" "))
74
101
 
75
102
  # 检查是否在任务上下文中
76
103
  if Funlog.in_task_context?
@@ -85,7 +112,7 @@ module Pindo
85
112
  # 输出静态警告信息(不使用spinner)
86
113
  # @param args [Array] 要输出的消息
87
114
  def warning(*args)
88
- message = args.join(" ")
115
+ message = Funlog.ensure_utf8(args.join(" "))
89
116
 
90
117
  # 检查是否在任务上下文中
91
118
  if Funlog.in_task_context?
@@ -100,7 +127,7 @@ module Pindo
100
127
  # 输出静态错误信息(不使用spinner,对应 fancyinfo_error)
101
128
  # @param args [Array] 要输出的消息
102
129
  def error(*args)
103
- message = args.join(" ")
130
+ message = Funlog.ensure_utf8(args.join(" "))
104
131
 
105
132
  # 检查是否在任务上下文中
106
133
  if Funlog.in_task_context?
@@ -1,6 +1,8 @@
1
1
  require 'fileutils'
2
2
  require 'tempfile'
3
3
  require 'nokogiri'
4
+ require 'shellwords'
5
+ require 'open3'
4
6
  require_relative 'android_project_helper'
5
7
  require_relative 'java_env_helper'
6
8
  require_relative '../../base/funlog'
@@ -49,12 +51,16 @@ module Pindo
49
51
  def self.check_aab_compliance(aab_path)
50
52
  result = ComplianceResult.new
51
53
 
54
+ # 先输出标题,使用 puts 确保始终在终端可见
55
+ puts "\n\e[1m-- 检测 AAB 文件的 Google Play 合规性 --\e[0m"
56
+
52
57
  unless File.exist?(aab_path)
58
+ puts "\e[31m✗ AAB 文件不存在: #{aab_path}\e[0m"
53
59
  result.add_issue("AAB 文件不存在: #{aab_path}")
54
60
  return result
55
61
  end
56
62
 
57
- Funlog.fancyinfo_start("Google Play 合规检测: #{File.basename(aab_path)}")
63
+ puts "\e[1m检测的 AAB 文件:#{File.basename(aab_path)}\e[0m"
58
64
 
59
65
  # 创建临时目录用于解压 AAB
60
66
  temp_dir = nil
@@ -68,9 +74,11 @@ module Pindo
68
74
  return result
69
75
  end
70
76
 
71
- # 解压 AAB 文件
72
- unless system("unzip", "-q", aab_path, "-d", temp_dir)
73
- result.add_issue("无法解压 AAB 文件")
77
+ # 解压 AAB 文件中的必要文件
78
+ extract_result = extract_required_files_from_aab(aab_path, temp_dir)
79
+ unless extract_result[:success]
80
+ puts "\e[31m✗ #{extract_result[:error_message]}\e[0m"
81
+ result.add_issue(extract_result[:error_message])
74
82
  return result
75
83
  end
76
84
 
@@ -99,6 +107,331 @@ module Pindo
99
107
 
100
108
  private
101
109
 
110
+ # 获取 UTF-8 环境变量(用于执行命令)
111
+ # @return [Hash] 环境变量哈希
112
+ def self.get_utf8_env
113
+ {
114
+ 'LANG' => 'en_US.UTF-8',
115
+ 'LC_ALL' => 'en_US.UTF-8'
116
+ }
117
+ end
118
+
119
+ # 判断错误信息是否表示文件不存在(可以安全忽略)
120
+ # @param error_text [String] 错误信息
121
+ # @return [Boolean] 如果错误表示文件不存在,返回 true
122
+ def self.is_file_not_found_error?(error_text)
123
+ error_text.include?('filename not matched') ||
124
+ error_text.include?('cannot find') ||
125
+ error_text.include?('not found') ||
126
+ error_text.empty?
127
+ end
128
+
129
+ # 解压压缩文件(AAB/APK)内部的单个文件
130
+ # 注意:内部文件路径不会被 File.expand_path 处理,保持原样
131
+ # @param archive_path [String] 压缩文件路径(AAB 或 APK)
132
+ # @param internal_file [String] 压缩文件内部的相对路径(如 base/lib/arm64-v8a/libunity.so)
133
+ # @param dest_dir [String] 解压目标目录
134
+ # @return [Hash] {success: Boolean, error: String}
135
+ def self.extract_internal_file(archive_path, internal_file, dest_dir)
136
+ begin
137
+ abs_archive_path = File.expand_path(archive_path)
138
+ abs_dest_dir = File.expand_path(dest_dir)
139
+ stdout, stderr, status = Open3.capture3(get_utf8_env, 'unzip', '-q', abs_archive_path, internal_file, '-d', abs_dest_dir)
140
+
141
+ if status.success?
142
+ return { success: true, error: nil }
143
+ else
144
+ error_text = [stderr, stdout].compact.join(' ')
145
+ if is_file_not_found_error?(error_text)
146
+ # 文件不存在,可以忽略
147
+ return { success: true, error: nil }
148
+ else
149
+ return { success: false, error: error_text }
150
+ end
151
+ end
152
+ rescue => e
153
+ return { success: false, error: e.message }
154
+ end
155
+ end
156
+
157
+ # 获取可能的 lib 目录列表(用于查找 .so 文件)
158
+ # @param temp_dir [String] 临时目录路径
159
+ # @return [Array<String>] lib 目录路径数组
160
+ def self.get_lib_dirs(temp_dir)
161
+ [
162
+ "#{temp_dir}/lib",
163
+ "#{temp_dir}/libs",
164
+ "#{temp_dir}/base/lib", # AAB文件结构
165
+ "#{temp_dir}/base/libs" # AAB文件结构
166
+ ]
167
+ end
168
+
169
+ # 在临时目录中查找指定的 .so 文件
170
+ # @param temp_dir [String] 临时目录路径
171
+ # @param so_filename [String] .so 文件名(如 libunity.so, libil2cpp.so)
172
+ # @return [Array<String>] 找到的文件路径数组
173
+ def self.find_so_files(temp_dir, so_filename)
174
+ so_files = []
175
+ get_lib_dirs(temp_dir).each do |lib_dir|
176
+ if Dir.exist?(lib_dir)
177
+ found_files = Dir.glob("#{lib_dir}/**/#{so_filename}")
178
+ so_files += found_files
179
+ end
180
+ end
181
+ so_files
182
+ end
183
+
184
+ # 判断文件是否是合规检测需要的文件
185
+ # @param filename [String] AAB 文件内部的相对路径
186
+ # @return [Boolean] 如果是需要的文件,返回 true
187
+ def self.is_required_file?(filename)
188
+ filename.start_with?('base/manifest/') ||
189
+ (filename.start_with?('base/lib/') && filename.end_with?('.so')) ||
190
+ filename == 'base.apk' ||
191
+ filename == 'base/assets/bin/Data/boot.config'
192
+ end
193
+
194
+ # 从 unzip -Z1 输出中提取文件列表
195
+ # @param output [String] unzip -Z1 的输出
196
+ # @return [Array<String>] 文件路径数组
197
+ def self.parse_unzip_z1_output(output)
198
+ files = []
199
+ output.lines.each do |line|
200
+ filename = line.strip
201
+ next if filename.empty?
202
+ files << filename if is_required_file?(filename)
203
+ end
204
+ files
205
+ end
206
+
207
+ # 从 unzip -l 输出中提取文件列表
208
+ # @param output [String] unzip -l 的输出
209
+ # @return [Array<String>] 文件路径数组
210
+ def self.parse_unzip_l_output(output)
211
+ files = []
212
+ output.lines.each do |line|
213
+ # unzip -l 输出格式:长度 日期 时间 文件名
214
+ # 跳过表头和空行
215
+ next if line.strip.empty? || line.start_with?('Archive:') ||
216
+ line.start_with?('Length') || line.start_with?('------') ||
217
+ line.match(/^\s*$/) || line.match(/^\s*\d+\s+files?\s*$/)
218
+
219
+ # 提取文件名:从行尾开始,跳过前面的数字和日期时间字段
220
+ filename_match = line.match(/^\s*\d+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+(.+)$/)
221
+ next unless filename_match
222
+
223
+ filename = filename_match[1].strip
224
+ files << filename if is_required_file?(filename)
225
+ end
226
+ files
227
+ end
228
+
229
+ # 从压缩文件中列出并提取需要的文件
230
+ # @param archive_path [String] 压缩文件路径
231
+ # @param dest_dir [String] 解压目标目录
232
+ # @return [Hash] {success: Boolean, error_messages: Array<String>}
233
+ def self.extract_required_files(archive_path, dest_dir)
234
+ error_messages = []
235
+ failed = false
236
+
237
+ # 优先使用 unzip -Z1
238
+ unzip_list_output, _, unzip_list_status = safe_execute_command('unzip', '-Z1', archive_path)
239
+ if unzip_list_status.success? && unzip_list_output && !unzip_list_output.strip.empty?
240
+ required_files = parse_unzip_z1_output(unzip_list_output)
241
+ else
242
+ # 回退到 unzip -l
243
+ unzip_list_output, _, unzip_list_status = safe_execute_command('unzip', '-l', archive_path)
244
+ if unzip_list_status.success? && unzip_list_output
245
+ required_files = parse_unzip_l_output(unzip_list_output)
246
+ else
247
+ return { success: false, error_messages: ["无法列出 #{archive_path} 中的文件"] }
248
+ end
249
+ end
250
+
251
+ # 解压文件
252
+ required_files.each do |file|
253
+ extract_result = extract_internal_file(archive_path, file, dest_dir)
254
+ unless extract_result[:success]
255
+ failed = true
256
+ error_messages << "解压 #{file} 失败: #{extract_result[:error]}"
257
+ end
258
+ end
259
+
260
+ { success: !failed, error_messages: error_messages }
261
+ end
262
+
263
+ # 解压 AAB 文件中的必要文件
264
+ # 只解压合规检测需要的文件,避免解压大型 assets 文件导致错误
265
+ # 合规检测只需要:
266
+ # 1. base/manifest/ - Target SDK 检测(如果 bundletool 失败)
267
+ # 2. base/lib/ - ELF 对齐检测(递归解压所有 .so 文件)
268
+ # 3. base.apk - 需要进一步解压其中的 lib/
269
+ # 4. base/assets/bin/Data/boot.config - Unity 检测(如果存在)
270
+ # 注意:不解压 assets 目录下的其他文件(如 bundle 文件),避免解压错误
271
+ # @param aab_path [String] AAB 文件路径
272
+ # @param temp_dir [String] 临时目录路径
273
+ # @return [Hash] {success: Boolean, error_message: String}
274
+ def self.extract_required_files_from_aab(aab_path, temp_dir)
275
+ # 解压 AAB 文件中的必要文件
276
+ extract_result = extract_required_files(aab_path, temp_dir)
277
+ unzip_error_messages = extract_result[:error_messages]
278
+ unzip_failed = !extract_result[:success]
279
+
280
+ # 如果 base.apk 存在,需要进一步解压其中的 lib 目录
281
+ base_apk_path = File.join(temp_dir, "base.apk")
282
+ if File.exist?(base_apk_path)
283
+ # 尝试解压整个 lib/* 目录(包括所有子目录和文件)
284
+ # 这样可以避免逐个文件解压时遇到文件名编码问题
285
+ base_apk_lib_output, base_apk_lib_error, base_apk_lib_status = safe_execute_command('unzip', '-q', base_apk_path, 'lib/*', '-d', temp_dir)
286
+ unless base_apk_lib_status.success?
287
+ # 如果解压 lib/* 失败,尝试只解压 .so 文件
288
+ # 优先使用 unzip -Z1 列出文件
289
+ base_apk_list_output, _, base_apk_list_status = safe_execute_command('unzip', '-Z1', base_apk_path)
290
+ if base_apk_list_status.success? && base_apk_list_output && !base_apk_list_output.strip.empty?
291
+ lib_files = base_apk_list_output.lines.map(&:strip).select { |f| f.start_with?('lib/') && f.end_with?('.so') }
292
+ else
293
+ # 回退到 unzip -l
294
+ base_apk_list_output, _, base_apk_list_status = safe_execute_command('unzip', '-l', base_apk_path)
295
+ if base_apk_list_status.success? && base_apk_list_output
296
+ lib_files = parse_unzip_l_output(base_apk_list_output).select { |f| f.start_with?('lib/') && f.end_with?('.so') }
297
+ else
298
+ lib_files = []
299
+ end
300
+ end
301
+
302
+ # 尝试逐个解压 .so 文件
303
+ lib_files.each do |lib_file|
304
+ extract_result = extract_internal_file(base_apk_path, lib_file, temp_dir)
305
+ unless extract_result[:success]
306
+ # 单个文件解压失败,记录警告但不标记为整体失败
307
+ # 因为 find_shared_libraries 会再次尝试解压
308
+ unzip_error_messages << "警告: 解压 base.apk 中的 #{lib_file} 失败: #{extract_result[:error]}"
309
+ end
310
+ end
311
+ end
312
+ end
313
+
314
+ # 返回结果
315
+ # 只在实际失败时返回 false,警告信息不影响成功状态
316
+ # 因为 find_shared_libraries 会再次尝试解压 base.apk 中的文件
317
+ critical_errors = unzip_error_messages.reject { |msg| msg.include?('警告:') }
318
+ if unzip_failed && critical_errors.any?
319
+ error_text = critical_errors.join('; ')
320
+ error_msg = "无法解压 AAB 文件中的必要文件: #{error_text}"
321
+ return { success: false, error_message: error_msg }
322
+ else
323
+ return { success: true, error_message: nil }
324
+ end
325
+ end
326
+
327
+ # 安全执行命令并处理路径编码问题
328
+ # 确保包含非ASCII字符(如中文、韩语)的路径能够正确处理
329
+ def self.safe_execute_command(*args)
330
+ begin
331
+ # 确保所有参数都是字符串,并正确处理编码
332
+ safe_args = args.map.with_index do |arg, index|
333
+ if arg.is_a?(String)
334
+ # 第一个参数通常是命令名(如 'unzip' 或 '/usr/bin/java')
335
+ # 绝对路径(以 '/' 开头)不需要展开,已经是完整路径
336
+ # 相对路径(包含 '/' 或 '.')需要展开
337
+ if index == 0
338
+ # 第一个参数(命令名):绝对路径保持原样,相对路径展开
339
+ if arg.start_with?('/')
340
+ # 绝对路径:只处理编码,不展开
341
+ arg.dup.force_encoding('UTF-8').scrub('')
342
+ else
343
+ # 相对路径或命令名:不展开(让系统在 PATH 中查找)
344
+ arg.dup.force_encoding('UTF-8').scrub('')
345
+ end
346
+ elsif arg.start_with?('/')
347
+ # 绝对路径参数:只处理编码,不展开
348
+ arg.dup.force_encoding('UTF-8').scrub('')
349
+ elsif arg.include?('/') || arg.start_with?('.')
350
+ # 相对路径参数:使用 File.expand_path 规范化路径
351
+ normalized_path = File.expand_path(arg)
352
+ normalized_path.dup.force_encoding('UTF-8').scrub('')
353
+ else
354
+ # 其他参数(如选项 '-q', '-d'):只处理编码
355
+ arg.dup.force_encoding('UTF-8').scrub('')
356
+ end
357
+ else
358
+ arg.to_s
359
+ end
360
+ end
361
+
362
+ # 使用 Open3 执行命令,避免shell解析问题
363
+ # 设置环境变量确保输出编码正确
364
+ stdout, stderr, status = Open3.capture3(get_utf8_env, *safe_args)
365
+
366
+ # 确保输出使用UTF-8编码,并清理可能的路径拼接错误
367
+ if stdout
368
+ stdout = stdout.force_encoding('UTF-8')
369
+ stdout = stdout.scrub('?') unless stdout.valid_encoding?
370
+
371
+ # 清理可能的路径拼接错误:移除临时目录路径和AAB文件名的错误拼接
372
+ # 例如:launcher-debug.aab/private/var/folders/... 应该被清理
373
+ stdout = stdout.lines.map do |line|
374
+ # 如果行中包含临时目录路径模式(/private/var/folders 或 /tmp/),
375
+ # 并且前面有AAB文件名,则移除临时目录部分
376
+ if line.match(/\.aab\/[\/\w\-]+(?:private\/var\/folders|tmp\/|aab_compliance_check_)/)
377
+ # 提取AAB文件名后的相对路径部分
378
+ # 例如:launcher-debug.aab/private/var/.../yoo_pack/...
379
+ # 应该只保留 yoo_pack/... 部分
380
+ if match = line.match(/\.aab\/[\/\w\-]+(?:private\/var\/folders|tmp\/|aab_compliance_check_)[\/\w\-]+(.+)$/)
381
+ # 保留压缩包内的相对路径
382
+ line.sub(/^[^:]*\.aab\/[\/\w\-]+(?:private\/var\/folders|tmp\/|aab_compliance_check_)[\/\w\-]+/, '')
383
+ else
384
+ line
385
+ end
386
+ else
387
+ line
388
+ end
389
+ end.join
390
+ end
391
+
392
+ [stdout, stderr, status]
393
+ rescue => e
394
+ # 如果出错,返回空结果
395
+ # 创建一个简单的状态对象,模拟 Process::Status
396
+ failed_status = Object.new
397
+ def failed_status.success?; false; end
398
+ ['', e.message, failed_status]
399
+ end
400
+ end
401
+
402
+ # 安全执行shell命令(用于需要管道等shell特性的情况)
403
+ # 使用 Shellwords.escape 转义路径参数
404
+ def self.safe_execute_shell_command(command_template, **kwargs)
405
+ begin
406
+ # 转义所有路径参数
407
+ escaped_kwargs = kwargs.transform_values do |value|
408
+ if value.is_a?(String)
409
+ # 确保字符串使用UTF-8编码
410
+ safe_value = value.dup.force_encoding('UTF-8').scrub('')
411
+ Shellwords.escape(safe_value)
412
+ else
413
+ Shellwords.escape(value.to_s)
414
+ end
415
+ end
416
+
417
+ # 替换模板中的占位符
418
+ command = command_template % escaped_kwargs
419
+
420
+ # 执行命令
421
+ output = `#{command} 2>/dev/null`
422
+
423
+ # 确保输出使用UTF-8编码
424
+ if output
425
+ output = output.force_encoding('UTF-8')
426
+ output = output.scrub('?') unless output.valid_encoding?
427
+ end
428
+
429
+ output || ''
430
+ rescue => e
431
+ ''
432
+ end
433
+ end
434
+
102
435
  # 检查工具是否可用
103
436
  def self.tool_available?(tool_name)
104
437
  case tool_name
@@ -129,8 +462,6 @@ module Pindo
129
462
 
130
463
  # 检测 AAB 包体积合规性
131
464
  def self.check_aab_size_compliance(aab_path, result)
132
- Funlog.fancyinfo_update("检测 AAB 包体积...")
133
-
134
465
  begin
135
466
  # 获取 AAB 文件大小
136
467
  aab_size = File.size(aab_path)
@@ -141,36 +472,69 @@ module Pindo
141
472
  base_limit_bytes = 194615705 # 185MB
142
473
  base_limit_mb = 185
143
474
 
144
- unzip_out = `unzip -v "#{aab_path}"`
145
- if unzip_out && !unzip_out.empty?
146
- base_lines = unzip_out.lines.select { |l| l.include?(" base/") }
147
- base_size = base_lines.map { |l| l.split[2].to_i }.sum
475
+ # 使用安全的方式执行 unzip 命令,处理路径中的非ASCII字符
476
+ # 注意:unzip -v 的输出格式为:
477
+ # Archive: filename.aab
478
+ # Length Method Size Cmpr Date Time CRC-32 Name
479
+ # --------- ------ ------- ---- ---------- ----- -------- ----
480
+ # 1012 Defl:N 403 60% 01-01-1970 08:00 70391f2c base/manifest/AndroidManifest.xml
481
+ # 我们需要提取的是压缩包内的压缩大小(Size列,第3列,索引为2),而不是解压后的大小(Length列)
482
+ unzip_out, _, status = safe_execute_command('unzip', '-v', aab_path)
483
+ if status.success? && unzip_out && !unzip_out.empty?
484
+ # 过滤掉头部信息,只处理文件列表行
485
+ # 跳过 "Archive:" 行和表头行,只处理实际的文件条目
486
+ file_lines = unzip_out.lines.reject do |line|
487
+ line.strip.empty? ||
488
+ line.start_with?('Archive:') ||
489
+ line.start_with?('Length') ||
490
+ line.start_with?('------') ||
491
+ line.match(/^\s*$/) # 空行
492
+ end
493
+
494
+ # 只处理包含 " base/" 的行,并提取压缩后文件大小(Size列,第3列,索引为2)
495
+ # unzip -v 输出格式:Length Method Size Cmpr Date Time CRC-32 Name
496
+ base_lines = file_lines.select { |l| l.include?(" base/") }
497
+ base_size = base_lines.map do |line|
498
+ parts = line.split
499
+ # 确保有足够的列,并且第3列(Size)是数字(压缩后大小)
500
+ # 至少需要8列:Length Method Size Cmpr Date Time CRC-32 Name
501
+ if parts.length >= 8 && parts[2].match(/^\d+$/)
502
+ parts[2].to_i
503
+ else
504
+ 0
505
+ end
506
+ end.sum
507
+
148
508
  result.base_size_mb = (base_size.to_f / 1024 / 1024).round(2)
149
509
  result.base_percent = aab_size > 0 ? ((base_size.to_f * 100) / aab_size).round(2) : 0
150
510
  end
151
-
511
+
152
512
  if base_size > base_limit_bytes
153
513
  result.size_compliant = false
154
514
  result.add_issue("base 文件夹已超出 Google Play 限制(#{base_limit_mb}MB),请优化资源或分包")
155
- Funlog.error("base 文件夹超出限制: #{result.base_size_mb}MB (限制 #{base_limit_mb}MB)")
515
+ # 只在不合规时输出日志
516
+ puts "\n\e[1m--- AAB 包体积检测 ---\e[0m"
517
+ puts "\e[31m✗ base 文件夹超出限制: #{result.base_size_mb}MB (限制 #{base_limit_mb}MB)\e[0m"
156
518
  else
157
519
  result.size_compliant = true
520
+ # 合规时不输出日志
158
521
  end
159
522
 
160
523
  rescue => e
524
+ result.size_compliant = false
161
525
  result.add_issue("AAB 包体积检测失败: #{e.message}")
526
+ # 检测失败时输出日志
527
+ puts "\n\e[1m--- AAB 包体积检测 ---\e[0m"
528
+ puts "\e[31m✗ AAB 包体积检测失败: #{e.message}\e[0m"
162
529
  end
163
530
  end
164
531
 
165
532
  # 检测 Target SDK 版本合规性
166
533
  def self.check_target_sdk_compliance(temp_dir, result, aab_path = nil)
167
- Funlog.fancyinfo_update("检测 Target SDK 版本...")
168
-
169
534
  target_sdk = 0
170
535
 
171
536
  # 方法1: 使用 bundletool dump manifest(首要方法)
172
537
  if aab_path && File.exist?(aab_path)
173
- # puts "使用 bundletool 解析 AAB 文件: #{File.basename(aab_path)}"
174
538
  target_sdk = extract_target_sdk_with_bundletool(aab_path)
175
539
  end
176
540
 
@@ -186,14 +550,17 @@ module Pindo
186
550
 
187
551
  if target_sdk >= 35
188
552
  result.target_sdk_compliant = true
553
+ # 合规时不输出日志
189
554
  else
190
555
  result.target_sdk_compliant = false
556
+ # 只在不合规时输出日志
557
+ puts "\n\e[1m--- Target SDK 版本检测 ---\e[0m"
191
558
  if target_sdk == 0
192
559
  result.add_issue("无法检测到 Target SDK 版本,请检查 AAB 文件结构")
193
- Funlog.error("无法检测到 Target SDK 版本")
560
+ puts "\e[31m✗ 无法检测到 Target SDK 版本\e[0m"
194
561
  else
195
562
  result.add_issue("Target SDK #{target_sdk} 不符合要求,需要至少 Target SDK 35 (Android 15)")
196
- Funlog.warning("Target SDK: #{target_sdk} (需要至少 35)")
563
+ puts "\e[33m✗ Target SDK: #{target_sdk} (需要至少 35)\e[0m"
197
564
  end
198
565
  end
199
566
  end
@@ -211,21 +578,26 @@ module Pindo
211
578
  # 使用 Pindo 自带的 bundletool.jar
212
579
  # 确保使用正确的 Java 版本 (Java 11+)
213
580
  java_cmd = get_java_command_for_bundletool
214
- bundletool_cmd = "#{java_cmd} -jar \"#{bundletool_jar}\" dump manifest --bundle=\"#{aab_path}\" | grep \"targetSdkVersion\""
215
- output = `#{bundletool_cmd} 2>/dev/null`
581
+
582
+ # 使用安全的方式执行 bundletool 命令,处理路径中的非ASCII字符
583
+ # 先执行 bundletool 命令,然后通过管道传递给 grep
584
+ bundletool_args = [java_cmd, '-jar', bundletool_jar, 'dump', 'manifest', '--bundle', aab_path]
585
+ bundletool_output, _, bundletool_status = safe_execute_command(*bundletool_args)
586
+
587
+ # 如果命令成功,在输出中查找 targetSdkVersion
588
+ output = ''
589
+ if bundletool_status.success? && bundletool_output
590
+ output = bundletool_output.lines.grep(/targetSdkVersion/).join
591
+ end
216
592
 
217
593
  if output && !output.empty?
218
- # puts "bundletool 输出: #{output.strip}"
219
-
220
594
  # 解析输出格式: <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
221
595
  if output =~ /android:targetSdkVersion="(\d+)"/
222
596
  target_sdk = $1.to_i
223
- # puts "从 bundletool 中找到 targetSdkVersion: #{target_sdk}"
224
597
  return target_sdk
225
598
  end
226
599
  end
227
600
 
228
- # puts "bundletool 未找到 targetSdkVersion"
229
601
  return 0
230
602
  rescue => e
231
603
  return 0
@@ -241,21 +613,23 @@ module Pindo
241
613
  end
242
614
 
243
615
  # 使用系统 bundletool 命令
244
- bundletool_cmd = "bundletool dump manifest --bundle=\"#{aab_path}\" | grep \"targetSdkVersion\""
245
- output = `#{bundletool_cmd} 2>/dev/null`
616
+ # 使用安全的方式执行 bundletool 命令,处理路径中的非ASCII字符
617
+ bundletool_output, _, bundletool_status = safe_execute_command('bundletool', 'dump', 'manifest', '--bundle', aab_path)
618
+
619
+ # 如果命令成功,在输出中查找 targetSdkVersion
620
+ output = ''
621
+ if bundletool_status.success? && bundletool_output
622
+ output = bundletool_output.lines.grep(/targetSdkVersion/).join
623
+ end
246
624
 
247
625
  if output && !output.empty?
248
- # puts "bundletool 输出: #{output.strip}"
249
-
250
626
  # 解析输出格式: <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
251
627
  if output =~ /android:targetSdkVersion="(\d+)"/
252
628
  target_sdk = $1.to_i
253
- # puts "从系统 bundletool 中找到 targetSdkVersion: #{target_sdk}"
254
629
  return target_sdk
255
630
  end
256
631
  end
257
632
 
258
- # puts "系统 bundletool 未找到 targetSdkVersion"
259
633
  return 0
260
634
  rescue => e
261
635
  return 0
@@ -314,26 +688,22 @@ module Pindo
314
688
  # 在二进制 XML 中搜索 targetSdkVersion 后的数字
315
689
  # 模式: targetSdkVersion + 一些二进制数据 + 数字
316
690
  if content =~ /targetSdkVersion[^\d]*(\d{1,2})/
317
- # puts "从二进制 XML 中找到 targetSdkVersion: #{$1}"
318
691
  return $1.to_i
319
692
  end
320
693
 
321
694
  # 尝试更宽泛的搜索模式
322
695
  if content =~ /targetSdkVersion.*?(\d{1,2})/
323
- # puts "从二进制 XML 中找到 targetSdkVersion (宽泛模式): #{$1}"
324
696
  return $1.to_i
325
697
  end
326
698
 
327
- # puts "二进制 XML 中未找到 targetSdkVersion"
328
699
  return 0
329
700
  end
330
701
 
331
702
  # 尝试使用 aapt 工具解析(适用于文本格式)
332
703
  if tool_available?('aapt')
333
- aapt_output = `aapt dump badging "#{manifest_path}" 2>/dev/null`
334
- if aapt_output && !aapt_output.empty?
704
+ aapt_output, _, aapt_status = safe_execute_command('aapt', 'dump', 'badging', manifest_path)
705
+ if aapt_status.success? && aapt_output && !aapt_output.empty?
335
706
  if aapt_output =~ /targetSdkVersion:'(\d+)'/
336
- # puts "从 aapt 中找到 targetSdkVersion: #{$1}"
337
707
  return $1.to_i
338
708
  end
339
709
  end
@@ -341,10 +711,9 @@ module Pindo
341
711
 
342
712
  # 尝试使用 aapt2 工具
343
713
  if tool_available?('aapt2')
344
- aapt2_output = `aapt2 dump badging "#{manifest_path}" 2>/dev/null`
345
- if aapt2_output && !aapt2_output.empty?
714
+ aapt2_output, _, aapt2_status = safe_execute_command('aapt2', 'dump', 'badging', manifest_path)
715
+ if aapt2_status.success? && aapt2_output && !aapt2_output.empty?
346
716
  if aapt2_output =~ /targetSdkVersion:'(\d+)'/
347
- # puts "从 aapt2 中找到 targetSdkVersion: #{$1}"
348
717
  return $1.to_i
349
718
  end
350
719
  end
@@ -352,10 +721,9 @@ module Pindo
352
721
 
353
722
  # 尝试使用 aapt dump xmltree
354
723
  if tool_available?('aapt')
355
- aapt_xml_output = `aapt dump xmltree "#{manifest_path}" 2>/dev/null`
356
- if aapt_xml_output && !aapt_xml_output.empty?
724
+ aapt_xml_output, _, aapt_xml_status = safe_execute_command('aapt', 'dump', 'xmltree', manifest_path)
725
+ if aapt_xml_status.success? && aapt_xml_output && !aapt_xml_output.empty?
357
726
  if aapt_xml_output =~ /targetSdkVersion.*?(\d+)/
358
- # puts "从 aapt xmltree 中找到 targetSdkVersion: #{$1}"
359
727
  return $1.to_i
360
728
  end
361
729
  end
@@ -372,7 +740,6 @@ module Pindo
372
740
  if uses_sdk
373
741
  target_sdk = uses_sdk['targetSdkVersion']
374
742
  if target_sdk
375
- # puts "从 XML 中找到 targetSdkVersion: #{target_sdk}"
376
743
  return target_sdk.to_i
377
744
  end
378
745
  end
@@ -381,7 +748,6 @@ module Pindo
381
748
  target_sdk_attrs = doc.xpath('//@targetSdkVersion')
382
749
  if !target_sdk_attrs.empty?
383
750
  target_sdk = target_sdk_attrs.first.value
384
- # puts "从 XML 属性中找到 targetSdkVersion: #{target_sdk}"
385
751
  return target_sdk.to_i
386
752
  end
387
753
 
@@ -389,11 +755,9 @@ module Pindo
389
755
  all_attrs = doc.xpath('//@*[contains(name(), "targetSdkVersion")]')
390
756
  if !all_attrs.empty?
391
757
  target_sdk = all_attrs.first.value
392
- # puts "从 XML 中找到 targetSdkVersion 属性: #{target_sdk}"
393
758
  return target_sdk.to_i
394
759
  end
395
760
 
396
- # puts "未找到 targetSdkVersion,返回默认值 0"
397
761
  return 0
398
762
 
399
763
  rescue => e
@@ -403,14 +767,13 @@ module Pindo
403
767
 
404
768
  # 检测 ELF 对齐合规性
405
769
  def self.check_elf_alignment_compliance(temp_dir, result)
406
- Funlog.fancyinfo_update("检测 ELF 对齐 (16KB 页面大小)...")
407
-
408
770
  # 查找所有 .so 文件
409
771
  so_files = find_shared_libraries(temp_dir)
410
772
  result.total_libs = so_files.length
411
773
 
412
774
  if so_files.empty?
413
775
  result.elf_alignment_compliant = true
776
+ # 合规时不输出日志
414
777
  return
415
778
  end
416
779
 
@@ -438,12 +801,16 @@ module Pindo
438
801
 
439
802
  if critical_unaligned.empty?
440
803
  result.elf_alignment_compliant = true
804
+ # 合规时不输出日志
441
805
  else
442
806
  result.elf_alignment_compliant = false
807
+ # 只在不合规时输出日志
808
+ puts "\n\e[1m--- ELF 对齐检测 ---\e[0m"
443
809
  critical_unaligned.each do |lib|
444
810
  result.add_issue("#{lib[:architecture]} 架构的共享库 #{File.basename(lib[:file])} 未对齐 (16KB 页面大小要求)")
811
+ puts "\e[31m✗ #{lib[:architecture]} 架构的共享库 #{File.basename(lib[:file])} 未对齐 (16KB 页面大小要求)\e[0m"
445
812
  end
446
- Funlog.error("发现 #{critical_unaligned.length} 个关键架构的未对齐共享库")
813
+ puts "\e[31m✗ 发现 #{critical_unaligned.length} 个关键架构的未对齐共享库\e[0m"
447
814
  end
448
815
  end
449
816
 
@@ -464,7 +831,9 @@ module Pindo
464
831
  base_temp_dir = Dir.mktmpdir("base_apk_libs_")
465
832
 
466
833
  # 解压 base.apk 中的 lib 目录
467
- if system("unzip", "-q", base_apk_path, "lib/*", "-d", base_temp_dir)
834
+ # 使用 safe_execute_command 解压 lib/*,因为这是通配符模式
835
+ lib_extract_output, _, lib_extract_status = safe_execute_command('unzip', '-q', base_apk_path, 'lib/*', '-d', base_temp_dir)
836
+ if lib_extract_status.success?
468
837
  Dir.glob(File.join(base_temp_dir, "lib", "**", "*.so")).each do |so_file|
469
838
  so_files << so_file
470
839
  end
@@ -481,8 +850,9 @@ module Pindo
481
850
  def self.check_elf_alignment(so_file)
482
851
  begin
483
852
  # 首先检查文件是否为有效的 ELF 文件
484
- file_output = `file "#{so_file}" 2>/dev/null`
485
- unless file_output && file_output.include?('ELF')
853
+ # 使用安全的方式执行 file 命令,处理路径中的非ASCII字符
854
+ file_output, _, file_status = safe_execute_command('file', so_file)
855
+ unless file_status.success? && file_output && file_output.include?('ELF')
486
856
  return {
487
857
  aligned: false,
488
858
  alignment: "not_elf",
@@ -499,9 +869,10 @@ module Pindo
499
869
  }
500
870
  end
501
871
 
502
- objdump_output = `objdump -p "#{so_file}" 2>/dev/null`
872
+ # 使用安全的方式执行 objdump 命令,处理路径中的非ASCII字符
873
+ objdump_output, _, objdump_status = safe_execute_command('objdump', '-p', so_file)
503
874
 
504
- if objdump_output && !objdump_output.empty?
875
+ if objdump_status.success? && objdump_output && !objdump_output.empty?
505
876
  # 查找 LOAD 段的对齐信息
506
877
  load_sections = objdump_output.lines.select { |line| line.include?('LOAD') }
507
878
 
@@ -539,8 +910,9 @@ module Pindo
539
910
  }
540
911
  end
541
912
 
542
- readelf_output = `readelf -l "#{so_file}" 2>/dev/null`
543
- if readelf_output && !readelf_output.empty?
913
+ # 使用安全的方式执行 readelf 命令,处理路径中的非ASCII字符
914
+ readelf_output, _, readelf_status = safe_execute_command('readelf', '-l', so_file)
915
+ if readelf_status.success? && readelf_output && !readelf_output.empty?
544
916
  # 查找 LOAD 段的对齐信息
545
917
  load_sections = readelf_output.lines.select { |line| line.include?('LOAD') }
546
918
 
@@ -600,7 +972,7 @@ module Pindo
600
972
 
601
973
  # 打印合规检测摘要
602
974
  def self.print_compliance_summary(result)
603
- Funlog.fancyinfo_update("生成合规检测摘要...")
975
+ puts "\n\e[1m--- 生成合规检测摘要 ---\e[0m"
604
976
 
605
977
  # 总体合规状态
606
978
  if result.compliant?
@@ -629,39 +1001,47 @@ module Pindo
629
1001
 
630
1002
  # 检测 Unity 漏洞合规性
631
1003
  def self.check_unity_patch_compliance(temp_dir, result)
632
- puts "\n\e[1m--- Unity 漏洞检测 ---\e[0m"
633
-
634
1004
  # 检查是否存在 Unity 相关文件
635
1005
  unity_files = find_unity_files(temp_dir)
636
1006
 
637
1007
  if unity_files.empty?
638
- puts "\e[36m信息: 未检测到 Unity 相关文件,跳过 Unity 漏洞检测\e[0m"
1008
+ # 没有 Unity 文件,可能是非 Unity 项目,合规
639
1009
  result.unity_patch_compliant = true
1010
+ # 不输出日志
640
1011
  return
641
1012
  end
642
1013
 
643
- puts "检测到 Unity 项目,开始检查漏洞修复..."
1014
+ # 找到了 Unity 文件,需要检查漏洞修复
1015
+ # 检查 libunity.so 文件(不输出日志)
1016
+ libunity_result = check_libunity_patch(temp_dir, verbose: false)
644
1017
 
645
- # 检查 libunity.so 文件
646
- libunity_result = check_libunity_patch(temp_dir)
1018
+ # 检查 boot.config 文件(不输出日志)
1019
+ boot_config_result = check_boot_config_patch(temp_dir, verbose: false)
647
1020
 
648
- # 检查 boot.config 文件
649
- boot_config_result = check_boot_config_patch(temp_dir)
650
-
651
- # 综合判断
1021
+ # 综合判断:libunity.so boot.config 都必须通过检查
652
1022
  if libunity_result && boot_config_result
653
1023
  result.unity_patch_compliant = true
654
1024
  result.unity_xrsdk_patched = true
655
1025
  result.unity_override_patched = true
656
- puts "\e[32m✓ Unity 漏洞检测: 通过\e[0m"
1026
+ # 合规时不输出日志
657
1027
  else
658
1028
  result.unity_patch_compliant = false
1029
+ # 只在不合规时输出详细日志
1030
+ puts "\n\e[1m--- Unity 漏洞检测 ---\e[0m"
1031
+ puts "检测到 Unity 项目,开始检查漏洞修复..."
1032
+
659
1033
  if !libunity_result
660
1034
  result.add_issue("Unity libunity.so 漏洞未修复")
1035
+ # 重新检查并输出详细日志
1036
+ check_libunity_patch(temp_dir, verbose: true)
661
1037
  end
1038
+
662
1039
  if !boot_config_result
663
1040
  result.add_issue("Unity boot.config 漏洞未修复")
1041
+ # 重新检查并输出详细日志
1042
+ check_boot_config_patch(temp_dir, verbose: true)
664
1043
  end
1044
+
665
1045
  puts "\e[31m✗ Unity 漏洞检测: 未通过\e[0m"
666
1046
  end
667
1047
  end
@@ -671,18 +1051,7 @@ module Pindo
671
1051
  unity_files = []
672
1052
 
673
1053
  # 查找 libunity.so 文件
674
- lib_dirs = [
675
- "#{temp_dir}/lib",
676
- "#{temp_dir}/libs",
677
- "#{temp_dir}/base/lib", # AAB文件结构
678
- "#{temp_dir}/base/libs" # AAB文件结构
679
- ]
680
- lib_dirs.each do |lib_dir|
681
- if Dir.exist?(lib_dir)
682
- found_so_files = Dir.glob("#{lib_dir}/**/libunity.so")
683
- unity_files += found_so_files
684
- end
685
- end
1054
+ unity_files += find_so_files(temp_dir, 'libunity.so')
686
1055
 
687
1056
  # 查找 boot.config 文件
688
1057
  boot_config_paths = [
@@ -704,110 +1073,107 @@ module Pindo
704
1073
  # 检查是否使用 il2cpp
705
1074
  def self.uses_il2cpp?(temp_dir)
706
1075
  # 检查是否存在 libil2cpp.so 文件
707
- lib_dirs = [
708
- "#{temp_dir}/lib",
709
- "#{temp_dir}/libs",
710
- "#{temp_dir}/base/lib", # AAB文件结构
711
- "#{temp_dir}/base/libs" # AAB文件结构
712
- ]
713
-
714
- lib_dirs.each do |lib_dir|
715
- if Dir.exist?(lib_dir)
716
- il2cpp_files = Dir.glob("#{lib_dir}/**/libil2cpp.so")
717
- return true if il2cpp_files.any?
718
- end
719
- end
720
-
721
- false
1076
+ il2cpp_files = find_so_files(temp_dir, 'libil2cpp.so')
1077
+ il2cpp_files.any?
722
1078
  end
723
1079
 
724
1080
  # 检查 libunity.so 漏洞修复
725
- def self.check_libunity_patch(temp_dir)
726
- lib_dirs = [
727
- "#{temp_dir}/lib",
728
- "#{temp_dir}/libs",
729
- "#{temp_dir}/base/lib", # AAB文件结构
730
- "#{temp_dir}/base/libs" # AAB文件结构
731
- ]
732
- xrsdk_changed = false
733
- override_disabled = false
1081
+ # @param temp_dir [String] 临时目录路径
1082
+ # @param verbose [Boolean] 是否输出详细日志,默认为 true
1083
+ # @return [Boolean] 是否通过检测
1084
+ def self.check_libunity_patch(temp_dir, verbose: true)
1085
+ # 收集所有 libunity.so 文件
1086
+ libunity_files = find_so_files(temp_dir, 'libunity.so')
1087
+
1088
+ # 如果没有找到 libunity.so 文件,返回 true(不是 Unity 项目或不需要检查)
1089
+ if libunity_files.empty?
1090
+ return true
1091
+ end
1092
+
1093
+ # 必须所有 libunity.so 文件都通过检查
1094
+ xrsdk_all_passed = true
1095
+ override_all_disabled = true
734
1096
  override_found = false
735
1097
  uses_il2cpp = uses_il2cpp?(temp_dir)
736
1098
 
737
- lib_dirs.each do |lib_dir|
738
- next unless Dir.exist?(lib_dir)
1099
+ libunity_files.each do |so_file|
1100
+ puts "检查文件: #{File.basename(so_file)}" if verbose
739
1101
 
740
- libunity_files = Dir.glob("#{lib_dir}/**/libunity.so")
1102
+ # 检查 xrsdk 字符串修改
1103
+ xrsdk_passed = check_xrsdk_patch(so_file, verbose: verbose)
1104
+ if xrsdk_passed
1105
+ puts " \e[32m✓ xrsdk-pre-init-library 字符串已正确修改\e[0m" if verbose
1106
+ else
1107
+ puts " \e[31m✗ xrsdk-pre-init-library 字符串未正确修改\e[0m" if verbose
1108
+ xrsdk_all_passed = false
1109
+ end
741
1110
 
742
- libunity_files.each do |so_file|
743
- puts "检查文件: #{File.basename(so_file)}"
744
-
745
- # 检查 xrsdk 字符串修改
746
- if check_xrsdk_patch(so_file)
747
- xrsdk_changed = true
748
- puts " \e[32m✓ xrsdk-pre-init-library 字符串已正确修改\e[0m"
749
- else
750
- puts " \e[31m✗ xrsdk-pre-init-library 字符串未正确修改\e[0m"
751
- end
752
-
753
- # 检查 overrideMonoSearchPath 禁用(仅在使用 Mono 时检查)
754
- if uses_il2cpp
755
- puts " \e[36m信息: 使用 IL2CPP,跳过 overrideMonoSearchPath 检测\e[0m"
756
- override_disabled = true # IL2CPP 不需要检查 overrideMonoSearchPath
757
- else
758
- override_result = check_override_patch(so_file)
759
- if override_result[:found]
760
- override_found = true
761
- if override_result[:disabled]
762
- override_disabled = true
763
- puts " \e[32m✓ overrideMonoSearchPath 已正确禁用\e[0m"
764
- else
765
- puts " \e[31m✗ overrideMonoSearchPath 未正确禁用\e[0m"
766
- end
1111
+ # 检查 overrideMonoSearchPath 禁用(仅在使用 Mono 时检查)
1112
+ if uses_il2cpp
1113
+ puts " \e[36m信息: 使用 IL2CPP,跳过 overrideMonoSearchPath 检测\e[0m" if verbose
1114
+ # IL2CPP 不需要检查 overrideMonoSearchPath
1115
+ else
1116
+ override_result = check_override_patch(so_file, verbose: verbose)
1117
+ if override_result[:found]
1118
+ override_found = true
1119
+ if override_result[:disabled]
1120
+ puts " \e[32m✓ overrideMonoSearchPath 已正确禁用\e[0m" if verbose
767
1121
  else
768
- puts " \e[36m信息: 未找到 overrideMonoSearchPath 字符串\e[0m"
1122
+ puts " \e[31m✗ overrideMonoSearchPath 未正确禁用\e[0m" if verbose
1123
+ override_all_disabled = false
769
1124
  end
1125
+ else
1126
+ puts " \e[36m信息: 未找到 overrideMonoSearchPath 字符串\e[0m" if verbose
770
1127
  end
771
1128
  end
772
1129
  end
773
1130
 
774
- # 如果没有找到 overrideMonoSearchPath 或使用 IL2CPP,认为是正常的
1131
+ # 如果没有找到 overrideMonoSearchPath 或使用 IL2CPP,认为 override 检查通过
775
1132
  if !override_found || uses_il2cpp
776
- override_disabled = true
1133
+ override_all_disabled = true
777
1134
  end
778
1135
 
779
- xrsdk_changed && override_disabled
1136
+ # 所有 libunity.so 文件都必须通过 xrsdk 检查,并且 override 检查也必须通过
1137
+ xrsdk_all_passed && override_all_disabled
780
1138
  end
781
1139
 
782
1140
  # 检查 xrsdk 字符串修补
783
- def self.check_xrsdk_patch(so_file)
1141
+ # @param so_file [String] .so 文件路径
1142
+ # @param verbose [Boolean] 是否输出详细日志,默认为 true
1143
+ # @return [Boolean] 是否通过检测
1144
+ def self.check_xrsdk_patch(so_file, verbose: true)
784
1145
  # 检查是否还存在原始的 xrsdk 字符串
785
1146
  has_original = false
786
1147
  has_modified = false
787
1148
 
788
1149
  begin
789
- # 使用 strings 命令检查文件内容
790
- strings_output = `strings "#{so_file}" 2>/dev/null`
1150
+ # 使用安全的方式执行 strings 命令,处理路径中的非ASCII字符
1151
+ strings_output, _, strings_status = safe_execute_command('strings', so_file)
791
1152
 
792
- if strings_output.include?("xrsdk-pre-init-library")
793
- has_original = true
794
- end
795
-
796
- if strings_output.include?("8rsdk-pre-init-library")
797
- has_modified = true
1153
+ if strings_status.success? && strings_output
1154
+ if strings_output.include?("xrsdk-pre-init-library")
1155
+ has_original = true
1156
+ end
1157
+
1158
+ if strings_output.include?("8rsdk-pre-init-library")
1159
+ has_modified = true
1160
+ end
798
1161
  end
799
1162
 
800
1163
  # 如果存在修改后的字符串且不存在原始字符串,说明修补成功
801
1164
  has_modified && !has_original
802
1165
 
803
1166
  rescue => e
804
- puts " \e[33m警告: 检查 #{so_file} 时出错: #{e.message}\e[0m"
1167
+ puts " \e[33m警告: 检查 #{so_file} 时出错: #{e.message}\e[0m" if verbose
805
1168
  false
806
1169
  end
807
1170
  end
808
1171
 
809
1172
  # 检查 overrideMonoSearchPath 禁用
810
- def self.check_override_patch(so_file)
1173
+ # @param so_file [String] .so 文件路径
1174
+ # @param verbose [Boolean] 是否输出详细日志,默认为 true
1175
+ # @return [Hash] 检测结果 {found: Boolean, disabled: Boolean}
1176
+ def self.check_override_patch(so_file, verbose: true)
811
1177
  result = { found: false, disabled: false }
812
1178
 
813
1179
  begin
@@ -831,14 +1197,17 @@ module Pindo
831
1197
  end
832
1198
 
833
1199
  rescue => e
834
- puts " \e[33m警告: 检查 #{so_file} 时出错: #{e.message}\e[0m"
1200
+ puts " \e[33m警告: 检查 #{so_file} 时出错: #{e.message}\e[0m" if verbose
835
1201
  end
836
1202
 
837
1203
  result
838
1204
  end
839
1205
 
840
1206
  # 检查 boot.config 漏洞修复
841
- def self.check_boot_config_patch(temp_dir)
1207
+ # @param temp_dir [String] 临时目录路径
1208
+ # @param verbose [Boolean] 是否输出详细日志,默认为 true
1209
+ # @return [Boolean] 是否通过检测
1210
+ def self.check_boot_config_patch(temp_dir, verbose: true)
842
1211
  # 尝试多个可能的boot.config路径
843
1212
  boot_config_paths = [
844
1213
  "#{temp_dir}/assets/bin/Data/boot.config", # 标准APK路径
@@ -855,11 +1224,14 @@ module Pindo
855
1224
  end
856
1225
 
857
1226
  unless boot_config_path
858
- puts " \e[33m信息: 未找到 boot.config 文件\e[0m"
859
- return true # 没有 boot.config 文件,认为是正常的
1227
+ # 如果没有找到 boot.config 文件,检查是否是 Unity 项目
1228
+ # 如果找到了 libunity.so,说明是 Unity 项目,但 boot.config 不存在,这可能是正常的(某些 Unity 版本可能没有)
1229
+ # 如果没找到 libunity.so,说明不是 Unity 项目,返回 true 是合理的
1230
+ puts " \e[33m信息: 未找到 boot.config 文件\e[0m" if verbose
1231
+ return true # 没有 boot.config 文件,认为是正常的(可能不是 Unity 项目或不需要修补)
860
1232
  end
861
1233
 
862
- puts "检查文件: #{File.basename(boot_config_path)}"
1234
+ puts "检查文件: #{File.basename(boot_config_path)}" if verbose
863
1235
 
864
1236
  begin
865
1237
  content = File.read(boot_config_path)
@@ -869,39 +1241,39 @@ module Pindo
869
1241
  modified_count = content.scan(/8rsdk-pre-init-library/).length
870
1242
 
871
1243
  if original_count > 0
872
- puts " \e[31m✗ boot.config 中仍存在 #{original_count} 个未修改的 xrsdk-pre-init-library\e[0m"
1244
+ puts " \e[31m✗ boot.config 中仍存在 #{original_count} 个未修改的 xrsdk-pre-init-library\e[0m" if verbose
873
1245
  end
874
1246
 
875
1247
  if modified_count > 0
876
- puts " \e[32m✓ boot.config 中发现 #{modified_count} 个修改后的 8rsdk-pre-init-library\e[0m"
1248
+ puts " \e[32m✓ boot.config 中发现 #{modified_count} 个修改后的 8rsdk-pre-init-library\e[0m" if verbose
877
1249
  end
878
1250
 
879
1251
  # 检查是否还有其他 xrsdk 相关字符串
880
1252
  other_xrsdk_count = content.scan(/xrsdk/).length
881
1253
  if other_xrsdk_count > 0
882
- puts " \e[36m信息: boot.config 中发现 #{other_xrsdk_count} 个其他 xrsdk 相关字符串\e[0m"
1254
+ puts " \e[36m信息: boot.config 中发现 #{other_xrsdk_count} 个其他 xrsdk 相关字符串\e[0m" if verbose
883
1255
  end
884
1256
 
885
1257
  # 如果没有 xrsdk 相关字符串,认为是正常的
886
1258
  if other_xrsdk_count == 0
887
- puts " \e[32m✓ boot.config 检查结果: 通过(未使用 XR SDK,无需修补)\e[0m"
1259
+ puts " \e[32m✓ boot.config 检查结果: 通过(未使用 XR SDK,无需修补)\e[0m" if verbose
888
1260
  return true
889
1261
  end
890
1262
 
891
1263
  # 如果存在修改后的字符串且不存在未修改的字符串,说明修补成功
892
1264
  if modified_count > 0 && original_count == 0
893
- puts " \e[32m✓ boot.config 检查结果: 通过(已正确替换 #{modified_count} 个字符串)\e[0m"
1265
+ puts " \e[32m✓ boot.config 检查结果: 通过(已正确替换 #{modified_count} 个字符串)\e[0m" if verbose
894
1266
  return true
895
1267
  elsif original_count > 0
896
- puts " \e[31m✗ boot.config 检查结果: 未通过(存在未修改的字符串)\e[0m"
1268
+ puts " \e[31m✗ boot.config 检查结果: 未通过(存在未修改的字符串)\e[0m" if verbose
897
1269
  return false
898
1270
  else
899
- puts " \e[31m✗ boot.config 检查结果: 未通过(未找到修改后的字符串)\e[0m"
1271
+ puts " \e[31m✗ boot.config 检查结果: 未通过(未找到修改后的字符串)\e[0m" if verbose
900
1272
  return false
901
1273
  end
902
1274
 
903
1275
  rescue => e
904
- puts " \e[33m警告: 检查 boot.config 时出错: #{e.message}\e[0m"
1276
+ puts " \e[33m警告: 检查 boot.config 时出错: #{e.message}\e[0m" if verbose
905
1277
  false
906
1278
  end
907
1279
  end
@@ -65,10 +65,10 @@ module Pindo
65
65
  )
66
66
  @task_displays[task_id] = display
67
67
 
68
- # 创建日志文件
68
+ # 创建日志文件(明确指定 UTF-8 编码,防止中文写入错误)
69
69
  log_file_name = "#{task.task_key}_#{task_id}.log"
70
70
  log_file_path = File.join(@log_dir, log_file_name)
71
- @task_loggers[task_id] = File.open(log_file_path, 'w')
71
+ @task_loggers[task_id] = File.open(log_file_path, 'w:utf-8')
72
72
 
73
73
  # 渲染终端
74
74
  render_terminal
@@ -356,11 +356,29 @@ module Pindo
356
356
  logger = @task_loggers[task_id]
357
357
  return unless logger
358
358
 
359
+ # 确保消息使用 UTF-8 编码,防止编码错误导致写入失败
360
+ safe_message = ensure_utf8(message)
359
361
  timestamp = Time.now.strftime('%H:%M:%S')
360
- logger.puts("[#{timestamp}] #{message}")
362
+ logger.puts("[#{timestamp}] #{safe_message}")
361
363
  logger.flush
362
364
  end
363
365
 
366
+ # 确保字符串使用 UTF-8 编码
367
+ # @param str [String] 输入字符串
368
+ # @return [String] UTF-8 编码的字符串
369
+ def ensure_utf8(str)
370
+ return str if str.nil?
371
+ return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
372
+
373
+ # 尝试转换为 UTF-8
374
+ begin
375
+ str.encode(Encoding::UTF_8)
376
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
377
+ # 如果转换失败,尝试使用替换字符
378
+ str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
379
+ end
380
+ end
381
+
364
382
  # 关闭日志文件
365
383
  # @param task_id [String] 任务 ID
366
384
  def close_log(task_id)
@@ -370,6 +388,15 @@ module Pindo
370
388
  logger.close
371
389
  @task_loggers.delete(task_id)
372
390
  end
391
+
392
+ # 关闭所有日志文件(用于清理资源)
393
+ def close_all_logs
394
+ @mutex.synchronize do
395
+ @task_loggers.each_key do |task_id|
396
+ close_log(task_id)
397
+ end
398
+ end
399
+ end
373
400
  end
374
401
  end
375
402
  end
@@ -45,23 +45,41 @@ module Pindo
45
45
  def write(message)
46
46
  # 捕获输出,写入日志文件(跳过空行)
47
47
  unless message.strip.empty?
48
+ # 确保消息使用 UTF-8 编码,防止编码错误导致任务中断
49
+ safe_message = ensure_utf8(message.chomp)
48
50
  # 根据流类型选择日志级别
49
51
  if @stream_type == :stderr
50
- @output_manager.log_error(@task_id, message.chomp)
52
+ @output_manager.log_error(@task_id, safe_message)
51
53
  else
52
- @output_manager.log_detail(@task_id, message.chomp)
54
+ @output_manager.log_detail(@task_id, safe_message)
53
55
  end
54
56
  end
55
57
  message.length
56
58
  end
57
59
 
60
+ # 确保字符串使用 UTF-8 编码
61
+ # @param str [String] 输入字符串
62
+ # @return [String] UTF-8 编码的字符串
63
+ def ensure_utf8(str)
64
+ return str if str.nil?
65
+ return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
66
+
67
+ # 尝试转换为 UTF-8
68
+ begin
69
+ str.encode(Encoding::UTF_8)
70
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
71
+ # 如果转换失败,尝试使用替换字符
72
+ str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
73
+ end
74
+ end
75
+
58
76
  # puts 方法
59
77
  # @param args [Array] 参数列表
60
78
  def puts(*args)
61
79
  if args.empty?
62
80
  write("\n")
63
81
  else
64
- args.each { |arg| write("#{arg}\n") }
82
+ args.each { |arg| write("#{ensure_utf8(arg.to_s)}\n") }
65
83
  end
66
84
  nil
67
85
  end
@@ -69,7 +87,7 @@ module Pindo
69
87
  # print 方法
70
88
  # @param args [Array] 参数列表
71
89
  def print(*args)
72
- write(args.join)
90
+ write(ensure_utf8(args.join))
73
91
  nil
74
92
  end
75
93
 
@@ -40,6 +40,9 @@ module Pindo
40
40
  # 启用输出管理系统
41
41
  # @param options [Hash] 配置选项
42
42
  def enable_output_management(options = {})
43
+ # 如果已经有一个输出管理器,先关闭所有日志文件(避免文件句柄泄漏)
44
+ @output_manager.close_all_logs if @output_manager
45
+
43
46
  @output_manager = MultiLineOutputManager.new(
44
47
  log_dir: options[:log_dir] || './pindo_logs',
45
48
  max_lines_per_task: options[:max_lines_per_task] || 5,
@@ -90,6 +93,15 @@ module Pindo
90
93
  mode = parse_execution_mode(options)
91
94
  strategy = ExecutionStrategy.create(mode, options)
92
95
 
96
+ # 并发模式必须启用输出管理器,否则多线程输出会混乱
97
+ if mode == :concurrent && @output_manager.nil?
98
+ enable_output_management(
99
+ log_dir: options[:log_dir] || './pindo_logs',
100
+ max_lines_per_task: options[:max_lines_per_task] || 5,
101
+ max_recent_completed: options[:max_recent_completed] || 3
102
+ )
103
+ end
104
+
93
105
  # 如果配置了输出管理器,注册所有任务
94
106
  if @output_manager
95
107
  @queue.pending_snapshot.each do |task|
data/lib/pindo/version.rb CHANGED
@@ -6,13 +6,13 @@ require 'time'
6
6
 
7
7
  module Pindo
8
8
 
9
- VERSION = "5.15.3"
9
+ VERSION = "5.15.9"
10
10
 
11
11
  class VersionCheck
12
12
  RUBYGEMS_API = 'https://rubygems.org/api/v1/gems/pindo.json'
13
13
  VERSION_INFO_FILE = File.expand_path('~/.pindo/version_info.yml')
14
14
  CHECK_INTERVAL = 5 * 60 * 60 # 5小时检查一次
15
- CONFIG_MIN_VERSION = '1.3.0' # 硬编码的配置版本要求
15
+ CONFIG_MIN_VERSION = '1.4.0' # 硬编码的配置版本要求
16
16
 
17
17
  class << self
18
18
  # 主版本检查方法(保持向后兼容)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pindo
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.15.3
4
+ version: 5.15.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - wade