diff --git a/README.md b/README.md index 582c87be490..32b629d2781 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ and also packed to `build/jadx-.zip` ### Usage ``` -jadx[-gui] [command] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts) +jadx[-gui] [command] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts) commands (use ' --help' for command options): plugins - manage jadx plugins diff --git a/jadx-cli/build.gradle.kts b/jadx-cli/build.gradle.kts index 4e11ca05b6c..68b2b56eb8d 100644 --- a/jadx-cli/build.gradle.kts +++ b/jadx-cli/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin")) runtimeOnly(project(":jadx-plugins:jadx-xapk-input")) runtimeOnly(project(":jadx-plugins:jadx-aab-input")) + runtimeOnly(project(":jadx-plugins:jadx-apkm-input")) implementation("org.jcommander:jcommander:2.0") implementation("ch.qos.logback:logback-classic:1.5.13") diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 525bdb79303..20526b41e51 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -35,7 +35,7 @@ public class JadxCLIArgs { - @Parameter(description = " (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .jadx.kts)") + @Parameter(description = " (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)") protected List files = new ArrayList<>(1); @Parameter(names = { "-d", "--output-dir" }, description = "output directory") diff --git a/jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java b/jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java index d31057cc826..cf0f8fea92a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java @@ -17,7 +17,7 @@ public class FileDialogWrapper { private static final List OPEN_FILES_EXTS = Arrays.asList( - "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "jadx.kts", "xapk"); + "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "jadx.kts", "xapk", "apkm"); private final MainWindow mainWindow; diff --git a/jadx-plugins/jadx-apkm-input/build.gradle.kts b/jadx-plugins/jadx-apkm-input/build.gradle.kts new file mode 100644 index 00000000000..58ea804a240 --- /dev/null +++ b/jadx-plugins/jadx-apkm-input/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("jadx-library") + id("jadx-kotlin") +} + +dependencies { + api(project(":jadx-core")) + + implementation(project(":jadx-plugins:jadx-dex-input")) + implementation("com.google.code.gson:gson:2.11.0") +} diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomCodeInput.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomCodeInput.kt new file mode 100644 index 00000000000..b4129976939 --- /dev/null +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomCodeInput.kt @@ -0,0 +1,38 @@ +package jadx.plugins.input.apkm + +import jadx.api.plugins.input.ICodeLoader +import jadx.api.plugins.input.JadxCodeInput +import jadx.api.plugins.utils.CommonFileUtils +import jadx.api.plugins.utils.ZipSecurity +import java.io.File +import java.nio.file.Path + +class ApkmCustomCodeInput( + private val plugin: ApkmInputPlugin, +) : JadxCodeInput { + override fun loadFiles(input: List): ICodeLoader { + val apkFiles = mutableListOf() + for (file in input.map { it.toFile() }) { + // Check if this is a valid APKM file + val manifest = ApkmUtils.getManifest(file) ?: continue + if (!ApkmUtils.isSupported(manifest)) continue + + // Load all files ending with .apk + ZipSecurity.visitZipEntries(file) { zip, entry -> + if (entry.name.endsWith(".apk")) { + val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use { + CommonFileUtils.saveToTempFile(it, ".apk").toFile() + } + apkFiles.add(tmpFile) + } + null + } + } + + val codeLoader = plugin.dexInputPlugin.loadFiles(apkFiles.map { it.toPath() }) + + apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) } + + return codeLoader + } +} diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomResourcesLoader.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomResourcesLoader.kt new file mode 100644 index 00000000000..7142ef25625 --- /dev/null +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomResourcesLoader.kt @@ -0,0 +1,36 @@ +package jadx.plugins.input.apkm + +import jadx.api.ResourceFile +import jadx.api.ResourcesLoader +import jadx.api.plugins.CustomResourcesLoader +import jadx.api.plugins.utils.CommonFileUtils +import jadx.api.plugins.utils.ZipSecurity +import java.io.File + +class ApkmCustomResourcesLoader : CustomResourcesLoader { + private val tmpFiles = mutableListOf() + + override fun load(loader: ResourcesLoader, list: MutableList, file: File): Boolean { + // Check if this is a valid APKM file + val manifest = ApkmUtils.getManifest(file) ?: return false + if (!ApkmUtils.isSupported(manifest)) return false + + // Load all files ending with .apk + ZipSecurity.visitZipEntries(file) { zip, entry -> + if (entry.name.endsWith(".apk")) { + val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use { + CommonFileUtils.saveToTempFile(it, ".apk").toFile() + } + loader.defaultLoadFile(list, tmpFile, entry.name + "/") + tmpFiles += tmpFile + } + null + } + return true + } + + override fun close() { + tmpFiles.forEach(CommonFileUtils::safeDeleteFile) + tmpFiles.clear() + } +} diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmInputPlugin.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmInputPlugin.kt new file mode 100644 index 00000000000..90a88410ca6 --- /dev/null +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmInputPlugin.kt @@ -0,0 +1,24 @@ +package jadx.plugins.input.apkm + +import jadx.api.plugins.JadxPlugin +import jadx.api.plugins.JadxPluginContext +import jadx.api.plugins.JadxPluginInfo +import jadx.plugins.input.dex.DexInputPlugin + +class ApkmInputPlugin : JadxPlugin { + private val codeInput = ApkmCustomCodeInput(this) + private val resourcesLoader = ApkmCustomResourcesLoader() + internal lateinit var dexInputPlugin: DexInputPlugin + + override fun getPluginInfo() = JadxPluginInfo( + "apkm-input", + "APKM Input", + "Load .apkm files", + ) + + override fun init(context: JadxPluginContext) { + dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java) + context.addCodeInput(codeInput) + context.decompiler.addCustomResourcesLoader(resourcesLoader) + } +} diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmManifest.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmManifest.kt new file mode 100644 index 00000000000..bece0922f50 --- /dev/null +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmManifest.kt @@ -0,0 +1,8 @@ +package jadx.plugins.input.apkm + +import com.google.gson.annotations.SerializedName + +data class ApkmManifest( + @SerializedName("apkm_version") + var apkmVersion: Int = -1, +) diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmUtils.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmUtils.kt new file mode 100644 index 00000000000..5e30c23e737 --- /dev/null +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmUtils.kt @@ -0,0 +1,28 @@ +package jadx.plugins.input.apkm + +import jadx.api.plugins.utils.ZipSecurity +import jadx.core.utils.GsonUtils.buildGson +import jadx.core.utils.files.FileUtils +import jadx.core.utils.files.ZipFile +import java.io.File +import java.io.InputStreamReader + +object ApkmUtils { + fun getManifest(file: File): ApkmManifest? { + if (!FileUtils.isZipFile(file)) return null + try { + ZipFile(file).use { zip -> + val manifestEntry = zip.getEntry("info.json") ?: return null + return InputStreamReader(ZipSecurity.getInputStreamForEntry(zip, manifestEntry)).use { + buildGson().fromJson(it, ApkmManifest::class.java) + } + } + } catch (e: Exception) { + return null + } + } + + fun isSupported(manifest: ApkmManifest): Boolean { + return manifest.apkmVersion != -1 + } +} diff --git a/jadx-plugins/jadx-apkm-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin b/jadx-plugins/jadx-apkm-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin new file mode 100644 index 00000000000..36c4443fa7f --- /dev/null +++ b/jadx-plugins/jadx-apkm-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin @@ -0,0 +1 @@ +jadx.plugins.input.apkm.ApkmInputPlugin diff --git a/settings.gradle.kts b/settings.gradle.kts index f6fc258a903..67f9946dcd6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,6 +26,7 @@ include("jadx-plugins:jadx-rename-mappings") include("jadx-plugins:jadx-kotlin-metadata") include("jadx-plugins:jadx-xapk-input") include("jadx-plugins:jadx-aab-input") +include("jadx-plugins:jadx-apkm-input") include("jadx-plugins:jadx-script:jadx-script-plugin") include("jadx-plugins:jadx-script:jadx-script-runtime")