diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 6d77c9ec0..4a82d9bb5 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -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(); } diff --git a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginsData.java b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginsData.java index 5f14ecdf5..c59d9f021 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/JadxPluginsData.java +++ b/jadx-core/src/main/java/jadx/core/plugins/JadxPluginsData.java @@ -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")); } diff --git a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java index 6fa79a4d5..ae1639080 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java @@ -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(); diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index 23f453176..fbdcf4180 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -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); diff --git a/jadx-plugins/jadx-xapk-input/build.gradle.kts b/jadx-plugins/jadx-xapk-input/build.gradle.kts index 9bfa50e1e..cadb23faa 100644 --- a/jadx-plugins/jadx-xapk-input/build.gradle.kts +++ b/jadx-plugins/jadx-xapk-input/build.gradle.kts @@ -1,6 +1,5 @@ plugins { id("jadx-library") - id("jadx-kotlin") } dependencies { diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkCustomInput.java b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkCustomInput.java new file mode 100644 index 000000000..a5eb315fb --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkCustomInput.java @@ -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 input) { + List 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 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 { + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkInputPlugin.java b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkInputPlugin.java new file mode 100644 index 000000000..842c5bf74 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkInputPlugin.java @@ -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(); + } + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkLoader.java b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkLoader.java new file mode 100644 index 000000000..f063bc5e1 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkLoader.java @@ -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 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 declaredApks = xApkManifest.getSplitApks().stream() + .map(SplitApk::getFile).collect(Collectors.toSet()); + List apks = new ArrayList<>(declaredApks.size()); + List 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(); + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt deleted file mode 100644 index 72a6126ba..000000000 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt +++ /dev/null @@ -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): ICodeLoader { - val apkFiles = mutableListOf() - 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 - } -} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomResourcesLoader.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomResourcesLoader.kt deleted file mode 100644 index 671a882db..000000000 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomResourcesLoader.kt +++ /dev/null @@ -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() - - override fun load(loader: ResourcesLoader, list: MutableList, 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() - } -} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkInputPlugin.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkInputPlugin.kt deleted file mode 100644 index 40465a2b1..000000000 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkInputPlugin.kt +++ /dev/null @@ -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)) - } -} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkManifest.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkManifest.kt deleted file mode 100644 index 883de6408..000000000 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkManifest.kt +++ /dev/null @@ -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 = listOf(), -) { - data class SplitApk( - @SerializedName("file") - var file: String = "", - @SerializedName("id") - var id: String = "", - ) -} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt deleted file mode 100644 index e33303dbc..000000000 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt +++ /dev/null @@ -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() - } -} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/SplitApk.java b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/SplitApk.java new file mode 100644 index 000000000..9bd007d69 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/SplitApk.java @@ -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; + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/XApkData.java b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/XApkData.java new file mode 100644 index 000000000..37adddae6 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/XApkData.java @@ -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 files; + private final List apks; + + public XApkData(XApkManifest manifest, Path tmpDir, List apks, List files) { + this.manifest = manifest; + this.tmpDir = tmpDir; + this.apks = apks; + this.files = files; + } + + public List getApks() { + return apks; + } + + public List getFiles() { + return files; + } + + public XApkManifest getManifest() { + return manifest; + } + + public Path getTmpDir() { + return tmpDir; + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/XApkManifest.java b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/XApkManifest.java new file mode 100644 index 000000000..beda0b2cf --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/XApkManifest.java @@ -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 splitApks; + + public List getSplitApks() { + return splitApks; + } + + public void setSplitApks(List splitApks) { + this.splitApks = splitApks; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin b/jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin index e50351aa7..78ee84c4f 100644 --- a/jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin +++ b/jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin @@ -1 +1 @@ -jadx.plugins.input.xapk.XapkInputPlugin +jadx.plugins.input.xapk.XApkInputPlugin