diff --git a/pom.xml b/pom.xml index f2ce1adf..33e9097b 100644 --- a/pom.xml +++ b/pom.xml @@ -145,12 +145,42 @@ 2.11.5 + + org.zeroturnaround + zt-zip + 1.18.2 + + + + com.android.tools + sdk-common + 32.3.1 + + com.android.tools.build bundletool 1.18.3 + + com.google.protobuf + protobuf-java + 3.21.12 + + + + com.android.tools.build + aapt2-proto + 9.3.1-15703166 + + + + com.android.tools.apkparser + apkanalyzer + 32.3.1 + + com.android.tools.apkparser binary-resources diff --git a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java index f72314f8..aae82cca 100644 --- a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java +++ b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java @@ -1,11 +1,9 @@ package com.youlai.boot.app.packages.component; import com.android.tools.build.bundletool.commands.DumpCommand; -import lombok.Data; import net.dongliu.apk.parser.ApkFile; import net.dongliu.apk.parser.bean.ApkMeta; import net.dongliu.apk.parser.bean.Icon; -import org.springframework.stereotype.Component; import java.io.ByteArrayOutputStream; import java.io.File; @@ -39,31 +37,12 @@ import java.util.zip.ZipInputStream; * 解包后定位主 APK 复用 APK 解析逻辑,再补充 JSON 描述信息 * */ -@Component public class ApkMetaParser { - @Data - public static class ApkMetaResult { - private String packageName; - private String appLabel; - private String versionName; - private Integer versionCode; - private Integer minSdk; - private Integer targetSdk; - private Integer compileSdk; - private Integer maxSdk; - private List abiList = new ArrayList<>(); - private List permissions = new ArrayList<>(); - private String iconPath; - private byte[] iconBytes; - /** 容器格式(apks/apkm/xapk)内解析出的 split apk 列表,便于审计 */ - private List splitApkList = new ArrayList<>(); - } - /** * 统一解析入口:根据文件扩展名分发到对应解析策略。 */ - public ApkMetaResult parse(File packageFile) throws IOException { + public static ApkMetaResult parse(File packageFile) throws IOException { String name = packageFile.getName().toLowerCase(Locale.ROOT); int dot = name.lastIndexOf('.'); String ext = dot >= 0 ? name.substring(dot + 1) : ""; @@ -82,7 +61,7 @@ public class ApkMetaParser { /** * 解析 APK 单包(apk-parser 解析二进制 AndroidManifest.xml)。 */ - public ApkMetaResult parseApk(File apkFile) throws IOException { + public static ApkMetaResult parseApk(File apkFile) throws IOException { ApkMetaResult result = new ApkMetaResult(); try (ApkFile apkFileParser = new ApkFile(apkFile)) { apkFileParser.setPreferredLocale(Locale.SIMPLIFIED_CHINESE); @@ -103,7 +82,7 @@ public class ApkMetaParser { * 直接使用 bundletool 依赖(非命令行、非 jar)的 {@link DumpCommand} 输出 * base 模块的 AndroidManifest.xml 文本,再提取关键字段。 */ - public ApkMetaResult parseAab(File aabFile) throws IOException { + public static ApkMetaResult parseAab(File aabFile) throws IOException { ApkMetaResult result = parseByBundleTool(aabFile); // 从 AAB 的 base 模块资源目录(res/mipmap-*、res/drawable-*)提取应用图标 fillBundleIcon(result, aabFile, true); @@ -118,7 +97,7 @@ public class ApkMetaParser { * bundletool 依赖可直接解析 .apks 整包并 dump 其 Manifest;同时枚举 ZIP 内 * 的 apk 条目记录 split 列表。 */ - public ApkMetaResult parseApks(File apksFile) throws IOException { + public static ApkMetaResult parseApks(File apksFile) throws IOException { ApkMetaResult result = parseByBundleTool(apksFile); result.setSplitApkList(collectApkEntries(apksFile)); // 图标位于内部各 split apk 的 res/mipmap-* 或 res/drawable-*,逐包扫描 @@ -139,7 +118,7 @@ public class ApkMetaParser { *
  • 补充解析 JSON 描述文件
  • * */ - public ApkMetaResult parseContainer(File containerFile) throws IOException { + public static ApkMetaResult parseContainer(File containerFile) throws IOException { List apks = new ArrayList<>(); byte[] manifestJson = null; byte[] infoJson = null; @@ -218,7 +197,7 @@ public class ApkMetaParser { *
  • APKM(APKMirror):info.json 描述的 base.apk
  • * */ - private ContainerEntry locateMainApk(List apks) { + private static ContainerEntry locateMainApk(List apks) { // 优先 base-master.apk / base.apk for (String candidate : new String[]{"base-master.apk", "base.apk"}) { for (ContainerEntry e : apks) { @@ -246,7 +225,8 @@ public class ApkMetaParser { * AndroidManifest.xml(文本形式)写入内存,再由 {@link #parseManifestXmlText} 提取字段。 * 支持 .aab 与 .apks 两种 Google 标准 bundle。 */ - private ApkMetaResult parseByBundleTool(File bundleFile) throws IOException { + @Deprecated + private static ApkMetaResult parseByBundleTool(File bundleFile) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (PrintStream ps = new PrintStream(baos, true, StandardCharsets.UTF_8)) { DumpCommand.builder() @@ -263,7 +243,7 @@ public class ApkMetaParser { /** * 枚举 zip 内所有 .apk 条目名(用于 APKS 记录 split 列表)。 */ - private List collectApkEntries(File zipFile) { + private static List collectApkEntries(File zipFile) { List apkNames = new ArrayList<>(); try (ZipFile zf = new ZipFile(zipFile)) { Enumeration entries = zf.entries(); @@ -288,7 +268,8 @@ public class ApkMetaParser { * * @param isAab true 表示 AAB(路径前缀 base/res/),false 表示 APKS(需解内嵌 apk) */ - private void fillBundleIcon(ApkMetaResult result, File bundleFile, boolean isAab) { + @Deprecated + private static void fillBundleIcon(ApkMetaResult result, File bundleFile, boolean isAab) { if (result.getIconBytes() != null && result.getIconBytes().length > 0) { return; } @@ -324,7 +305,7 @@ public class ApkMetaParser { /** * 用 ZipFile 随机访问扫描:先记录最佳条目名,再按名读取字节。 */ - private IconCandidate scanBestIcon(ZipFile zf, Predicate prefixFilter) { + private static IconCandidate scanBestIcon(ZipFile zf, Predicate prefixFilter) { String bestName = null; int bestRank = -1; Enumeration entries = zf.entries(); @@ -345,7 +326,7 @@ public class ApkMetaParser { /** * 用 ZipInputStream 流式扫描(用于嵌套 apk),同时读入候选图标字节。 */ - private IconCandidate scanBestIconFromStream(ZipInputStream zis, Predicate prefixFilter) { + private static IconCandidate scanBestIconFromStream(ZipInputStream zis, Predicate prefixFilter) { IconCandidate best = null; try { ZipEntry entry; @@ -364,7 +345,7 @@ public class ApkMetaParser { return best; } - private void applyIcon(ApkMetaResult result, ZipFile zf, IconCandidate candidate) { + private static void applyIcon(ApkMetaResult result, ZipFile zf, IconCandidate candidate) { if (candidate == null) { return; } @@ -388,7 +369,7 @@ public class ApkMetaParser { *
  • 跳过 anydpi / nodpi / *_round 等不适合直接作为图标字节的条目
  • * */ - private int iconRank(String path) { + private static int iconRank(String path) { String lower = path.toLowerCase(Locale.ROOT); if (!lower.endsWith(".png") && !lower.endsWith(".webp")) { return -1; @@ -423,7 +404,7 @@ public class ApkMetaParser { /** * 从字节数组解析 APK(用于容器内的主 APK)。 */ - private ApkMetaResult tryParseApkBytes(byte[] apkBytes) { + private static ApkMetaResult tryParseApkBytes(byte[] apkBytes) { // ApkFile 仅支持 File / String 路径,故将容器内主 APK 字节落盘为临时文件解析 File tmp = null; try { @@ -445,7 +426,7 @@ public class ApkMetaParser { } } - private void fillFromApkMeta(ApkMetaResult result, ApkMeta meta) { + private static void fillFromApkMeta(ApkMetaResult result, ApkMeta meta) { result.setPackageName(meta.getPackageName()); result.setAppLabel(meta.getLabel()); result.setVersionName(meta.getVersionName()); @@ -453,14 +434,13 @@ public class ApkMetaParser { result.setMinSdk(toInt(meta.getMinSdkVersion())); result.setTargetSdk(toInt(meta.getTargetSdkVersion())); result.setCompileSdk(toInt(meta.getCompileSdkVersion())); - result.setMaxSdk(toInt(meta.getMaxSdkVersion())); if (meta.getUsesPermissions() != null) { result.setPermissions(new ArrayList<>(meta.getUsesPermissions())); } } - private void fillIcon(ApkMetaResult result, ApkFile apkFileParser) { + private static void fillIcon(ApkMetaResult result, ApkFile apkFileParser) { try { List icons = apkFileParser.getIconFiles(); if (icons != null && !icons.isEmpty()) { @@ -479,7 +459,7 @@ public class ApkMetaParser { /** * 从 bundletool 输出的 AndroidManifest.xml 文本中提取关键字段。 */ - private ApkMetaResult parseManifestXmlText(String manifestXml) { + private static ApkMetaResult parseManifestXmlText(String manifestXml) { ApkMetaResult result = new ApkMetaResult(); // 包名: result.setPackageName(attr(manifestXml, "package")); @@ -492,11 +472,10 @@ public class ApkMetaParser { result.setMinSdk(toInt(attr(manifestXml, "android:minSdkVersion"))); result.setTargetSdk(toInt(attr(manifestXml, "android:targetSdkVersion"))); result.setCompileSdk(toInt(attr(manifestXml, "android:compileSdkVersion"))); - result.setMaxSdk(toInt(attr(manifestXml, "android:maxSdkVersion"))); return result; } - private String attr(String xml, String name) { + private static String attr(String xml, String name) { // 兼容 package="x" 与 android:label="x" 两种写法 String pattern = name + "=\""; int idx = xml.indexOf(pattern); @@ -515,7 +494,7 @@ public class ApkMetaParser { * 从容器内的 JSON 描述文件补充信息(XAPK 的 manifest.json / APKM 的 info.json)。 * 仅做轻量提取:包名 / 版本名 / 版本号缺失时回填。 */ - private void supplementFromJson(ApkMetaResult result, byte[] jsonBytes) { + private static void supplementFromJson(ApkMetaResult result, byte[] jsonBytes) { String json = new String(jsonBytes, StandardCharsets.UTF_8); if (result.getPackageName() == null || result.getPackageName().isBlank()) { String pkg = jsonStringField(json, "package"); @@ -543,7 +522,7 @@ public class ApkMetaParser { } } - private String jsonStringField(String json, String key) { + private static String jsonStringField(String json, String key) { // 简单匹配 "key" : "value" 或 "key":"value" String pattern = "\"" + key + "\"\\s*:\\s*\""; int idx = json.indexOf(pattern); @@ -558,7 +537,7 @@ public class ApkMetaParser { return json.substring(start, end); } - private Integer jsonIntField(String json, String key) { + private static Integer jsonIntField(String json, String key) { String pattern = "\"" + key + "\"\\s*:\\s*"; int idx = json.indexOf(pattern); if (idx < 0) { @@ -582,7 +561,7 @@ public class ApkMetaParser { /** * 扫描 apk 内 lib// 目录,提取支持的 CPU 架构列表。 */ - private List extractAbiList(File apkFile) { + private static List extractAbiList(File apkFile) { Set abis = new LinkedHashSet<>(); try (ZipFile zf = new ZipFile(apkFile)) { Enumeration entries = zf.entries(); @@ -601,7 +580,7 @@ public class ApkMetaParser { return new ArrayList<>(abis); } - private Integer toInt(Object value) { + private static Integer toInt(Object value) { if (value == null) { return null; } diff --git a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaResult.java b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaResult.java new file mode 100644 index 00000000..b8166587 --- /dev/null +++ b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaResult.java @@ -0,0 +1,26 @@ +package com.youlai.boot.app.packages.component; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * Android 安装包解析结果。 + */ +@Data +public class ApkMetaResult { + private String packageName; + private String appLabel; + private String versionName; + private Integer versionCode; + private Integer minSdk; + private Integer targetSdk; + private Integer compileSdk; + private List abiList = new ArrayList<>(); + private List permissions = new ArrayList<>(); + private String iconPath; + private byte[] iconBytes; + /** 容器格式(apks/apkm/xapk)内解析出的 split apk 列表,便于审计 */ + private List splitApkList = new ArrayList<>(); +} diff --git a/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java b/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java index 19c1f9cc..1d76e03b 100644 --- a/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java +++ b/src/main/java/com/youlai/boot/app/packages/service/impl/AppPackageServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.digest.DigestUtil; import com.youlai.boot.app.packages.component.ApkMetaParser; +import com.youlai.boot.app.packages.component.ApkMetaResult; import com.youlai.boot.app.packages.model.document.AppPackageDocument; import com.youlai.boot.app.packages.model.query.AppPackagePageQuery; import com.youlai.boot.app.packages.model.vo.AppPackageVO; @@ -34,7 +35,6 @@ import java.util.List; public class AppPackageServiceImpl implements AppPackageService { private final AppPackageRepository appPackageRepository; - private final ApkMetaParser apkMetaParser; private static final List ALLOWED_EXT = List.of("apk", "apks", "aab", "apkm", "xapk"); @@ -84,7 +84,7 @@ public class AppPackageServiceImpl implements AppPackageService { if ("apk".equalsIgnoreCase(ext)) { try { - ApkMetaParser.ApkMetaResult meta = apkMetaParser.parse(storedFile); + ApkMetaResult meta = ApkMetaParser.parse(storedFile); builder.packageName(firstNonBlank(meta.getPackageName(), packageName)) .appLabel(meta.getAppLabel()) .versionName(firstNonBlank(meta.getVersionName(), versionName)) @@ -114,7 +114,7 @@ public class AppPackageServiceImpl implements AppPackageService { } else { // aab / xapk / apks / apkm:统一交由解析器解包内部 APK try { - ApkMetaParser.ApkMetaResult meta = apkMetaParser.parse(storedFile); + ApkMetaResult meta = ApkMetaParser.parse(storedFile); builder.packageName(firstNonBlank(meta.getPackageName(), packageName)) .appLabel(meta.getAppLabel()) .versionName(firstNonBlank(meta.getVersionName(), versionName)) diff --git a/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java b/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java index 099d3fe8..f921292f 100644 --- a/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java +++ b/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java @@ -1,11 +1,11 @@ package com.youlai.boot.app.packages.component; import com.youlai.boot.common.util.FileUtils; +import com.youlai.boot.app.packages.component.ApkMetaResult; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.io.File; -import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -56,7 +56,7 @@ class ApkMetaParserTest { @Test @DisplayName("批量解析本地 apk 文件并打印元数据") - void parseLocalApks() throws IOException { + void parseLocalApks() { List paths = resolveApkPaths(); if (paths.isEmpty()) { System.out.println("===== 跳过测试 ====="); @@ -67,7 +67,6 @@ class ApkMetaParserTest { return; } - ApkMetaParser parser = new ApkMetaParser(); int total = paths.size(); int success = 0; @@ -85,7 +84,7 @@ class ApkMetaParserTest { System.out.println("文件魔数为: " + FileUtils.getMagicBytes(apkFile.getAbsolutePath(), 0, 4)); System.out.println("文件类型为: " + new AndroidPackageTypeDetector().detect(apkFile.toPath())); - ApkMetaParser.ApkMetaResult result = parser.parse(apkFile); + ApkMetaResult result = ApkMetaParser.parse(apkFile); assertNotNull(result, "解析结果不应为空"); assertNotNull(result.getPackageName(), "包名不应为空"); @@ -97,7 +96,6 @@ class ApkMetaParserTest { System.out.println("minSdk : " + result.getMinSdk()); System.out.println("targetSdk : " + result.getTargetSdk()); System.out.println("compileSdk : " + result.getCompileSdk()); - System.out.println("maxSdk : " + result.getMaxSdk()); System.out.println("abiList : " + result.getAbiList()); System.out.println("permissions : " + result.getPermissions()); System.out.println("iconPath : " + result.getIconPath());