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 合规性



49
50
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
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 49

def self.check_aab_compliance(aab_path)
  result = ComplianceResult.new
  
  unless File.exist?(aab_path)
    result.add_issue("AAB 文件不存在: #{aab_path}")
    return result
  end
  
  Funlog.fancyinfo_start("Google Play 合规检测: #{File.basename(aab_path)}")
  
  # 创建临时目录用于解压 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 文件
    unless system("unzip", "-q", aab_path, "-d", temp_dir)
      result.add_issue("无法解压 AAB 文件")
      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 包体积合规性



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 131

def self.check_aab_size_compliance(aab_path, result)
  Funlog.fancyinfo_update("检测 AAB 包体积...")
  
  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_out = `unzip -v "#{aab_path}"`
    if unzip_out && !unzip_out.empty?
      base_lines = unzip_out.lines.select { |l| l.include?(" base/") }
      base_size = base_lines.map { |l| l.split[2].to_i }.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),请优化资源或分包")
      Funlog.error("base 文件夹超出限制: #{result.base_size_mb}MB (限制 #{base_limit_mb}MB)")
    else
      result.size_compliant = true
    end

  rescue => e
    result.add_issue("AAB 包体积检测失败: #{e.message}")
  end
end

.check_boot_config_patch(temp_dir) ⇒ Object

检查 boot.config 漏洞修复



841
842
843
844
845
846
847
848
849
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
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 841

def self.check_boot_config_patch(temp_dir)
  # 尝试多个可能的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
    puts "  \e[33m信息: 未找到 boot.config 文件\e[0m"
    return true  # 没有 boot.config 文件,认为是正常的
  end
  
  puts "检查文件: #{File.basename(boot_config_path)}"
  
  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"
    end
    
    if modified_count > 0
      puts "  \e[32m✓ boot.config 中发现 #{modified_count} 个修改后的 8rsdk-pre-init-library\e[0m"
    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"
    end
    
    # 如果没有 xrsdk 相关字符串,认为是正常的
    if other_xrsdk_count == 0
      puts "  \e[32m✓ boot.config 检查结果: 通过(未使用 XR SDK,无需修补)\e[0m"
      return true
    end
    
    # 如果存在修改后的字符串且不存在未修改的字符串,说明修补成功
    if modified_count > 0 && original_count == 0
      puts "  \e[32m✓ boot.config 检查结果: 通过(已正确替换 #{modified_count} 个字符串)\e[0m"
      return true
    elsif original_count > 0
      puts "  \e[31m✗ boot.config 检查结果: 未通过(存在未修改的字符串)\e[0m"
      return false
    else
      puts "  \e[31m✗ boot.config 检查结果: 未通过(未找到修改后的字符串)\e[0m"
      return false
    end
    
  rescue => e
    puts "  \e[33m警告: 检查 boot.config 时出错: #{e.message}\e[0m"
    false
  end
end

.check_elf_alignment(so_file) ⇒ Object

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



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
531
532
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
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 481

def self.check_elf_alignment(so_file)
  begin
    # 首先检查文件是否为有效的 ELF 文件
    file_output = `file "#{so_file}" 2>/dev/null`
    unless 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_output = `objdump -p "#{so_file}" 2>/dev/null`
    
    if 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_output = `readelf -l "#{so_file}" 2>/dev/null`
    if 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 对齐合规性



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
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 405

def self.check_elf_alignment_compliance(temp_dir, result)
  Funlog.fancyinfo_update("检测 ELF 对齐 (16KB 页面大小)...")
  
  # 查找所有 .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
    critical_unaligned.each do |lib|
      result.add_issue("#{lib[:architecture]} 架构的共享库 #{File.basename(lib[:file])} 未对齐 (16KB 页面大小要求)")
    end
    Funlog.error("发现 #{critical_unaligned.length} 个关键架构的未对齐共享库")
  end
end

.check_libunity_patch(temp_dir) ⇒ Object

检查 libunity.so 漏洞修复



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
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 725

def self.check_libunity_patch(temp_dir)
  lib_dirs = [
    "#{temp_dir}/lib", 
    "#{temp_dir}/libs",
    "#{temp_dir}/base/lib",  # AAB文件结构
    "#{temp_dir}/base/libs"  # AAB文件结构
  ]
  xrsdk_changed = false
  override_disabled = false
  override_found = false
  uses_il2cpp = uses_il2cpp?(temp_dir)
  
  lib_dirs.each do |lib_dir|
    next unless Dir.exist?(lib_dir)
    
    libunity_files = Dir.glob("#{lib_dir}/**/libunity.so")
    
    libunity_files.each do |so_file|
      puts "检查文件: #{File.basename(so_file)}"
      
      # 检查 xrsdk 字符串修改
      if check_xrsdk_patch(so_file)
        xrsdk_changed = true
        puts "  \e[32m✓ xrsdk-pre-init-library 字符串已正确修改\e[0m"
      else
        puts "  \e[31m✗ xrsdk-pre-init-library 字符串未正确修改\e[0m"
      end
      
      # 检查 overrideMonoSearchPath 禁用(仅在使用 Mono 时检查)
      if uses_il2cpp
        puts "  \e[36m信息: 使用 IL2CPP,跳过 overrideMonoSearchPath 检测\e[0m"
        override_disabled = true  # IL2CPP 不需要检查 overrideMonoSearchPath
      else
        override_result = check_override_patch(so_file)
        if override_result[:found]
          override_found = true
          if override_result[:disabled]
            override_disabled = true
            puts "  \e[32m✓ overrideMonoSearchPath 已正确禁用\e[0m"
          else
            puts "  \e[31m✗ overrideMonoSearchPath 未正确禁用\e[0m"
          end
        else
          puts "  \e[36m信息: 未找到 overrideMonoSearchPath 字符串\e[0m"
        end
      end
    end
  end
  
  # 如果没有找到 overrideMonoSearchPath 或使用 IL2CPP,认为是正常的
  if !override_found || uses_il2cpp
    override_disabled = true
  end
  
  xrsdk_changed && override_disabled
end

.check_override_patch(so_file) ⇒ Object

检查 overrideMonoSearchPath 禁用



810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 810

def self.check_override_patch(so_file)
  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"
  end
  
  result
end

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

检测 Target SDK 版本合规性



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 166

def self.check_target_sdk_compliance(temp_dir, result, aab_path = nil)
  Funlog.fancyinfo_update("检测 Target SDK 版本...")
  
  target_sdk = 0
  
  # 方法1: 使用 bundletool dump manifest(首要方法)
  if aab_path && File.exist?(aab_path)
    # puts "使用 bundletool 解析 AAB 文件: #{File.basename(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
    if target_sdk == 0
      result.add_issue("无法检测到 Target SDK 版本,请检查 AAB 文件结构")
      Funlog.error("无法检测到 Target SDK 版本")
    else
      result.add_issue("Target SDK #{target_sdk} 不符合要求,需要至少 Target SDK 35 (Android 15)")
      Funlog.warning("Target SDK: #{target_sdk} (需要至少 35)")
    end
  end
end

.check_unity_patch_compliance(temp_dir, result) ⇒ Object

检测 Unity 漏洞合规性



631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 631

def self.check_unity_patch_compliance(temp_dir, result)
  puts "\n\e[1m--- Unity 漏洞检测 ---\e[0m"
  
  # 检查是否存在 Unity 相关文件
  unity_files = find_unity_files(temp_dir)
  
  if unity_files.empty?
    puts "\e[36m信息: 未检测到 Unity 相关文件,跳过 Unity 漏洞检测\e[0m"
    result.unity_patch_compliant = true
    return
  end
  
  puts "检测到 Unity 项目,开始检查漏洞修复..."
  
  # 检查 libunity.so 文件
  libunity_result = check_libunity_patch(temp_dir)
  
  # 检查 boot.config 文件
  boot_config_result = check_boot_config_patch(temp_dir)
  
  # 综合判断
  if libunity_result && boot_config_result
    result.unity_patch_compliant = true
    result.unity_xrsdk_patched = true
    result.unity_override_patched = true
    puts "\e[32m✓ Unity 漏洞检测: 通过\e[0m"
  else
    result.unity_patch_compliant = false
    if !libunity_result
      result.add_issue("Unity libunity.so 漏洞未修复")
    end
    if !boot_config_result
      result.add_issue("Unity boot.config 漏洞未修复")
    end
    puts "\e[31m✗ Unity 漏洞检测: 未通过\e[0m"
  end
end

.check_xrsdk_patch(so_file) ⇒ Object

检查 xrsdk 字符串修补



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
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 783

def self.check_xrsdk_patch(so_file)
  # 检查是否还存在原始的 xrsdk 字符串
  has_original = false
  has_modified = false
  
  begin
    # 使用 strings 命令检查文件内容
    strings_output = `strings "#{so_file}" 2>/dev/null`
    
    if strings_output.include?("xrsdk-pre-init-library")
      has_original = true
    end
    
    if strings_output.include?("8rsdk-pre-init-library")
      has_modified = true
    end
    
    # 如果存在修改后的字符串且不存在原始字符串,说明修补成功
    has_modified && !has_original
    
  rescue => e
    puts "  \e[33m警告: 检查 #{so_file} 时出错: #{e.message}\e[0m"
    false
  end
end

.determine_architecture(so_file) ⇒ Object

确定共享库的架构



584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 584

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_target_sdk_from_manifest(manifest_path) ⇒ Object

从 AndroidManifest.xml 中提取 Target SDK 版本



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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
401
402
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 306

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})/
        # puts "从二进制 XML 中找到 targetSdkVersion: #{$1}"
        return $1.to_i
      end
      
      # 尝试更宽泛的搜索模式
      if content =~ /targetSdkVersion.*?(\d{1,2})/
        # puts "从二进制 XML 中找到 targetSdkVersion (宽泛模式): #{$1}"
        return $1.to_i
      end
      
      # puts "二进制 XML 中未找到 targetSdkVersion"
      return 0
    end
    
    # 尝试使用 aapt 工具解析(适用于文本格式)
    if tool_available?('aapt')
      aapt_output = `aapt dump badging "#{manifest_path}" 2>/dev/null`
      if aapt_output && !aapt_output.empty?
        if aapt_output =~ /targetSdkVersion:'(\d+)'/
          # puts "从 aapt 中找到 targetSdkVersion: #{$1}"
          return $1.to_i
        end
      end
    end
    
    # 尝试使用 aapt2 工具
    if tool_available?('aapt2')
      aapt2_output = `aapt2 dump badging "#{manifest_path}" 2>/dev/null`
      if aapt2_output && !aapt2_output.empty?
        if aapt2_output =~ /targetSdkVersion:'(\d+)'/
          # puts "从 aapt2 中找到 targetSdkVersion: #{$1}"
          return $1.to_i
        end
      end
    end
    
    # 尝试使用 aapt dump xmltree
    if tool_available?('aapt')
      aapt_xml_output = `aapt dump xmltree "#{manifest_path}" 2>/dev/null`
      if aapt_xml_output && !aapt_xml_output.empty?
        if aapt_xml_output =~ /targetSdkVersion.*?(\d+)/
          # puts "从 aapt xmltree 中找到 targetSdkVersion: #{$1}"
          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
        # puts "从 XML 中找到 targetSdkVersion: #{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
      # puts "从 XML 属性中找到 targetSdkVersion: #{target_sdk}"
      return target_sdk.to_i
    end
    
    # 尝试查找所有包含 targetSdkVersion 的属性
    all_attrs = doc.xpath('//@*[contains(name(), "targetSdkVersion")]')
    if !all_attrs.empty?
      target_sdk = all_attrs.first.value
      # puts "从 XML 中找到 targetSdkVersion 属性: #{target_sdk}"
      return target_sdk.to_i
    end
    
    # puts "未找到 targetSdkVersion,返回默认值 0"
    return 0
    
  rescue => e
    return 0
  end
end

.extract_target_sdk_with_bundletool(aab_path) ⇒ Object

使用 bundletool 提取 Target SDK 版本



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 203

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_cmd = "#{java_cmd} -jar \"#{bundletool_jar}\" dump manifest --bundle=\"#{aab_path}\" | grep \"targetSdkVersion\""
    output = `#{bundletool_cmd} 2>/dev/null`
    
    if output && !output.empty?
      # puts "bundletool 输出: #{output.strip}"
      
      # 解析输出格式: <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
      if output =~ /android:targetSdkVersion="(\d+)"/
        target_sdk = $1.to_i
        # puts "从 bundletool 中找到 targetSdkVersion: #{target_sdk}"
        return target_sdk
      end
    end
    
    # puts "bundletool 未找到 targetSdkVersion"
    return 0
  rescue => e
    return 0
  end
end

.extract_target_sdk_with_system_bundletool(aab_path) ⇒ Object

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



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
262
263
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 236

def self.extract_target_sdk_with_system_bundletool(aab_path)
  begin
    # 检查系统 bundletool 是否可用
    unless tool_available?('bundletool')
      return 0
    end
    
    # 使用系统 bundletool 命令
    bundletool_cmd = "bundletool dump manifest --bundle=\"#{aab_path}\" | grep \"targetSdkVersion\""
    output = `#{bundletool_cmd} 2>/dev/null`
    
    if output && !output.empty?
      # puts "bundletool 输出: #{output.strip}"
      
      # 解析输出格式: <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
      if output =~ /android:targetSdkVersion="(\d+)"/
        target_sdk = $1.to_i
        # puts "从系统 bundletool 中找到 targetSdkVersion: #{target_sdk}"
        return target_sdk
      end
    end
    
    # puts "系统 bundletool 未找到 targetSdkVersion"
    return 0
  rescue => e
    return 0
  end
end

.find_android_manifest(temp_dir) ⇒ Object

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



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 286

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

查找共享库文件



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 451

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 目录
      if system("unzip", "-q", base_apk_path, "lib/*", "-d", base_temp_dir)
        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_unity_files(temp_dir) ⇒ Object

查找 Unity 相关文件



670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 670

def self.find_unity_files(temp_dir)
  unity_files = []
  
  # 查找 libunity.so 文件
  lib_dirs = [
    "#{temp_dir}/lib", 
    "#{temp_dir}/libs",
    "#{temp_dir}/base/lib",  # AAB文件结构
    "#{temp_dir}/base/libs"  # AAB文件结构
  ]
  lib_dirs.each do |lib_dir|
    if Dir.exist?(lib_dir)
      found_so_files = Dir.glob("#{lib_dir}/**/libunity.so")
      unity_files += found_so_files
    end
  end
  
  # 查找 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 命令



281
282
283
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 281

def self.get_java_command_for_bundletool
  Pindo::JavaEnvHelper.find_java_command
end

.get_pindo_bundletool_pathObject

获取 Pindo 自带的 bundletool.jar 路径



266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 266

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

打印合规检测摘要



602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 602

def self.print_compliance_summary(result)
  Funlog.fancyinfo_update("生成合规检测摘要...")
  
  # 总体合规状态
  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

.tool_available?(tool_name) ⇒ Boolean

检查工具是否可用

Returns:

  • (Boolean)


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 103

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)


705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
# File 'lib/pindo/module/android/gp_compliance_helper.rb', line 705

def self.uses_il2cpp?(temp_dir)
  # 检查是否存在 libil2cpp.so 文件
  lib_dirs = [
    "#{temp_dir}/lib", 
    "#{temp_dir}/libs",
    "#{temp_dir}/base/lib",  # AAB文件结构
    "#{temp_dir}/base/libs"  # AAB文件结构
  ]
  
  lib_dirs.each do |lib_dir|
    if Dir.exist?(lib_dir)
      il2cpp_files = Dir.glob("#{lib_dir}/**/libil2cpp.so")
      return true if il2cpp_files.any?
    end
  end
  
  false
end