feat(packages): 支持AAB/APKS/APKM/XAPK格式安装包解析
新增AndroidPackageTypeDetector用于识别安装包类型,扩展ApkMetaParser以支持AAB、APKS、APKM、XAPK等容器格式的元数据解析,并在上传服务中集成解析逻辑,完善应用包信息提取能力。
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package com.youlai.boot.app.packages.component;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class AndroidPackageTypeDetector {
|
||||
|
||||
// 第一层:判 ZIP 魔数
|
||||
public enum PkgType { APK, AAB, APKS, APKM, XAPK, UNKNOWN }
|
||||
|
||||
public PkgType detect(Path file) throws IOException {
|
||||
// 1. 读前 4 字节判 ZIP
|
||||
byte[] header = new byte[4];
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
int read = in.read(header);
|
||||
if (read < 4) return PkgType.UNKNOWN;
|
||||
}
|
||||
// ZIP 魔数:50 4B 03 04
|
||||
if (!(header[0] == 0x50 && header[1] == 0x4B
|
||||
&& header[2] == 0x03 && header[3] == 0x04)) {
|
||||
return PkgType.UNKNOWN; // 不是 ZIP,直接淘汰
|
||||
}
|
||||
|
||||
// 2. 枚举 ZIP 条目,收集条目名(用 ZipFile 避免 ZipInputStream 对
|
||||
// STORED 条目携带 EXT descriptor 标志时的严格校验异常)
|
||||
Set<String> entries = new HashSet<>();
|
||||
try (ZipFile zf = new ZipFile(file.toFile())) {
|
||||
Enumeration<? extends ZipEntry> en = zf.entries();
|
||||
while (en.hasMoreElements()) {
|
||||
entries.add(en.nextElement().getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 按特征条目判定(注意顺序:先判容器,再判单包)
|
||||
boolean hasTocPb = entries.contains("toc.pb");
|
||||
boolean hasInfoJson = entries.contains("info.json");
|
||||
boolean hasManifestJson = entries.contains("manifest.json");
|
||||
boolean hasBaseDir = entries.stream().anyMatch(n -> n.startsWith("base/"));
|
||||
boolean hasAndroidManifest = entries.contains("AndroidManifest.xml");
|
||||
boolean hasClassesDex = entries.stream().anyMatch(n -> n.equals("classes.dex")
|
||||
|| n.startsWith("classes"));
|
||||
boolean hasBundleMetadata = entries.stream()
|
||||
.anyMatch(n -> n.startsWith("BUNDLE-METADATA/"));
|
||||
boolean hasObIn = entries.stream().anyMatch(n -> n.startsWith("Android/obb/"));
|
||||
|
||||
// 4. 决策树
|
||||
if (hasTocPb) {
|
||||
return PkgType.APKS; // toc.pb 是 APKS 独有
|
||||
}
|
||||
if (hasInfoJson && !hasManifestJson) {
|
||||
return PkgType.APKM; // info.json 且无 manifest.json → APKM
|
||||
}
|
||||
if (hasManifestJson) {
|
||||
return PkgType.XAPK; // manifest.json → XAPK
|
||||
}
|
||||
if (hasBaseDir && hasBundleMetadata && !hasClassesDex) {
|
||||
return PkgType.AAB; // 模块化 + BUNDLE-METADATA → AAB
|
||||
}
|
||||
if (hasAndroidManifest && hasClassesDex) {
|
||||
return PkgType.APK; // 根级 AndroidManifest + classes.dex → APK
|
||||
}
|
||||
return PkgType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,29 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
* APK 安装包元数据解析器(基于 apk-parser)
|
||||
* Android 安装包元数据解析器(基于 apk-parser + zip4j)
|
||||
* <p>
|
||||
* 用于从 apk 文件中尽可能详细地读取包名、版本、SDK 版本、权限、CPU 架构、图标等信息。
|
||||
* aab / xapk / apks / apkm 为其他格式,本解析器不适用,由上传流程按文件基础信息处理。
|
||||
* 支持五种格式:
|
||||
* <ul>
|
||||
* <li>APK:单包,直接用 apk-parser 解析(最成熟)</li>
|
||||
* <li>AAB:ZIP 容器,内部为 base/manifest/AndroidManifest.xml 等模块结构,
|
||||
* 解包后用 apk-parser 解析二进制 Manifest(兜底:bundletool 命令行)</li>
|
||||
* <li>APKS/APKM/XAPK:ZIP 容器,内部包含一个或多个 APK 及描述文件,
|
||||
* 解包后定位主 APK,复用 APK 解析逻辑,再补充 JSON 描述信息</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component
|
||||
public class ApkMetaParser {
|
||||
@@ -38,54 +49,374 @@ public class ApkMetaParser {
|
||||
private List<String> permissions = new ArrayList<>();
|
||||
private String iconPath;
|
||||
private byte[] iconBytes;
|
||||
/** 容器格式(apks/apkm/xapk)内解析出的 split apk 列表,便于审计 */
|
||||
private List<String> splitApkList = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 apk 文件元数据
|
||||
* 统一解析入口:根据文件扩展名分发到对应解析策略。
|
||||
*/
|
||||
public ApkMetaResult parse(File apkFile) throws IOException {
|
||||
public 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) : "";
|
||||
|
||||
return switch (ext) {
|
||||
case "apk" -> parseApk(packageFile);
|
||||
case "aab" -> parseAab(packageFile);
|
||||
case "apks", "apkm", "xapk" -> parseContainer(packageFile);
|
||||
default -> throw new IllegalArgumentException("不支持的安装包格式: ." + ext);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================ APK ============================
|
||||
|
||||
/**
|
||||
* 解析 APK 单包(apk-parser 解析二进制 AndroidManifest.xml)。
|
||||
*/
|
||||
public ApkMetaResult parseApk(File apkFile) throws IOException {
|
||||
ApkMetaResult result = new ApkMetaResult();
|
||||
try (ApkFile apkFileParser = new ApkFile(apkFile)) {
|
||||
apkFileParser.setPreferredLocale(Locale.SIMPLIFIED_CHINESE);
|
||||
ApkMeta meta = apkFileParser.getApkMeta();
|
||||
|
||||
result.setPackageName(meta.getPackageName());
|
||||
result.setAppLabel(meta.getLabel());
|
||||
result.setVersionName(meta.getVersionName());
|
||||
result.setVersionCode(toInt(meta.getVersionCode()));
|
||||
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()));
|
||||
}
|
||||
|
||||
// CPU 架构:扫描 lib/<abi>/ 目录
|
||||
fillFromApkMeta(result, meta);
|
||||
result.setAbiList(extractAbiList(apkFile));
|
||||
|
||||
// 图标:优先 launcher 图标,取首个图标的字节
|
||||
try {
|
||||
List<Icon> icons = apkFileParser.getIconFiles();
|
||||
if (icons != null && !icons.isEmpty()) {
|
||||
Icon icon = icons.get(0);
|
||||
result.setIconPath(icon.getPath());
|
||||
byte[] data = icon.getData();
|
||||
if (data != null && data.length > 0) {
|
||||
result.setIconBytes(data);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 图标解析失败不影响其它字段
|
||||
}
|
||||
fillIcon(result, apkFileParser);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================ AAB ============================
|
||||
|
||||
/**
|
||||
* 解析 AAB(Android App Bundle)。
|
||||
* <p>
|
||||
* 策略:AAB 本质是一个 ZIP,解包后读取 base/manifest/AndroidManifest.xml 这个二进制 XML,
|
||||
* 用 apk-parser 的 ApkFile 直接解析(2.6.x 兼容 AAB 内部文件)。
|
||||
* 若 apk-parser 解析失败,则尝试调用 bundletool 命令行 dump manifest 兜底。
|
||||
*/
|
||||
public ApkMetaResult parseAab(File aabFile) throws IOException {
|
||||
// 优先:用 apk-parser 直接读取(它能识别 AAB 内的模块结构)
|
||||
try (ApkFile apkFileParser = new ApkFile(aabFile)) {
|
||||
apkFileParser.setPreferredLocale(Locale.SIMPLIFIED_CHINESE);
|
||||
ApkMeta meta = apkFileParser.getApkMeta();
|
||||
ApkMetaResult result = new ApkMetaResult();
|
||||
fillFromApkMeta(result, meta);
|
||||
// AAB 本身不含 lib/<abi> 与图标资源(图标在运行时生成),此处跳过
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
// 兜底:bundletool 命令行解析
|
||||
return parseAabByBundleTool(aabFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底方案:调用 Google 官方 bundletool 输出 AndroidManifest.xml 文本并提取关键字段。
|
||||
* 需要环境中存在 bundletool.jar(可通过 -Dbundletool.jar 指定路径)。
|
||||
*/
|
||||
private ApkMetaResult parseAabByBundleTool(File aabFile) throws IOException {
|
||||
String jarPath = System.getProperty("bundletool.jar");
|
||||
if (jarPath == null || jarPath.isBlank()) {
|
||||
throw new IOException("apk-parser 解析 AAB 失败,且未配置 bundletool.jar 兜底");
|
||||
}
|
||||
try {
|
||||
Process process = new ProcessBuilder(
|
||||
"java", "-jar", jarPath, "dump", "manifest",
|
||||
"--bundle=" + aabFile.getAbsolutePath()
|
||||
).redirectErrorStream(true).start();
|
||||
|
||||
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
int exit = process.waitFor();
|
||||
if (exit != 0) {
|
||||
throw new IOException("bundletool 执行失败: " + output);
|
||||
}
|
||||
return parseManifestXmlText(output);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("bundletool 执行被中断", ie);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ APKS / APKM / XAPK ============================
|
||||
|
||||
/**
|
||||
* 解析 ZIP 容器格式(APKS / APKM / XAPK)。
|
||||
* <p>
|
||||
* 通用流程:
|
||||
* 1. 用 zip4j / java.util.zip 解包,收集所有 .apk 及描述文件(manifest.json / info.json)
|
||||
* 2. 定位主 APK(base-master.apk / base.apk / 第一个能解析出 packageName 的)
|
||||
* 3. 复用 APK 解析逻辑
|
||||
* 4. 补充解析 JSON 描述文件(obb / split 列表等)
|
||||
*/
|
||||
public ApkMetaResult parseContainer(File containerFile) throws IOException {
|
||||
List<ContainerEntry> apks = new ArrayList<>();
|
||||
byte[] manifestJson = null;
|
||||
byte[] infoJson = null;
|
||||
|
||||
try (ZipFile zf = new ZipFile(containerFile)) {
|
||||
Enumeration<? extends ZipEntry> entries = zf.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
if (entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
String entryName = entry.getName();
|
||||
if (entryName.toLowerCase(Locale.ROOT).endsWith(".apk")) {
|
||||
try (InputStream in = zf.getInputStream(entry)) {
|
||||
apks.add(new ContainerEntry(entryName, in.readAllBytes()));
|
||||
}
|
||||
} else if (entryName.equalsIgnoreCase("manifest.json")) {
|
||||
try (InputStream in = zf.getInputStream(entry)) {
|
||||
manifestJson = in.readAllBytes();
|
||||
}
|
||||
} else if (entryName.equalsIgnoreCase("info.json")) {
|
||||
try (InputStream in = zf.getInputStream(entry)) {
|
||||
infoJson = in.readAllBytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (apks.isEmpty()) {
|
||||
throw new IOException("容器内未找到任何 .apk 文件: " + containerFile.getName());
|
||||
}
|
||||
|
||||
// 1) 按经验法则优先定位主 APK
|
||||
ContainerEntry mainApk = locateMainApk(apks);
|
||||
|
||||
// 2) 若经验法则未命中,兜底:逐个尝试解析,第一个拿到 packageName 的即为主包
|
||||
ApkMetaResult result = null;
|
||||
if (mainApk != null) {
|
||||
result = tryParseApkBytes(mainApk.data);
|
||||
}
|
||||
if (result == null) {
|
||||
for (ContainerEntry entry : apks) {
|
||||
ApkMetaResult r = tryParseApkBytes(entry.data);
|
||||
if (r != null && r.getPackageName() != null && !r.getPackageName().isBlank()) {
|
||||
result = r;
|
||||
mainApk = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
throw new IOException("容器内所有 apk 均无法解析出有效包名: " + containerFile.getName());
|
||||
}
|
||||
|
||||
// 3) 记录 split apk 列表
|
||||
for (ContainerEntry entry : apks) {
|
||||
result.getSplitApkList().add(entry.name);
|
||||
}
|
||||
|
||||
// 4) 补充 JSON 描述信息(如 XAPK 的 obb 文件名、APKM 的 split 列表)
|
||||
if (manifestJson != null) {
|
||||
supplementFromJson(result, manifestJson);
|
||||
}
|
||||
if (infoJson != null) {
|
||||
supplementFromJson(result, infoJson);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 经验法则定位主 APK:
|
||||
* <ul>
|
||||
* <li>APKS(Google 官方 split 集):base-master.apk 或 base.apk</li>
|
||||
* <li>XAPK(APKPure):与包名对应的主 apk(通常不含 split 关键字)</li>
|
||||
* <li>APKM(APKMirror):info.json 描述的 base.apk</li>
|
||||
* </ul>
|
||||
*/
|
||||
private ContainerEntry locateMainApk(List<ContainerEntry> apks) {
|
||||
// 优先 base-master.apk / base.apk
|
||||
for (String candidate : new String[]{"base-master.apk", "base.apk"}) {
|
||||
for (ContainerEntry e : apks) {
|
||||
if (e.name.toLowerCase(Locale.ROOT).endsWith(candidate)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 再找不含 split / config / density 等关键字的主包
|
||||
for (ContainerEntry e : apks) {
|
||||
String lower = e.name.toLowerCase(Locale.ROOT);
|
||||
if (!lower.contains("split") && !lower.contains("config") && !lower.contains("density")) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================ 通用工具 ============================
|
||||
|
||||
private record ContainerEntry(String name, byte[] data) {}
|
||||
|
||||
/**
|
||||
* 从字节数组解析 APK(用于容器内的主 APK)。
|
||||
*/
|
||||
private ApkMetaResult tryParseApkBytes(byte[] apkBytes) {
|
||||
// ApkFile 仅支持 File / String 路径,故将容器内主 APK 字节落盘为临时文件解析
|
||||
File tmp = null;
|
||||
try {
|
||||
tmp = File.createTempFile("apk-parser-", ".apk");
|
||||
Files.write(tmp.toPath(), apkBytes);
|
||||
try (ApkFile apkFileParser = new ApkFile(tmp)) {
|
||||
apkFileParser.setPreferredLocale(Locale.SIMPLIFIED_CHINESE);
|
||||
ApkMeta meta = apkFileParser.getApkMeta();
|
||||
ApkMetaResult result = new ApkMetaResult();
|
||||
fillFromApkMeta(result, meta);
|
||||
return result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
} finally {
|
||||
if (tmp != null && tmp.exists()) {
|
||||
tmp.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fillFromApkMeta(ApkMetaResult result, ApkMeta meta) {
|
||||
result.setPackageName(meta.getPackageName());
|
||||
result.setAppLabel(meta.getLabel());
|
||||
result.setVersionName(meta.getVersionName());
|
||||
result.setVersionCode(toInt(meta.getVersionCode()));
|
||||
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) {
|
||||
try {
|
||||
List<Icon> icons = apkFileParser.getIconFiles();
|
||||
if (icons != null && !icons.isEmpty()) {
|
||||
Icon icon = icons.get(0);
|
||||
result.setIconPath(icon.getPath());
|
||||
byte[] data = icon.getData();
|
||||
if (data != null && data.length > 0) {
|
||||
result.setIconBytes(data);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 图标解析失败不影响其它字段
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 bundletool 输出的 AndroidManifest.xml 文本中提取关键字段。
|
||||
*/
|
||||
private ApkMetaResult parseManifestXmlText(String manifestXml) {
|
||||
ApkMetaResult result = new ApkMetaResult();
|
||||
// 包名:<manifest ... package="xxx">
|
||||
result.setPackageName(attr(manifestXml, "package"));
|
||||
// 应用名:<application ... android:label="xxx">
|
||||
result.setAppLabel(attr(manifestXml, "android:label"));
|
||||
// 版本名 / 版本号
|
||||
result.setVersionName(attr(manifestXml, "android:versionName"));
|
||||
result.setVersionCode(toInt(attr(manifestXml, "android:versionCode")));
|
||||
// SDK 版本:<uses-sdk android:minSdkVersion=".." android:targetSdkVersion=".." android:compileSdkVersion=".."/>
|
||||
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) {
|
||||
// 兼容 package="x" 与 android:label="x" 两种写法
|
||||
String pattern = name + "=\"";
|
||||
int idx = xml.indexOf(pattern);
|
||||
if (idx < 0) {
|
||||
return null;
|
||||
}
|
||||
int start = idx + pattern.length();
|
||||
int end = xml.indexOf('"', start);
|
||||
if (end < 0) {
|
||||
return null;
|
||||
}
|
||||
return xml.substring(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从容器内的 JSON 描述文件补充信息(XAPK 的 manifest.json / APKM 的 info.json)。
|
||||
* 仅做轻量提取:包名 / 版本名 / 版本号缺失时回填。
|
||||
*/
|
||||
private 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");
|
||||
if (pkg != null) {
|
||||
result.setPackageName(pkg);
|
||||
}
|
||||
}
|
||||
if (result.getVersionName() == null || result.getVersionName().isBlank()) {
|
||||
String vn = jsonStringField(json, "versionName");
|
||||
if (vn == null) {
|
||||
vn = jsonStringField(json, "version");
|
||||
}
|
||||
if (vn != null) {
|
||||
result.setVersionName(vn);
|
||||
}
|
||||
}
|
||||
if (result.getVersionCode() == null) {
|
||||
Integer vc = jsonIntField(json, "versionCode");
|
||||
if (vc == null) {
|
||||
vc = jsonIntField(json, "version_code");
|
||||
}
|
||||
if (vc != null) {
|
||||
result.setVersionCode(vc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String jsonStringField(String json, String key) {
|
||||
// 简单匹配 "key" : "value" 或 "key":"value"
|
||||
String pattern = "\"" + key + "\"\\s*:\\s*\"";
|
||||
int idx = json.indexOf(pattern);
|
||||
if (idx < 0) {
|
||||
return null;
|
||||
}
|
||||
int start = idx + pattern.length();
|
||||
int end = json.indexOf('"', start);
|
||||
if (end < 0) {
|
||||
return null;
|
||||
}
|
||||
return json.substring(start, end);
|
||||
}
|
||||
|
||||
private Integer jsonIntField(String json, String key) {
|
||||
String pattern = "\"" + key + "\"\\s*:\\s*";
|
||||
int idx = json.indexOf(pattern);
|
||||
if (idx < 0) {
|
||||
return null;
|
||||
}
|
||||
int start = idx + pattern.length();
|
||||
int end = start;
|
||||
while (end < json.length() && (Character.isDigit(json.charAt(end)) || json.charAt(end) == '-')) {
|
||||
end++;
|
||||
}
|
||||
if (end == start) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(json.substring(start, end));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描 apk 内 lib/<abi>/ 目录,提取支持的 CPU 架构列表。
|
||||
*/
|
||||
private List<String> extractAbiList(File apkFile) {
|
||||
Set<String> abis = new LinkedHashSet<>();
|
||||
try (ZipFile zf = new ZipFile(apkFile)) {
|
||||
java.util.Enumeration<? extends ZipEntry> entries = zf.entries();
|
||||
Enumeration<? extends ZipEntry> entries = zf.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
String name = entries.nextElement().getName();
|
||||
if (name.startsWith("lib/") && name.length() > 4) {
|
||||
|
||||
@@ -112,8 +112,35 @@ public class AppPackageServiceImpl implements AppPackageService {
|
||||
fillFallback(builder, packageName, versionName, versionCode);
|
||||
}
|
||||
} else {
|
||||
// aab / xapk / apks / apkm 暂不支持二进制解析,使用表单补充信息
|
||||
fillFallback(builder, packageName, versionName, versionCode);
|
||||
// aab / xapk / apks / apkm:统一交由解析器解包内部 APK
|
||||
try {
|
||||
ApkMetaParser.ApkMetaResult meta = apkMetaParser.parse(storedFile);
|
||||
builder.packageName(firstNonBlank(meta.getPackageName(), packageName))
|
||||
.appLabel(meta.getAppLabel())
|
||||
.versionName(firstNonBlank(meta.getVersionName(), versionName))
|
||||
.versionCode(orNull(meta.getVersionCode(), versionCode))
|
||||
.minSdk(meta.getMinSdk())
|
||||
.targetSdk(meta.getTargetSdk())
|
||||
.compileSdk(meta.getCompileSdk())
|
||||
.abiList(meta.getAbiList())
|
||||
.permissions(meta.getPermissions());
|
||||
if (meta.getIconPath() != null && meta.getIconBytes() != null && meta.getIconBytes().length > 0) {
|
||||
String iconMd5 = DigestUtil.md5Hex(meta.getIconBytes());
|
||||
String iconName = iconMd5 + ".png";
|
||||
File iconDir = new File(FilePath.getApkIconPath());
|
||||
if (!iconDir.exists()) {
|
||||
iconDir.mkdirs();
|
||||
}
|
||||
File iconTarget = new File(iconDir, iconName);
|
||||
if (!iconTarget.exists()) {
|
||||
Files.write(iconTarget.toPath(), meta.getIconBytes());
|
||||
}
|
||||
builder.iconUrl("/static/app_icon/" + iconName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("{} 元数据解析失败,使用表单补充信息:{}", ext, e.getMessage());
|
||||
fillFallback(builder, packageName, versionName, versionCode);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> categoryList = (categories != null) ? categories : List.of();
|
||||
|
||||
25
src/main/java/com/youlai/boot/common/util/FileUtils.java
Normal file
25
src/main/java/com/youlai/boot/common/util/FileUtils.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.youlai.boot.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class FileUtils {
|
||||
public static String getMagicBytes(String filePath, int position, int bytes) {
|
||||
try {
|
||||
byte[] data = Files.readAllBytes(Paths.get(filePath));
|
||||
ByteBuffer bb = ByteBuffer.wrap(data);
|
||||
bb.position(position);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int i=0; i<bytes; i++) {
|
||||
sb.append(String.format("%02x", bb.get()));
|
||||
}
|
||||
return sb.toString();
|
||||
}catch(IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user