Module: Pindo::GPComplianceHelper

Defined in:
lib/pindo/module/android/gp_compliance_helper.rb

Defined Under Namespace

Classes: ComplianceResult

Class Method Summary collapse

Class Method Details

.check_aab_compliance(aab_path) ⇒ Object

检测 AAB 文件的 Google Play 合规性



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 51

def self.check_aab_compliance(aab_path)
  result = ComplianceResult.new
  
  # 先输出标题,使用 puts 确保始终在终端可见
  puts "\n\e[1m-- 检测 AAB 文件的 Google Play 合规性 --\e[0m"

  unless File.exist?(aab_path)
    puts "\e[31m✗ AAB 文件不存在: #{aab_path}\e[0m"
    result.add_issue("AAB 文件不存在: #{aab_path}")
    return result
  end
  
  puts "\e[1m检测的 AAB 文件:#{File.basename(aab_path)}\e[0m"
  
  # 创建临时目录用于解压 AAB
  temp_dir = nil
  begin
    temp_dir = Dir.mktmpdir("aab_compliance_check_")
    
    # 检查 unzip 工具是否可用
    unless tool_available?('unzip')
      result.add_issue("unzip 工具不可用,无法解压 AAB 文件")
      Funlog.error("请安装 unzip 工具: brew install unzip (macOS) 或 apt-get install unzip (Ubuntu)")
      return result
    end
    
    # 解压 AAB 文件中的必要文件
    extract_result = extract_required_files_from_aab(aab_path, temp_dir)
    unless extract_result[:success]
      puts "\e[31m✗ #{extract_result[:error_message]}\e[0m"
      result.add_issue(extract_result[:error_message])
      return result
    end
    
    # 检测 AAB 包体积
    check_aab_size_compliance(aab_path, result)
    
    # 检测 Target SDK 版本(传递 AAB 路径)
    check_target_sdk_compliance(temp_dir, result, aab_path)
    
    # 检测 ELF 对齐
    check_elf_alignment_compliance(temp_dir, result)
    
    # 检测 Unity 漏洞修复
    check_unity_patch_compliance(temp_dir, result)
    
    # 输出检测结果
    print_compliance_summary(result)
    
  ensure
    # 清理临时目录
    FileUtils.rm_rf(temp_dir) if temp_dir && File.directory?(temp_dir)
  end
  
  result
end

.check_aab_size_compliance(aab_path, result) ⇒ Object

检测 AAB 包体积合规性



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 464

def self.check_aab_size_compliance(aab_path, result)
  begin
    # 获取 AAB 文件大小
    aab_size = File.size(aab_path)
    result.aab_size_mb = (aab_size.to_f / 1024 / 1024).round(2)
    
    # 统计 base/ 文件夹压缩体积
    base_size = 0
    base_limit_bytes = 194615705 # 185MB
    base_limit_mb = 185
    
    # 使用安全的方式执行 unzip 命令,处理路径中的非ASCII字符
    # 注意:unzip -v 的输出格式为:
    # Archive:  filename.aab
    #   Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
    # ---------  ------  ------- ---- ---------- ----- --------  ----
    #    1012  Defl:N      403  60% 01-01-1970 08:00 70391f2c  base/manifest/AndroidManifest.xml
    # 我们需要提取的是压缩包内的压缩大小(Size列,第3列,索引为2),而不是解压后的大小(Length列)
    unzip_out, _, status = safe_execute_command('unzip', '-v', aab_path)
    if status.success? && unzip_out && !unzip_out.empty?
      # 过滤掉头部信息,只处理文件列表行
      # 跳过 "Archive:" 行和表头行,只处理实际的文件条目
      file_lines = unzip_out.lines.reject do |line|
        line.strip.empty? || 
        line.start_with?('Archive:') || 
        line.start_with?('Length') || 
        line.start_with?('------') ||
        line.match(/^\s*$/)  # 空行
      end
      
      # 只处理包含 " base/" 的行,并提取压缩后文件大小(Size列,第3列,索引为2)
      # unzip -v 输出格式:Length Method Size Cmpr Date Time CRC-32 Name
      base_lines = file_lines.select { |l| l.include?(" base/") }
      base_size = base_lines.map do |line|
        parts = line.split
        # 确保有足够的列,并且第3列(Size)是数字(压缩后大小)
        # 至少需要8列:Length Method Size Cmpr Date Time CRC-32 Name
        if parts.length >= 8 && parts[2].match(/^\d+$/)
          parts[2].to_i
        else
          0
        end
      end.sum
      
      result.base_size_mb = (base_size.to_f / 1024 / 1024).round(2)
      result.base_percent = aab_size > 0 ? ((base_size.to_f * 100) / aab_size).round(2) : 0
    end

    if base_size > base_limit_bytes
      result.size_compliant = false
      result.add_issue("base 文件夹已超出 Google Play 限制(#{base_limit_mb}MB),请优化资源或分包")
      # 只在不合规时输出日志
      puts "\n\e[1m--- AAB 包体积检测 ---\e[0m"
      puts "\e[31m✗ base 文件夹超出限制: #{result.base_size_mb}MB (限制 #{base_limit_mb}MB)\e[0m"
    else
      result.size_compliant = true
      # 合规时不输出日志
    end

  rescue => e
    result.size_compliant = false
    result.add_issue("AAB 包体积检测失败: #{e.message}")
    # 检测失败时输出日志
    puts "\n\e[1m--- AAB 包体积检测 ---\e[0m"
    puts "\e[31m✗ AAB 包体积检测失败: #{e.message}\e[0m"
  end
end

.check_boot_config_patch(temp_dir, verbose: true) ⇒ Boolean

检查 boot.config 漏洞修复

Parameters:

  • temp_dir (String)

    临时目录路径

  • verbose (Boolean) (defaults to: true)

    是否输出详细日志,默认为 true

Returns:

  • (Boolean)

    是否通过检测



1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 1210

def self.check_boot_config_patch(temp_dir, verbose: true)
  # 尝试多个可能的boot.config路径
  boot_config_paths = [
    "#{temp_dir}/assets/bin/Data/boot.config",      # 标准APK路径
    "#{temp_dir}/base/assets/bin/Data/boot.config",  # AAB base模块路径
    "#{temp_dir}/base/bin/Data/boot.config"          # AAB简化路径
  ]
  
  boot_config_path = nil
  boot_config_paths.each do |path|
    if File.exist?(path)
      boot_config_path = path
      break
    end
  end
  
  unless boot_config_path
    # 如果没有找到 boot.config 文件,检查是否是 Unity 项目
    # 如果找到了 libunity.so,说明是 Unity 项目,但 boot.config 不存在,这可能是正常的(某些 Unity 版本可能没有)
    # 如果没找到 libunity.so,说明不是 Unity 项目,返回 true 是合理的
    puts "  \e[33m信息: 未找到 boot.config 文件\e[0m" if verbose
    return true  # 没有 boot.config 文件,认为是正常的(可能不是 Unity 项目或不需要修补)
  end
  
  puts "检查文件: #{File.basename(boot_config_path)}" if verbose
  
  begin
    content = File.read(boot_config_path)
    
    # 检查是否存在未修改的字符串
    original_count = content.scan(/xrsdk-pre-init-library/).length
    modified_count = content.scan(/8rsdk-pre-init-library/).length
    
    if original_count > 0
      puts "  \e[31m✗ boot.config 中仍存在 #{original_count} 个未修改的 xrsdk-pre-init-library\e[0m" if verbose
    end
    
    if modified_count > 0
      puts "  \e[32m✓ boot.config 中发现 #{modified_count} 个修改后的 8rsdk-pre-init-library\e[0m" if verbose
    end
    
    # 检查是否还有其他 xrsdk 相关字符串
    other_xrsdk_count = content.scan(/xrsdk/).length
    if other_xrsdk_count > 0
      puts "  \e[36m信息: boot.config 中发现 #{other_xrsdk_count} 个其他 xrsdk 相关字符串\e[0m" if verbose
    end
    
    # 如果没有 xrsdk 相关字符串,认为是正常的
    if other_xrsdk_count == 0
      puts "  \e[32m✓ boot.config 检查结果: 通过(未使用 XR SDK,无需修补)\e[0m" if verbose
      return true
    end
    
    # 如果存在修改后的字符串且不存在未修改的字符串,说明修补成功
    if modified_count > 0 && original_count == 0
      puts "  \e[32m✓ boot.config 检查结果: 通过(已正确替换 #{modified_count} 个字符串)\e[0m" if verbose
      return true
    elsif original_count > 0
      puts "  \e[31m✗ boot.config 检查结果: 未通过(存在未修改的字符串)\e[0m" if verbose
      return false
    else
      puts "  \e[31m✗ boot.config 检查结果: 未通过(未找到修改后的字符串)\e[0m" if verbose
      return false
    end
    
  rescue => e
    puts "  \e[33m警告: 检查 boot.config 时出错: #{e.message}\e[0m" if verbose
    false
  end
end

.check_elf_alignment(so_file) ⇒ Object

检查单个 ELF 文件的对齐状态



850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 850

def self.check_elf_alignment(so_file)
  begin
    # 首先检查文件是否为有效的 ELF 文件
    # 使用安全的方式执行 file 命令,处理路径中的非ASCII字符
    file_output, _, file_status = safe_execute_command('file', so_file)
    unless file_status.success? && file_output && file_output.include?('ELF')
      return {
        aligned: false,
        alignment: "not_elf",
        architecture: determine_architecture(so_file)
      }
    end
    
    # 使用 objdump 检查 ELF 文件的对齐
    unless tool_available?('objdump')
      return {
        aligned: false,
        alignment: "tool_missing",
        architecture: determine_architecture(so_file)
      }
    end
    
    # 使用安全的方式执行 objdump 命令,处理路径中的非ASCII字符
    objdump_output, _, objdump_status = safe_execute_command('objdump', '-p', so_file)
    
    if objdump_status.success? && objdump_output && !objdump_output.empty?
      # 查找 LOAD 段的对齐信息
      load_sections = objdump_output.lines.select { |line| line.include?('LOAD') }
      
      if !load_sections.empty?
        # 获取第一个 LOAD 段的对齐值
        first_load = load_sections.first
        # 更精确的正则表达式匹配对齐值
        alignment_match = first_load.match(/LOAD\s+off\s+0x[0-9a-f]+\s+vaddr\s+0x[0-9a-f]+\s+paddr\s+0x[0-9a-f]+\s+align\s+2\*\*(\d+)/)
        
        if alignment_match
          alignment_power = alignment_match[1].to_i
          alignment_value = 2 ** alignment_power
          
          # 检查是否满足 16KB 对齐要求 (2^14 = 16384)
          aligned = alignment_value >= 16384
          
          # 确定架构
          architecture = determine_architecture(so_file)
          
          return {
            aligned: aligned,
            alignment: "2^#{alignment_power}",
            architecture: architecture
          }
        end
      end
    end
    
    # 如果 objdump 失败,尝试使用 readelf
    unless tool_available?('readelf')
      return {
        aligned: false,
        alignment: "tool_missing",
        architecture: determine_architecture(so_file)
      }
    end
    
    # 使用安全的方式执行 readelf 命令,处理路径中的非ASCII字符
    readelf_output, _, readelf_status = safe_execute_command('readelf', '-l', so_file)
    if readelf_status.success? && readelf_output && !readelf_output.empty?
      # 查找 LOAD 段的对齐信息
      load_sections = readelf_output.lines.select { |line| line.include?('LOAD') }
      
      if !load_sections.empty?
        first_load = load_sections.first
        alignment_match = first_load.match(/LOAD\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+(\d+)/)
        
        if alignment_match
          alignment_value = alignment_match[1].to_i
          alignment_power = Math.log2(alignment_value).to_i if alignment_value > 0
          
          aligned = alignment_value >= 16384
          architecture = determine_architecture(so_file)
          
          return {
            aligned: aligned,
            alignment: alignment_value > 0 ? "2^#{alignment_power}" : "0",
            architecture: architecture
          }
        end
      end
    end
    
    # 如果所有方法都失败,返回未对齐状态
    return {
      aligned: false,
      alignment: "unknown",
      architecture: determine_architecture(so_file)
    }
    
  rescue => e
    return {
      aligned: false,
      alignment: "error",
      architecture: determine_architecture(so_file)
    }
  end
end

.check_elf_alignment_compliance(temp_dir, result) ⇒ Object

检测 ELF 对齐合规性



769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 769

def self.check_elf_alignment_compliance(temp_dir, result)
  # 查找所有 .so 文件
  so_files = find_shared_libraries(temp_dir)
  result.total_libs = so_files.length
  
  if so_files.empty?
    result.elf_alignment_compliant = true
    # 合规时不输出日志
    return
  end
  
  unaligned_libs = []
  
  so_files.each do |so_file|
    alignment_status = check_elf_alignment(so_file)
    
    unless alignment_status[:aligned]
      unaligned_libs << {
        file: so_file,
        alignment: alignment_status[:alignment],
        architecture: alignment_status[:architecture]
      }
    end
  end
  
  result.unaligned_libs = unaligned_libs
  
  # 检查是否有 arm64-v8a 或 x86_64 架构的未对齐库
  critical_unaligned = unaligned_libs.select do |lib|
    arch = lib[:architecture]
    arch == 'arm64-v8a' || arch == 'x86_64'
  end
  
  if critical_unaligned.empty?
    result.elf_alignment_compliant = true
    # 合规时不输出日志
  else
    result.elf_alignment_compliant = false
    # 只在不合规时输出日志
    puts "\n\e[1m--- ELF 对齐检测 ---\e[0m"
    critical_unaligned.each do |lib|
      result.add_issue("#{lib[:architecture]} 架构的共享库 #{File.basename(lib[:file])} 未对齐 (16KB 页面大小要求)")
      puts "\e[31m✗ #{lib[:architecture]} 架构的共享库 #{File.basename(lib[:file])} 未对齐 (16KB 页面大小要求)\e[0m"
    end
    puts "\e[31m✗ 发现 #{critical_unaligned.length} 个关键架构的未对齐共享库\e[0m"
  end
end

.check_libunity_patch(temp_dir, verbose: true) ⇒ Boolean

检查 libunity.so 漏洞修复

Parameters:

  • temp_dir (String)

    临时目录路径

  • verbose (Boolean) (defaults to: true)

    是否输出详细日志,默认为 true

Returns:

  • (Boolean)

    是否通过检测



1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 1084

def self.check_libunity_patch(temp_dir, verbose: true)
  # 收集所有 libunity.so 文件
  libunity_files = find_so_files(temp_dir, 'libunity.so')
  
  # 如果没有找到 libunity.so 文件,返回 true(不是 Unity 项目或不需要检查)
  if libunity_files.empty?
    return true
  end
  
  # 必须所有 libunity.so 文件都通过检查
  xrsdk_all_passed = true
  override_all_disabled = true
  override_found = false
  uses_il2cpp = uses_il2cpp?(temp_dir)
  
  libunity_files.each do |so_file|
    puts "检查文件: #{File.basename(so_file)}" if verbose
    
    # 检查 xrsdk 字符串修改
    xrsdk_passed = check_xrsdk_patch(so_file, verbose: verbose)
    if xrsdk_passed
      puts "  \e[32m✓ xrsdk-pre-init-library 字符串已正确修改\e[0m" if verbose
    else
      puts "  \e[31m✗ xrsdk-pre-init-library 字符串未正确修改\e[0m" if verbose
      xrsdk_all_passed = false
    end
    
    # 检查 overrideMonoSearchPath 禁用(仅在使用 Mono 时检查)
    if uses_il2cpp
      puts "  \e[36m信息: 使用 IL2CPP,跳过 overrideMonoSearchPath 检测\e[0m" if verbose
      # IL2CPP 不需要检查 overrideMonoSearchPath
    else
      override_result = check_override_patch(so_file, verbose: verbose)
      if override_result[:found]
        override_found = true
        if override_result[:disabled]
          puts "  \e[32m✓ overrideMonoSearchPath 已正确禁用\e[0m" if verbose
        else
          puts "  \e[31m✗ overrideMonoSearchPath 未正确禁用\e[0m" if verbose
          override_all_disabled = false
        end
      else
        puts "  \e[36m信息: 未找到 overrideMonoSearchPath 字符串\e[0m" if verbose
      end
    end
  end
  
  # 如果没有找到 overrideMonoSearchPath 或使用 IL2CPP,认为 override 检查通过
  if !override_found || uses_il2cpp
    override_all_disabled = true
  end
  
  # 所有 libunity.so 文件都必须通过 xrsdk 检查,并且 override 检查也必须通过
  xrsdk_all_passed && override_all_disabled
end

.check_override_patch(so_file, verbose: true) ⇒ Hash

检查 overrideMonoSearchPath 禁用

Parameters:

  • so_file (String)

    .so 文件路径

  • verbose (Boolean) (defaults to: true)

    是否输出详细日志,默认为 true

Returns:

  • (Hash)

    检测结果 Boolean, disabled: Boolean



1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 1176

def self.check_override_patch(so_file, verbose: true)
  result = { found: false, disabled: false }
  
  begin
    # 读取文件内容
    file_content = File.binread(so_file)
    
    # 查找 overrideMonoSearchPath 字符串
    override_pattern = "overrideMonoSearchPath"
    pattern_pos = file_content.index(override_pattern)
    
    if pattern_pos
      result[:found] = true
      
      # 检查字符串前一个字节是否为 0xC0
      if pattern_pos > 0
        prev_byte = file_content[pattern_pos - 1].ord
        if prev_byte == 0xC0
          result[:disabled] = true
        end
      end
    end
    
  rescue => e
    puts "  \e[33m警告: 检查 #{so_file} 时出错: #{e.message}\e[0m" if verbose
  end
  
  result
end

.check_target_sdk_compliance(temp_dir, result, aab_path = nil) ⇒ Object

检测 Target SDK 版本合规性



533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 533

def self.check_target_sdk_compliance(temp_dir, result, aab_path = nil)
  target_sdk = 0
  
  # 方法1: 使用 bundletool dump manifest(首要方法)
  if aab_path && File.exist?(aab_path)
    target_sdk = extract_target_sdk_with_bundletool(aab_path)
  end
  
  # 方法2: 如果 bundletool 失败,使用二进制 XML 解析(备用方法)
  if target_sdk == 0
    manifest_path = find_android_manifest(temp_dir)
    if manifest_path
      target_sdk = extract_target_sdk_from_manifest(manifest_path)
    end
  end

  result.target_sdk_version = target_sdk

  if target_sdk >= 35
    result.target_sdk_compliant = true
    # 合规时不输出日志
  else
    result.target_sdk_compliant = false
    # 只在不合规时输出日志
    puts "\n\e[1m--- Target SDK 版本检测 ---\e[0m"
    if target_sdk == 0
      result.add_issue("无法检测到 Target SDK 版本,请检查 AAB 文件结构")
      puts "\e[31m✗ 无法检测到 Target SDK 版本\e[0m"
    else
      result.add_issue("Target SDK #{target_sdk} 不符合要求,需要至少 Target SDK 35 (Android 15)")
      puts "\e[33m✗ Target SDK: #{target_sdk} (需要至少 35)\e[0m"
    end
  end
end

.check_unity_patch_compliance(temp_dir, result) ⇒ Object

检测 Unity 漏洞合规性



1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 1003

def self.check_unity_patch_compliance(temp_dir, result)
  # 检查是否存在 Unity 相关文件
  unity_files = find_unity_files(temp_dir)
  
  if unity_files.empty?
    # 没有 Unity 文件,可能是非 Unity 项目,合规
    result.unity_patch_compliant = true
    # 不输出日志
    return
  end
  
  # 找到了 Unity 文件,需要检查漏洞修复
  # 检查 libunity.so 文件(不输出日志)
  libunity_result = check_libunity_patch(temp_dir, verbose: false)
  
  # 检查 boot.config 文件(不输出日志)
  boot_config_result = check_boot_config_patch(temp_dir, verbose: false)
  
  # 综合判断:libunity.so 和 boot.config 都必须通过检查
  if libunity_result && boot_config_result
    result.unity_patch_compliant = true
    result.unity_xrsdk_patched = true
    result.unity_override_patched = true
    # 合规时不输出日志
  else
    result.unity_patch_compliant = false
    # 只在不合规时输出详细日志
    puts "\n\e[1m--- Unity 漏洞检测 ---\e[0m"
    puts "检测到 Unity 项目,开始检查漏洞修复..."
    
    if !libunity_result
      result.add_issue("Unity libunity.so 漏洞未修复")
      # 重新检查并输出详细日志
      check_libunity_patch(temp_dir, verbose: true)
    end
    
    if !boot_config_result
      result.add_issue("Unity boot.config 漏洞未修复")
      # 重新检查并输出详细日志
      check_boot_config_patch(temp_dir, verbose: true)
    end
    
    puts "\e[31m✗ Unity 漏洞检测: 未通过\e[0m"
  end
end

.check_xrsdk_patch(so_file, verbose: true) ⇒ Boolean

检查 xrsdk 字符串修补

Parameters:

  • so_file (String)

    .so 文件路径

  • verbose (Boolean) (defaults to: true)

    是否输出详细日志,默认为 true

Returns:

  • (Boolean)

    是否通过检测



1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 1144

def self.check_xrsdk_patch(so_file, verbose: true)
  # 检查是否还存在原始的 xrsdk 字符串
  has_original = false
  has_modified = false
  
  begin
    # 使用安全的方式执行 strings 命令,处理路径中的非ASCII字符
    strings_output, _, strings_status = safe_execute_command('strings', so_file)
    
    if strings_status.success? && strings_output
      if strings_output.include?("xrsdk-pre-init-library")
        has_original = true
      end
      
      if strings_output.include?("8rsdk-pre-init-library")
        has_modified = true
      end
    end
    
    # 如果存在修改后的字符串且不存在原始字符串,说明修补成功
    has_modified && !has_original
    
  rescue => e
    puts "  \e[33m警告: 检查 #{so_file} 时出错: #{e.message}\e[0m" if verbose
    false
  end
end

.determine_architecture(so_file) ⇒ Object

确定共享库的架构



956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 956

def self.determine_architecture(so_file)
  # 从文件路径中提取架构信息
  if so_file.include?('/arm64-v8a/')
    'arm64-v8a'
  elsif so_file.include?('/x86_64/')
    'x86_64'
  elsif so_file.include?('/armeabi-v7a/')
    'armeabi-v7a'
  elsif so_file.include?('/x86/')
    'x86'
  elsif so_file.include?('/armeabi/')
    'armeabi'
  else
    'unknown'
  end
end

.extract_internal_file(archive_path, internal_file, dest_dir) ⇒ Hash

解压压缩文件(AAB/APK)内部的单个文件注意:内部文件路径不会被 File.expand_path 处理,保持原样

Parameters:

  • archive_path (String)

    压缩文件路径(AAB 或 APK)

  • internal_file (String)

    压缩文件内部的相对路径(如 base/lib/arm64-v8a/libunity.so)

  • dest_dir (String)

    解压目标目录

Returns:

  • (Hash)

    Boolean, error: String



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 135

def self.extract_internal_file(archive_path, internal_file, dest_dir)
  begin
    abs_archive_path = File.expand_path(archive_path)
    abs_dest_dir = File.expand_path(dest_dir)
    stdout, stderr, status = Open3.capture3(get_utf8_env, 'unzip', '-q', abs_archive_path, internal_file, '-d', abs_dest_dir)
    
    if status.success?
      return { success: true, error: nil }
    else
      error_text = [stderr, stdout].compact.join(' ')
      if is_file_not_found_error?(error_text)
        # 文件不存在,可以忽略
        return { success: true, error: nil }
      else
        return { success: false, error: error_text }
      end
    end
  rescue => e
    return { success: false, error: e.message }
  end
end

.extract_required_files(archive_path, dest_dir) ⇒ Hash

从压缩文件中列出并提取需要的文件

Parameters:

  • archive_path (String)

    压缩文件路径

  • dest_dir (String)

    解压目标目录

Returns:

  • (Hash)

    Boolean, error_messages: Array<String>



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 233

def self.extract_required_files(archive_path, dest_dir)
  error_messages = []
  failed = false
  
  # 优先使用 unzip -Z1
  unzip_list_output, _, unzip_list_status = safe_execute_command('unzip', '-Z1', archive_path)
  if unzip_list_status.success? && unzip_list_output && !unzip_list_output.strip.empty?
    required_files = parse_unzip_z1_output(unzip_list_output)
  else
    # 回退到 unzip -l
    unzip_list_output, _, unzip_list_status = safe_execute_command('unzip', '-l', archive_path)
    if unzip_list_status.success? && unzip_list_output
      required_files = parse_unzip_l_output(unzip_list_output)
    else
      return { success: false, error_messages: ["无法列出 #{archive_path} 中的文件"] }
    end
  end
  
  # 解压文件
  required_files.each do |file|
    extract_result = extract_internal_file(archive_path, file, dest_dir)
    unless extract_result[:success]
      failed = true
      error_messages << "解压 #{file} 失败: #{extract_result[:error]}"
    end
  end
  
  { success: !failed, error_messages: error_messages }
end

.extract_required_files_from_aab(aab_path, temp_dir) ⇒ Hash

解压 AAB 文件中的必要文件只解压合规检测需要的文件,避免解压大型 assets 文件导致错误合规检测只需要:

  1. base/manifest/ - Target SDK 检测(如果 bundletool 失败)

  2. base/lib/ - ELF 对齐检测(递归解压所有 .so 文件)

  3. base.apk - 需要进一步解压其中的 lib/

  4. base/assets/bin/Data/boot.config - Unity 检测(如果存在)

注意:不解压 assets 目录下的其他文件(如 bundle 文件),避免解压错误

Parameters:

  • aab_path (String)

    AAB 文件路径

  • temp_dir (String)

    临时目录路径

Returns:

  • (Hash)

    Boolean, error_message: String



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 274

def self.extract_required_files_from_aab(aab_path, temp_dir)
  # 解压 AAB 文件中的必要文件
  extract_result = extract_required_files(aab_path, temp_dir)
  unzip_error_messages = extract_result[:error_messages]
  unzip_failed = !extract_result[:success]
  
  # 如果 base.apk 存在,需要进一步解压其中的 lib 目录
  base_apk_path = File.join(temp_dir, "base.apk")
  if File.exist?(base_apk_path)
    # 尝试解压整个 lib/* 目录(包括所有子目录和文件)
    # 这样可以避免逐个文件解压时遇到文件名编码问题
    base_apk_lib_output, base_apk_lib_error, base_apk_lib_status = safe_execute_command('unzip', '-q', base_apk_path, 'lib/*', '-d', temp_dir)
    unless base_apk_lib_status.success?
      # 如果解压 lib/* 失败,尝试只解压 .so 文件
      # 优先使用 unzip -Z1 列出文件
      base_apk_list_output, _, base_apk_list_status = safe_execute_command('unzip', '-Z1', base_apk_path)
      if base_apk_list_status.success? && base_apk_list_output && !base_apk_list_output.strip.empty?
        lib_files = base_apk_list_output.lines.map(&:strip).select { |f| f.start_with?('lib/') && f.end_with?('.so') }
      else
        # 回退到 unzip -l
        base_apk_list_output, _, base_apk_list_status = safe_execute_command('unzip', '-l', base_apk_path)
        if base_apk_list_status.success? && base_apk_list_output
          lib_files = parse_unzip_l_output(base_apk_list_output).select { |f| f.start_with?('lib/') && f.end_with?('.so') }
        else
          lib_files = []
        end
      end
      
      # 尝试逐个解压 .so 文件
      lib_files.each do |lib_file|
        extract_result = extract_internal_file(base_apk_path, lib_file, temp_dir)
        unless extract_result[:success]
          # 单个文件解压失败,记录警告但不标记为整体失败
          # 因为 find_shared_libraries 会再次尝试解压
          unzip_error_messages << "警告: 解压 base.apk 中的 #{lib_file} 失败: #{extract_result[:error]}"
        end
      end
    end
  end
  
  # 返回结果
  # 只在实际失败时返回 false,警告信息不影响成功状态
  # 因为 find_shared_libraries 会再次尝试解压 base.apk 中的文件
  critical_errors = unzip_error_messages.reject { |msg| msg.include?('警告:') }
  if unzip_failed && critical_errors.any?
    error_text = critical_errors.join('; ')
    error_msg = "无法解压 AAB 文件中的必要文件: #{error_text}"
    return { success: false, error_message: error_msg }
  else
    return { success: true, error_message: nil }
  end
end

.extract_target_sdk_from_manifest(manifest_path) ⇒ Object

从 AndroidManifest.xml 中提取 Target SDK 版本



680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 680

def self.extract_target_sdk_from_manifest(manifest_path)
  begin
    # 读取文件内容(二进制模式)
    content = File.binread(manifest_path)
    
    # 检查是否为二进制 XML
    if content.start_with?("\x03\x00\x08\x00") || content.include?("\x00")
      
      # 在二进制 XML 中搜索 targetSdkVersion 后的数字
      # 模式: targetSdkVersion + 一些二进制数据 + 数字
      if content =~ /targetSdkVersion[^\d]*(\d{1,2})/
        return $1.to_i
      end
      
      # 尝试更宽泛的搜索模式
      if content =~ /targetSdkVersion.*?(\d{1,2})/
        return $1.to_i
      end
      
      return 0
    end
    
    # 尝试使用 aapt 工具解析(适用于文本格式)
    if tool_available?('aapt')
      aapt_output, _, aapt_status = safe_execute_command('aapt', 'dump', 'badging', manifest_path)
      if aapt_status.success? && aapt_output && !aapt_output.empty?
        if aapt_output =~ /targetSdkVersion:'(\d+)'/
          return $1.to_i
        end
      end
    end
    
    # 尝试使用 aapt2 工具
    if tool_available?('aapt2')
      aapt2_output, _, aapt2_status = safe_execute_command('aapt2', 'dump', 'badging', manifest_path)
      if aapt2_status.success? && aapt2_output && !aapt2_output.empty?
        if aapt2_output =~ /targetSdkVersion:'(\d+)'/
          return $1.to_i
        end
      end
    end
    
    # 尝试使用 aapt dump xmltree
    if tool_available?('aapt')
      aapt_xml_output, _, aapt_xml_status = safe_execute_command('aapt', 'dump', 'xmltree', manifest_path)
      if aapt_xml_status.success? && aapt_xml_output && !aapt_xml_output.empty?
        if aapt_xml_output =~ /targetSdkVersion.*?(\d+)/
          return $1.to_i
        end
      end
    end
    
    # 解析文本 XML
    doc = Nokogiri::XML(content)
    
    # 处理命名空间
    doc.remove_namespaces!
    
    # 查找 uses-sdk 标签
    uses_sdk = doc.at_xpath('//uses-sdk')
    if uses_sdk
      target_sdk = uses_sdk['targetSdkVersion']
      if target_sdk
        return target_sdk.to_i
      end
    end
    
    # 如果没找到 uses-sdk 标签,尝试查找其他可能的标签
    target_sdk_attrs = doc.xpath('//@targetSdkVersion')
    if !target_sdk_attrs.empty?
      target_sdk = target_sdk_attrs.first.value
      return target_sdk.to_i
    end
    
    # 尝试查找所有包含 targetSdkVersion 的属性
    all_attrs = doc.xpath('//@*[contains(name(), "targetSdkVersion")]')
    if !all_attrs.empty?
      target_sdk = all_attrs.first.value
      return target_sdk.to_i
    end
    
    return 0
    
  rescue => e
    return 0
  end
end

.extract_target_sdk_with_bundletool(aab_path) ⇒ Object

使用 bundletool 提取 Target SDK 版本



570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 570

def self.extract_target_sdk_with_bundletool(aab_path)
  begin
    # 获取 Pindo 自带的 bundletool.jar 路径
    bundletool_jar = get_pindo_bundletool_path
    unless bundletool_jar && File.exist?(bundletool_jar)
      return extract_target_sdk_with_system_bundletool(aab_path)
    end
    
    # 使用 Pindo 自带的 bundletool.jar
    # 确保使用正确的 Java 版本 (Java 11+)
    java_cmd = get_java_command_for_bundletool
    
    # 使用安全的方式执行 bundletool 命令,处理路径中的非ASCII字符
    # 先执行 bundletool 命令,然后通过管道传递给 grep
    bundletool_args = [java_cmd, '-jar', bundletool_jar, 'dump', 'manifest', '--bundle', aab_path]
    bundletool_output, _, bundletool_status = safe_execute_command(*bundletool_args)
    
    # 如果命令成功,在输出中查找 targetSdkVersion
    output = ''
    if bundletool_status.success? && bundletool_output
      output = bundletool_output.lines.grep(/targetSdkVersion/).join
    end
    
    if output && !output.empty?
      # 解析输出格式: <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
      if output =~ /android:targetSdkVersion="(\d+)"/
        target_sdk = $1.to_i
        return target_sdk
      end
    end
    
    return 0
  rescue => e
    return 0
  end
end

.extract_target_sdk_with_system_bundletool(aab_path) ⇒ Object

使用系统 bundletool 命令(备用方法)



608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 608

def self.extract_target_sdk_with_system_bundletool(aab_path)
  begin
    # 检查系统 bundletool 是否可用
    unless tool_available?('bundletool')
      return 0
    end
    
    # 使用系统 bundletool 命令
    # 使用安全的方式执行 bundletool 命令,处理路径中的非ASCII字符
    bundletool_output, _, bundletool_status = safe_execute_command('bundletool', 'dump', 'manifest', '--bundle', aab_path)
    
    # 如果命令成功,在输出中查找 targetSdkVersion
    output = ''
    if bundletool_status.success? && bundletool_output
      output = bundletool_output.lines.grep(/targetSdkVersion/).join
    end
    
    if output && !output.empty?
      # 解析输出格式: <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
      if output =~ /android:targetSdkVersion="(\d+)"/
        target_sdk = $1.to_i
        return target_sdk
      end
    end
    
    return 0
  rescue => e
    return 0
  end
end

.find_android_manifest(temp_dir) ⇒ Object

查找 AndroidManifest.xml 文件(备用方法)



660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 660

def self.find_android_manifest(temp_dir)
  # 查找可能的 AndroidManifest.xml 路径
  possible_paths = [
    # AAB 解压后的标准路径
    File.join(temp_dir, "base", "manifest", "AndroidManifest.xml"),
    File.join(temp_dir, "manifest", "AndroidManifest.xml"),
    # 其他可能的路径
    File.join(temp_dir, "AndroidManifest.xml"),
    # 递归查找
    Dir.glob(File.join(temp_dir, "**", "AndroidManifest.xml")).first
  ]
  
  possible_paths.each do |path|
    return path if path && File.exist?(path)
  end
  
  nil
end

.find_shared_libraries(temp_dir) ⇒ Object

查找共享库文件



818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 818

def self.find_shared_libraries(temp_dir)
  so_files = []
  
  # 在 temp_dir 中查找所有 .so 文件
  Dir.glob(File.join(temp_dir, "**", "*.so")).each do |so_file|
    so_files << so_file
  end
  
  # 也检查 base.apk 中的库文件
  base_apk_path = File.join(temp_dir, "base.apk")
  if File.exist?(base_apk_path)
    base_temp_dir = nil
    begin
      base_temp_dir = Dir.mktmpdir("base_apk_libs_")
      
      # 解压 base.apk 中的 lib 目录
      # 使用 safe_execute_command 解压 lib/*,因为这是通配符模式
      lib_extract_output, _, lib_extract_status = safe_execute_command('unzip', '-q', base_apk_path, 'lib/*', '-d', base_temp_dir)
      if lib_extract_status.success?
        Dir.glob(File.join(base_temp_dir, "lib", "**", "*.so")).each do |so_file|
          so_files << so_file
        end
      end
    ensure
      FileUtils.rm_rf(base_temp_dir) if base_temp_dir && File.directory?(base_temp_dir)
    end
  end
  
  so_files.uniq
end

.find_so_files(temp_dir, so_filename) ⇒ Array<String>

在临时目录中查找指定的 .so 文件

Parameters:

  • temp_dir (String)

    临时目录路径

  • so_filename (String)

    .so 文件名(如 libunity.so, libil2cpp.so)

Returns:

  • (Array<String>)

    找到的文件路径数组



173
174
175
176
177
178
179
180
181
182
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 173

def self.find_so_files(temp_dir, so_filename)
  so_files = []
  get_lib_dirs(temp_dir).each do |lib_dir|
    if Dir.exist?(lib_dir)
      found_files = Dir.glob("#{lib_dir}/**/#{so_filename}")
      so_files += found_files
    end
  end
  so_files
end

.find_unity_files(temp_dir) ⇒ Object

查找 Unity 相关文件



1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 1050

def self.find_unity_files(temp_dir)
  unity_files = []
  
  # 查找 libunity.so 文件
  unity_files += find_so_files(temp_dir, 'libunity.so')
  
  # 查找 boot.config 文件
  boot_config_paths = [
    "#{temp_dir}/assets/bin/Data/boot.config",
    "#{temp_dir}/base/assets/bin/Data/boot.config",
    "#{temp_dir}/base/bin/Data/boot.config"
  ]
  
  boot_config_paths.each do |path|
    if File.exist?(path)
      unity_files << path
      break
    end
  end
  
  unity_files
end

.get_java_command_for_bundletoolObject

获取用于 bundletool 的 Java 命令



655
656
657
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 655

def self.get_java_command_for_bundletool
  Pindo::JavaEnvHelper.find_java_command
end

.get_lib_dirs(temp_dir) ⇒ Array<String>

获取可能的 lib 目录列表(用于查找 .so 文件)

Parameters:

  • temp_dir (String)

    临时目录路径

Returns:

  • (Array<String>)

    lib 目录路径数组



160
161
162
163
164
165
166
167
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 160

def self.get_lib_dirs(temp_dir)
  [
    "#{temp_dir}/lib", 
    "#{temp_dir}/libs",
    "#{temp_dir}/base/lib",  # AAB文件结构
    "#{temp_dir}/base/libs"  # AAB文件结构
  ]
end

.get_pindo_bundletool_pathObject

获取 Pindo 自带的 bundletool.jar 路径



640
641
642
643
644
645
646
647
648
649
650
651
652
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 640

def self.get_pindo_bundletool_path
  begin
    # 使用与 apk_helper.rb 相同的方法获取工具路径
    pindo_dir = File.expand_path(ENV['PINDO_DIR'] || '~/.pindo')
    pindo_common_configdir = File.join(pindo_dir, "pindo_common_config")
    tools_dir = File.join(pindo_common_configdir, 'android_tools')
    bundletool_jar = File.join(tools_dir, 'bundletool.jar')
    
    File.exist?(bundletool_jar) ? bundletool_jar : nil
  rescue
    nil
  end
end

.get_utf8_envHash

获取 UTF-8 环境变量(用于执行命令)

Returns:

  • (Hash)

    环境变量哈希



112
113
114
115
116
117
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 112

def self.get_utf8_env
  {
    'LANG' => 'en_US.UTF-8',
    'LC_ALL' => 'en_US.UTF-8'
  }
end

.is_file_not_found_error?(error_text) ⇒ Boolean

判断错误信息是否表示文件不存在(可以安全忽略)

Parameters:

  • error_text (String)

    错误信息

Returns:

  • (Boolean)

    如果错误表示文件不存在,返回 true



122
123
124
125
126
127
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 122

def self.is_file_not_found_error?(error_text)
  error_text.include?('filename not matched') || 
  error_text.include?('cannot find') || 
  error_text.include?('not found') ||
  error_text.empty?
end

.is_required_file?(filename) ⇒ Boolean

判断文件是否是合规检测需要的文件

Parameters:

  • filename (String)

    AAB 文件内部的相对路径

Returns:

  • (Boolean)

    如果是需要的文件,返回 true



187
188
189
190
191
192
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 187

def self.is_required_file?(filename)
  filename.start_with?('base/manifest/') ||
  (filename.start_with?('base/lib/') && filename.end_with?('.so')) ||
  filename == 'base.apk' ||
  filename == 'base/assets/bin/Data/boot.config'
end

.parse_unzip_l_output(output) ⇒ Array<String>

从 unzip -l 输出中提取文件列表

Parameters:

  • output (String)

    unzip -l 的输出

Returns:

  • (Array<String>)

    文件路径数组



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 210

def self.parse_unzip_l_output(output)
  files = []
  output.lines.each do |line|
    # unzip -l 输出格式:长度 日期 时间 文件名
    # 跳过表头和空行
    next if line.strip.empty? || line.start_with?('Archive:') || 
            line.start_with?('Length') || line.start_with?('------') ||
            line.match(/^\s*$/) || line.match(/^\s*\d+\s+files?\s*$/)
    
    # 提取文件名:从行尾开始,跳过前面的数字和日期时间字段
    filename_match = line.match(/^\s*\d+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+(.+)$/)
    next unless filename_match
    
    filename = filename_match[1].strip
    files << filename if is_required_file?(filename)
  end
  files
end

.parse_unzip_z1_output(output) ⇒ Array<String>

从 unzip -Z1 输出中提取文件列表

Parameters:

  • output (String)

    unzip -Z1 的输出

Returns:

  • (Array<String>)

    文件路径数组



197
198
199
200
201
202
203
204
205
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 197

def self.parse_unzip_z1_output(output)
  files = []
  output.lines.each do |line|
    filename = line.strip
    next if filename.empty?
    files << filename if is_required_file?(filename)
  end
  files
end

打印合规检测摘要



974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 974

def self.print_compliance_summary(result)
  puts "\n\e[1m--- 生成合规检测摘要 ---\e[0m"
  
  # 总体合规状态
  if result.compliant?
    Funlog.fancyinfo_success("符合 Google Play 最新合规要求")
  else
    Funlog.fancyinfo_error("不符合 Google Play 合规要求")
    if !result.size_compliant
      Funlog.error("AAB 包体积: #{result.aab_size_mb} MB (超出限制)")
    end
    if !result.target_sdk_compliant
      Funlog.error("Target SDK: #{result.target_sdk_version} (需要至少 35)")
    end
    if !result.elf_alignment_compliant
      Funlog.error("ELF 对齐: 发现 #{result.unaligned_libs.length} 个未对齐的共享库")
    end
    if !result.unity_patch_compliant
      Funlog.warning("Unity 漏洞修复未检测到或不完整")
    end
  end
  
  # 警告信息
  result.warnings.each do |warning|
    Funlog.warning(warning)
  end
end

.safe_execute_command(*args) ⇒ Object

安全执行命令并处理路径编码问题确保包含非ASCII字符(如中文、韩语)的路径能够正确处理



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 329

def self.safe_execute_command(*args)
  begin
    # 确保所有参数都是字符串,并正确处理编码
    safe_args = args.map.with_index do |arg, index|
      if arg.is_a?(String)
        # 第一个参数通常是命令名(如 'unzip' 或 '/usr/bin/java')
        # 绝对路径(以 '/' 开头)不需要展开,已经是完整路径
        # 相对路径(包含 '/' 或 '.')需要展开
        if index == 0
          # 第一个参数(命令名):绝对路径保持原样,相对路径展开
          if arg.start_with?('/')
            # 绝对路径:只处理编码,不展开
            arg.dup.force_encoding('UTF-8').scrub('')
          else
            # 相对路径或命令名:不展开(让系统在 PATH 中查找)
            arg.dup.force_encoding('UTF-8').scrub('')
          end
        elsif arg.start_with?('/')
          # 绝对路径参数:只处理编码,不展开
          arg.dup.force_encoding('UTF-8').scrub('')
        elsif arg.include?('/') || arg.start_with?('.')
          # 相对路径参数:使用 File.expand_path 规范化路径
          normalized_path = File.expand_path(arg)
          normalized_path.dup.force_encoding('UTF-8').scrub('')
        else
          # 其他参数(如选项 '-q', '-d'):只处理编码
          arg.dup.force_encoding('UTF-8').scrub('')
        end
      else
        arg.to_s
      end
    end
    
    # 使用 Open3 执行命令,避免shell解析问题
    # 设置环境变量确保输出编码正确
    stdout, stderr, status = Open3.capture3(get_utf8_env, *safe_args)
    
    # 确保输出使用UTF-8编码,并清理可能的路径拼接错误
    if stdout
      stdout = stdout.force_encoding('UTF-8')
      stdout = stdout.scrub('?') unless stdout.valid_encoding?
      
      # 清理可能的路径拼接错误:移除临时目录路径和AAB文件名的错误拼接
      # 例如:launcher-debug.aab/private/var/folders/... 应该被清理
      stdout = stdout.lines.map do |line|
        # 如果行中包含临时目录路径模式(/private/var/folders 或 /tmp/),
        # 并且前面有AAB文件名,则移除临时目录部分
        if line.match(/\.aab\/[\/\w\-]+(?:private\/var\/folders|tmp\/|aab_compliance_check_)/)
          # 提取AAB文件名后的相对路径部分
          # 例如:launcher-debug.aab/private/var/.../yoo_pack/... 
          # 应该只保留 yoo_pack/... 部分
          if match = line.match(/\.aab\/[\/\w\-]+(?:private\/var\/folders|tmp\/|aab_compliance_check_)[\/\w\-]+(.+)$/)
            # 保留压缩包内的相对路径
            line.sub(/^[^:]*\.aab\/[\/\w\-]+(?:private\/var\/folders|tmp\/|aab_compliance_check_)[\/\w\-]+/, '')
          else
            line
          end
        else
          line
        end
      end.join
    end
    
    [stdout, stderr, status]
  rescue => e
    # 如果出错,返回空结果
    # 创建一个简单的状态对象,模拟 Process::Status
    failed_status = Object.new
    def failed_status.success?; false; end
    ['', e.message, failed_status]
  end
end

.safe_execute_shell_command(command_template, **kwargs) ⇒ Object

安全执行shell命令(用于需要管道等shell特性的情况)使用 Shellwords.escape 转义路径参数



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 404

def self.safe_execute_shell_command(command_template, **kwargs)
  begin
    # 转义所有路径参数
    escaped_kwargs = kwargs.transform_values do |value|
      if value.is_a?(String)
        # 确保字符串使用UTF-8编码
        safe_value = value.dup.force_encoding('UTF-8').scrub('')
        Shellwords.escape(safe_value)
      else
        Shellwords.escape(value.to_s)
      end
    end
    
    # 替换模板中的占位符
    command = command_template % escaped_kwargs
    
    # 执行命令
    output = `#{command} 2>/dev/null`
    
    # 确保输出使用UTF-8编码
    if output
      output = output.force_encoding('UTF-8')
      output = output.scrub('?') unless output.valid_encoding?
    end
    
    output || ''
  rescue => e
    ''
  end
end

.tool_available?(tool_name) ⇒ Boolean

检查工具是否可用

Returns:

  • (Boolean)


436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 436

def self.tool_available?(tool_name)
  case tool_name
  when 'bundletool'
    # 检查 bundletool 是否可用(可能是别名或直接命令)
    # 使用 Open3 来正确检测命令是否可用
    require 'open3'
    begin
      stdout, stderr, status = Open3.capture3('bundletool help')
      status.success?
    rescue
      false
    end
  when 'aapt'
    system("which aapt > /dev/null 2>&1") || system("aapt version > /dev/null 2>&1")
  when 'aapt2'
    system("which aapt2 > /dev/null 2>&1") || system("aapt2 version > /dev/null 2>&1")
  when 'objdump'
    system("which objdump > /dev/null 2>&1")
  when 'readelf'
    system("which readelf > /dev/null 2>&1")
  when 'unzip'
    system("which unzip > /dev/null 2>&1")
  else
    system("which #{tool_name} > /dev/null 2>&1")
  end
end

.uses_il2cpp?(temp_dir) ⇒ Boolean

检查是否使用 il2cpp

Returns:

  • (Boolean)


1074
1075
1076
1077
1078
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 1074

def self.uses_il2cpp?(temp_dir)
  # 检查是否存在 libil2cpp.so 文件
  il2cpp_files = find_so_files(temp_dir, 'libil2cpp.so')
  il2cpp_files.any?
end