fix(xapk): new implementation to reduce zip unpack and fix NPE (#2501)
This commit is contained in:
@@ -188,6 +188,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
closeAll(customCodeLoaders);
|
||||
closeAll(customResourcesLoaders);
|
||||
closeAll(closeableList);
|
||||
FileUtils.deleteDirIfExists(args.getFilesGetter().getTempDir());
|
||||
args.close();
|
||||
FileUtils.clearTempRootDir();
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public class JadxPluginsData implements IJadxPlugins {
|
||||
return pluginManager.getResolvedPluginContexts()
|
||||
.stream()
|
||||
.filter(p -> p.getPluginInstance().getClass().equals(pluginCls))
|
||||
.map(p -> ((P) p.getPluginInstance()))
|
||||
.map(p -> (P) p.getPluginInstance())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new JadxRuntimeException("Plugin class '" + pluginCls + "' not found"));
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ public class FileUtils {
|
||||
|
||||
public static synchronized Path updateTempRootDir(Path newTempRootDir) {
|
||||
try {
|
||||
makeDirs(newTempRootDir);
|
||||
Path dir = Files.createTempDirectory(newTempRootDir, JADX_TMP_INSTANCE_PREFIX);
|
||||
tempRootDir = dir;
|
||||
dir.toFile().deleteOnExit();
|
||||
|
||||
@@ -66,6 +66,7 @@ public class JadxWrapper {
|
||||
JadxProject project = getProject();
|
||||
JadxArgs jadxArgs = getSettings().toJadxArgs();
|
||||
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
|
||||
jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE);
|
||||
project.fillJadxArgs(jadxArgs);
|
||||
JadxAppCommon.applyEnvVars(jadxArgs);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
plugins {
|
||||
id("jadx-library")
|
||||
id("jadx-kotlin")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package jadx.plugins.input.xapk;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourcesLoader;
|
||||
import jadx.api.plugins.CustomResourcesLoader;
|
||||
import jadx.api.plugins.JadxPluginContext;
|
||||
import jadx.api.plugins.input.ICodeLoader;
|
||||
import jadx.api.plugins.input.JadxCodeInput;
|
||||
import jadx.api.plugins.input.data.impl.EmptyCodeLoader;
|
||||
import jadx.plugins.input.dex.DexInputPlugin;
|
||||
import jadx.plugins.input.xapk.data.XApkData;
|
||||
|
||||
public class XApkCustomInput implements JadxCodeInput, CustomResourcesLoader {
|
||||
private final JadxPluginContext context;
|
||||
private final XApkLoader loader;
|
||||
|
||||
public XApkCustomInput(JadxPluginContext context, XApkLoader loader) {
|
||||
this.context = context;
|
||||
this.loader = loader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICodeLoader loadFiles(List<Path> input) {
|
||||
List<Path> apks = new ArrayList<>();
|
||||
for (Path inputPath : input) {
|
||||
XApkData data = loader.checkAndLoad(inputPath);
|
||||
if (data != null) {
|
||||
apks.addAll(data.getApks());
|
||||
}
|
||||
}
|
||||
if (apks.isEmpty()) {
|
||||
return EmptyCodeLoader.INSTANCE;
|
||||
}
|
||||
DexInputPlugin dexInputPlugin = context.plugins().getInstance(DexInputPlugin.class);
|
||||
return dexInputPlugin.loadFiles(apks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean load(ResourcesLoader resLoader, List<ResourceFile> list, File file) {
|
||||
XApkData xApkData = loader.checkAndLoad(file.toPath());
|
||||
if (xApkData == null) {
|
||||
return false;
|
||||
}
|
||||
for (Path apkPath : xApkData.getApks()) {
|
||||
resLoader.defaultLoadFile(list, apkPath.toFile(), apkPath.getFileName() + "/");
|
||||
}
|
||||
for (Path filePath : xApkData.getFiles()) {
|
||||
resLoader.defaultLoadFile(list, filePath.toFile(), "");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package jadx.plugins.input.xapk;
|
||||
|
||||
import jadx.api.plugins.JadxPlugin;
|
||||
import jadx.api.plugins.JadxPluginContext;
|
||||
import jadx.api.plugins.JadxPluginInfo;
|
||||
import jadx.api.plugins.JadxPluginInfoBuilder;
|
||||
|
||||
public class XApkInputPlugin implements JadxPlugin {
|
||||
|
||||
private XApkLoader loader;
|
||||
|
||||
@Override
|
||||
public JadxPluginInfo getPluginInfo() {
|
||||
return JadxPluginInfoBuilder.pluginId("xapk-input")
|
||||
.name("XApk Input")
|
||||
.description("Load .xapk files")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JadxPluginContext context) {
|
||||
loader = new XApkLoader(context);
|
||||
XApkCustomInput customInput = new XApkCustomInput(context, loader);
|
||||
context.addCodeInput(customInput);
|
||||
context.getDecompiler().addCustomResourcesLoader(customInput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
if (loader != null) {
|
||||
loader.unload();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package jadx.plugins.input.xapk;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.plugins.JadxPluginContext;
|
||||
import jadx.core.utils.GsonUtils;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.plugins.input.xapk.data.SplitApk;
|
||||
import jadx.plugins.input.xapk.data.XApkData;
|
||||
import jadx.plugins.input.xapk.data.XApkManifest;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.ZipContent;
|
||||
|
||||
public class XApkLoader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XApkLoader.class);
|
||||
|
||||
private final JadxPluginContext context;
|
||||
private final Map<String, XApkData> loaded = new HashMap<>();
|
||||
|
||||
public XApkLoader(JadxPluginContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public @Nullable XApkData checkAndLoad(Path inputPath) {
|
||||
String fileName = inputPath.getFileName().toString();
|
||||
if (!fileName.toLowerCase(Locale.ROOT).endsWith(".xapk")) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
XApkData loadedData = getLoaded(inputPath);
|
||||
if (loadedData != null) {
|
||||
return loadedData;
|
||||
}
|
||||
File xapkFile = inputPath.toFile();
|
||||
if (!FileUtils.isZipFile(xapkFile)) {
|
||||
return null;
|
||||
}
|
||||
try (ZipContent content = context.getZipReader().open(xapkFile)) {
|
||||
IZipEntry manifestEntry = content.searchEntry("manifest.json");
|
||||
if (manifestEntry == null) {
|
||||
return null;
|
||||
}
|
||||
String manifestStr = new String(manifestEntry.getBytes(), StandardCharsets.UTF_8);
|
||||
XApkManifest xApkManifest = GsonUtils.buildGson().fromJson(manifestStr, XApkManifest.class);
|
||||
if (xApkManifest.getVersion() != 2 || xApkManifest.getSplitApks().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// checks complete
|
||||
// unpack all files into temp directory
|
||||
XApkData xApkData = unpackXApk(xapkFile, xApkManifest, content);
|
||||
saveLoaded(inputPath, xApkData);
|
||||
return xApkData;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to load XApk file: {}", inputPath.toAbsolutePath(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private XApkData unpackXApk(File xapkFile, XApkManifest xApkManifest, ZipContent content) throws IOException {
|
||||
Set<String> declaredApks = xApkManifest.getSplitApks().stream()
|
||||
.map(SplitApk::getFile).collect(Collectors.toSet());
|
||||
List<Path> apks = new ArrayList<>(declaredApks.size());
|
||||
List<Path> files = new ArrayList<>();
|
||||
|
||||
String dirName = xapkFile.getName() + '_' + System.currentTimeMillis();
|
||||
Path tmpDir = context.files().getPluginTempDir().resolve(dirName);
|
||||
FileUtils.makeDirs(tmpDir);
|
||||
for (IZipEntry entry : content.getEntries()) {
|
||||
String fileName = entry.getName();
|
||||
Path file = tmpDir.resolve(fileName);
|
||||
Files.write(file, entry.getBytes());
|
||||
if (declaredApks.contains(fileName)) {
|
||||
apks.add(file);
|
||||
} else {
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
return new XApkData(xApkManifest, tmpDir, apks, files);
|
||||
}
|
||||
|
||||
private XApkData getLoaded(Path inputPath) throws IOException {
|
||||
return loaded.get(pathToKey(inputPath));
|
||||
}
|
||||
|
||||
private void saveLoaded(Path inputPath, XApkData xApkData) throws IOException {
|
||||
loaded.put(pathToKey(inputPath), xApkData);
|
||||
}
|
||||
|
||||
private static String pathToKey(Path path) throws IOException {
|
||||
return path.toRealPath(LinkOption.NOFOLLOW_LINKS).toString();
|
||||
}
|
||||
|
||||
public synchronized void unload() {
|
||||
for (XApkData data : loaded.values()) {
|
||||
FileUtils.deleteDirIfExists(data.getTmpDir());
|
||||
}
|
||||
loaded.clear();
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
package jadx.plugins.input.xapk
|
||||
|
||||
import jadx.api.plugins.input.ICodeLoader
|
||||
import jadx.api.plugins.input.JadxCodeInput
|
||||
import jadx.api.plugins.utils.CommonFileUtils
|
||||
import jadx.plugins.input.dex.DexInputPlugin
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
class XapkCustomCodeInput(
|
||||
private val dexInputPlugin: DexInputPlugin,
|
||||
private val zipReader: ZipReader,
|
||||
) : JadxCodeInput {
|
||||
|
||||
override fun loadFiles(input: List<Path>): ICodeLoader {
|
||||
val apkFiles = mutableListOf<File>()
|
||||
for (file in input.map { it.toFile() }) {
|
||||
if (!file.name.endsWith(".xapk")) continue
|
||||
|
||||
val manifest = XapkUtils.getManifest(file, zipReader) ?: continue
|
||||
if (!XapkUtils.isSupported(manifest)) continue
|
||||
|
||||
zipReader.open(file).use { zip ->
|
||||
for (splitApk in manifest.splitApks) {
|
||||
val splitApkEntry = zip.searchEntry(splitApk.file)
|
||||
if (splitApkEntry != null) {
|
||||
val tmpFile = splitApkEntry.inputStream.use {
|
||||
CommonFileUtils.saveToTempFile(it, ".apk").toFile()
|
||||
}
|
||||
apkFiles.add(tmpFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val codeLoader = dexInputPlugin.loadFiles(apkFiles.map { it.toPath() })
|
||||
|
||||
apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) }
|
||||
|
||||
return codeLoader
|
||||
}
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
package jadx.plugins.input.xapk
|
||||
|
||||
import jadx.api.ResourceFile
|
||||
import jadx.api.ResourcesLoader
|
||||
import jadx.api.plugins.CustomResourcesLoader
|
||||
import jadx.api.plugins.utils.CommonFileUtils
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
|
||||
class XapkCustomResourcesLoader(private val zipReader: ZipReader) : CustomResourcesLoader {
|
||||
private val tmpFiles = mutableListOf<File>()
|
||||
|
||||
override fun load(loader: ResourcesLoader, list: MutableList<ResourceFile>, file: File): Boolean {
|
||||
if (!file.name.endsWith(".xapk")) return false
|
||||
|
||||
val manifest = XapkUtils.getManifest(file, zipReader) ?: return false
|
||||
if (!XapkUtils.isSupported(manifest)) return false
|
||||
|
||||
val apkEntries = manifest.splitApks.map { it.file }.toHashSet()
|
||||
zipReader.visitEntries(file) { entry ->
|
||||
if (apkEntries.contains(entry.name)) {
|
||||
val tmpFile = entry.inputStream.use {
|
||||
CommonFileUtils.saveToTempFile(it, ".apk").toFile()
|
||||
}
|
||||
loader.defaultLoadFile(list, tmpFile, entry.name + "/")
|
||||
tmpFiles += tmpFile
|
||||
} else {
|
||||
loader.addEntry(list, file, entry, "")
|
||||
}
|
||||
null
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
tmpFiles.forEach(CommonFileUtils::safeDeleteFile)
|
||||
tmpFiles.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package jadx.plugins.input.xapk
|
||||
|
||||
import jadx.api.plugins.JadxPlugin
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.api.plugins.JadxPluginInfo
|
||||
import jadx.api.plugins.JadxPluginInfoBuilder
|
||||
import jadx.plugins.input.dex.DexInputPlugin
|
||||
|
||||
class XapkInputPlugin : JadxPlugin {
|
||||
|
||||
override fun getPluginInfo(): JadxPluginInfo =
|
||||
JadxPluginInfoBuilder.pluginId("xapk-input")
|
||||
.name("XAPK Input")
|
||||
.description("Load .xapk files")
|
||||
.build()
|
||||
|
||||
override fun init(context: JadxPluginContext) {
|
||||
val dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java)
|
||||
context.addCodeInput(XapkCustomCodeInput(dexInputPlugin, context.zipReader))
|
||||
context.decompiler.addCustomResourcesLoader(XapkCustomResourcesLoader(context.zipReader))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package jadx.plugins.input.xapk
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class XapkManifest(
|
||||
@SerializedName("xapk_version")
|
||||
var xapkVersion: Int = 0,
|
||||
@SerializedName("split_apks")
|
||||
var splitApks: List<SplitApk> = listOf(),
|
||||
) {
|
||||
data class SplitApk(
|
||||
@SerializedName("file")
|
||||
var file: String = "",
|
||||
@SerializedName("id")
|
||||
var id: String = "",
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package jadx.plugins.input.xapk
|
||||
|
||||
import jadx.core.utils.GsonUtils.buildGson
|
||||
import jadx.core.utils.files.FileUtils
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
|
||||
object XapkUtils {
|
||||
fun getManifest(file: File, zipReader: ZipReader): XapkManifest? {
|
||||
if (!FileUtils.isZipFile(file)) return null
|
||||
try {
|
||||
zipReader.open(file).use { zip ->
|
||||
val manifestEntry = zip.searchEntry("manifest.json") ?: return null
|
||||
return InputStreamReader(manifestEntry.inputStream).use {
|
||||
buildGson().fromJson(it, XapkManifest::class.java)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun isSupported(manifest: XapkManifest): Boolean {
|
||||
return manifest.xapkVersion == 2 && manifest.splitApks.isNotEmpty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package jadx.plugins.input.xapk.data;
|
||||
|
||||
public class SplitApk {
|
||||
private String file;
|
||||
private String id;
|
||||
|
||||
public String getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void setFile(String file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package jadx.plugins.input.xapk.data;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public class XApkData {
|
||||
private final XApkManifest manifest;
|
||||
private final Path tmpDir;
|
||||
private final List<Path> files;
|
||||
private final List<Path> apks;
|
||||
|
||||
public XApkData(XApkManifest manifest, Path tmpDir, List<Path> apks, List<Path> files) {
|
||||
this.manifest = manifest;
|
||||
this.tmpDir = tmpDir;
|
||||
this.apks = apks;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public List<Path> getApks() {
|
||||
return apks;
|
||||
}
|
||||
|
||||
public List<Path> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public XApkManifest getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public Path getTmpDir() {
|
||||
return tmpDir;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package jadx.plugins.input.xapk.data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class XApkManifest {
|
||||
@SerializedName("xapk_version")
|
||||
int version;
|
||||
@SerializedName("split_apks")
|
||||
List<SplitApk> splitApks;
|
||||
|
||||
public List<SplitApk> getSplitApks() {
|
||||
return splitApks;
|
||||
}
|
||||
|
||||
public void setSplitApks(List<SplitApk> splitApks) {
|
||||
this.splitApks = splitApks;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(int version) {
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
jadx.plugins.input.xapk.XapkInputPlugin
|
||||
jadx.plugins.input.xapk.XApkInputPlugin
|
||||
|
||||
Reference in New Issue
Block a user