feat: add support for xapk files (#1597)(PR #2064)

* feat: annotate JadxPlugin with NotNull

Allows for better Kotlin support

* feat: add support for custom resources loader

* feat: add support for xapk resources loading

* fix: rename "decode" to "load"

* refactor: annotate JadxCodeInput with NotNull

* feat: add support for xapk code loading

* feat: add xapk support to file filter

* fix code formatting

* revert NotNull annotation

* several improvements

* refactor: fix typo

---------

Co-authored-by: Skylot <skylot@gmail.com>
This commit is contained in:
Iscle 2023-12-21 19:46:40 +01:00 committed by GitHub
parent 238fe17df0
commit f5accc8464
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 223 additions and 7 deletions

View File

@ -79,7 +79,7 @@ and also packed to `build/jadx-<version>.zip`
### Usage
```
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)
jadx[-gui] [command] [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk)
commands (use '<command> --help' for command options):
plugins - manage jadx plugins

View File

@ -17,6 +17,7 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
implementation("org.jcommander:jcommander:1.83")
implementation("ch.qos.logback:logback-classic:1.4.14")

View File

@ -34,7 +34,7 @@ import jadx.core.utils.files.FileUtils;
public class JadxCLIArgs {
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)")
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk)")
protected List<String> files = new ArrayList<>(1);
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")

View File

@ -28,6 +28,7 @@ import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.metadata.annotations.VarNode;
import jadx.api.metadata.annotations.VarRef;
import jadx.api.plugins.CustomResourcesLoader;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.events.IJadxEvents;
import jadx.api.plugins.input.ICodeLoader;
@ -99,6 +100,7 @@ public final class JadxDecompiler implements Closeable {
private final JadxEventsImpl events = new JadxEventsImpl();
private final List<ICodeLoader> customCodeLoaders = new ArrayList<>();
private final List<CustomResourcesLoader> customResourcesLoaders = new ArrayList<>();
private final Map<JadxPassType, List<JadxPass>> customPasses = new HashMap<>();
public JadxDecompiler() {
@ -170,6 +172,7 @@ public final class JadxDecompiler implements Closeable {
public void close() {
reset();
closeInputs();
closeLoaders();
args.close();
}
@ -184,6 +187,17 @@ public final class JadxDecompiler implements Closeable {
loadedInputs.clear();
}
private void closeLoaders() {
for (CustomResourcesLoader resourcesLoader : customResourcesLoaders) {
try {
resourcesLoader.close();
} catch (Exception e) {
LOG.error("Failed to close resource loader: " + resourcesLoader, e);
}
}
customResourcesLoaders.clear();
}
private void loadPlugins() {
pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input");
pluginManager.load(args.getPluginLoader());
@ -678,6 +692,17 @@ public final class JadxDecompiler implements Closeable {
return customCodeLoaders;
}
public void addCustomResourcesLoader(CustomResourcesLoader loader) {
if (customResourcesLoaders.contains(loader)) {
return;
}
customResourcesLoaders.add(loader);
}
public List<CustomResourcesLoader> getCustomResourcesLoaders() {
return customResourcesLoaders;
}
public void addCustomPass(JadxPass pass) {
customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass);
}

View File

@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile.ZipRef;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.CustomResourcesLoader;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
@ -140,9 +141,23 @@ public final class ResourcesLoader {
if (file == null || file.isDirectory()) {
return;
}
// Try to load the resources with a custom loader first
for (CustomResourcesLoader loader : jadxRef.getCustomResourcesLoaders()) {
if (loader.load(this, list, file)) {
LOG.debug("Custom loader used for {}", file.getAbsolutePath());
return;
}
}
// If no custom decoder was able to decode the resources, use the default decoder
defaultLoadFile(list, file, "");
}
public void defaultLoadFile(List<ResourceFile> list, File file, String subDir) {
if (FileUtils.isZipFile(file)) {
ZipSecurity.visitZipEntries(file, (zipFile, entry) -> {
addEntry(list, file, entry);
addEntry(list, file, entry, subDir);
return null;
});
} else {
@ -151,13 +166,13 @@ public final class ResourcesLoader {
}
}
private void addEntry(List<ResourceFile> list, File zipFile, ZipEntry entry) {
public void addEntry(List<ResourceFile> list, File zipFile, ZipEntry entry, String subDir) {
if (entry.isDirectory()) {
return;
}
String name = entry.getName();
ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type);
ResourceFile rf = ResourceFile.createResourceFile(jadxRef, subDir + name, type);
if (rf != null) {
rf.setZipRef(new ZipRef(zipFile, name));
list.add(rf);

View File

@ -0,0 +1,19 @@
package jadx.api.plugins;
import java.io.Closeable;
import java.io.File;
import java.util.List;
import jadx.api.ResourceFile;
import jadx.api.ResourcesLoader;
public interface CustomResourcesLoader extends Closeable {
/**
* Load resources from file to list of ResourceFile
*
* @param list list to add loaded resources
* @param file file to load
* @return true if file was loaded
*/
boolean load(ResourcesLoader loader, List<ResourceFile> list, File file);
}

View File

@ -17,7 +17,7 @@ import jadx.gui.utils.NLS;
public class FileDialogWrapper {
private static final List<String> OPEN_FILES_EXTS = Arrays.asList(
"apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "jadx.kts");
"apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "jadx.kts", "xapk");
private final MainWindow mainWindow;

View File

@ -32,7 +32,6 @@ public class DexFileLoader {
public DexFileLoader(DexInputOptions options) {
this.options = options;
resetDexUniqId();
}
public List<DexReader> collectDexFiles(List<Path> pathsList) {

View File

@ -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.10.1")
}

View File

@ -0,0 +1,39 @@
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.api.plugins.utils.ZipSecurity
import java.io.File
import java.nio.file.Path
import java.util.zip.ZipFile
class XapkCustomCodeInput(
private val plugin: XapkInputPlugin,
) : JadxCodeInput {
override fun loadFiles(input: List<Path>): ICodeLoader {
val apkFiles = mutableListOf<File>()
for (file in input.map { it.toFile() }) {
val manifest = XapkUtils.getManifest(file) ?: continue
if (!XapkUtils.isSupported(manifest)) continue
ZipFile(file).use { zip ->
for (splitApk in manifest.splitApks) {
val splitApkEntry = zip.getEntry(splitApk.file)
if (splitApkEntry != null) {
val tmpFile = ZipSecurity.getInputStreamForEntry(zip, splitApkEntry).use {
CommonFileUtils.saveToTempFile(it, ".apk").toFile()
}
apkFiles.add(tmpFile)
}
}
}
}
val codeLoader = plugin.dexInputPlugin.loadFiles(apkFiles.map { it.toPath() })
apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) }
return codeLoader
}
}

View File

@ -0,0 +1,37 @@
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.api.plugins.utils.ZipSecurity
import java.io.File
class XapkCustomResourcesLoader : CustomResourcesLoader {
private val tmpFiles = mutableListOf<File>()
override fun load(loader: ResourcesLoader, list: MutableList<ResourceFile>, file: File): Boolean {
val manifest = XapkUtils.getManifest(file) ?: return false
if (!XapkUtils.isSupported(manifest)) return false
val apkEntries = manifest.splitApks.map { it.file }.toHashSet()
ZipSecurity.visitZipEntries(file) { zip, entry ->
if (apkEntries.contains(entry.name)) {
val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).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()
}
}

View File

@ -0,0 +1,23 @@
package jadx.plugins.input.xapk
import jadx.api.plugins.JadxPlugin
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.JadxPluginInfo
import jadx.plugins.input.dex.DexInputPlugin
class XapkInputPlugin : JadxPlugin {
private val codeInput = XapkCustomCodeInput(this)
private val resourcesLoader = XapkCustomResourcesLoader()
internal var dexInputPlugin = DexInputPlugin()
override fun getPluginInfo() = JadxPluginInfo(
"xapk-input",
"XAPK Input",
"Load .xapk files",
)
override fun init(context: JadxPluginContext) {
context.addCodeInput(codeInput)
context.decompiler.addCustomResourcesLoader(resourcesLoader)
}
}

View File

@ -0,0 +1,17 @@
package jadx.plugins.input.xapk
import com.google.gson.annotations.SerializedName
data class XapkManifest(
@SerializedName("xapk_version")
val xapkVersion: Int,
@SerializedName("split_apks")
val splitApks: List<SplitApk>,
) {
data class SplitApk(
@SerializedName("file")
val file: String,
@SerializedName("id")
val id: String,
)
}

View File

@ -0,0 +1,28 @@
package jadx.plugins.input.xapk
import com.google.gson.Gson
import jadx.api.plugins.utils.ZipSecurity
import jadx.core.utils.files.FileUtils
import java.io.File
import java.io.InputStreamReader
import java.util.zip.ZipFile
object XapkUtils {
fun getManifest(file: File): XapkManifest? {
if (!FileUtils.isZipFile(file)) return null
try {
ZipFile(file).use { zip ->
val manifestEntry = zip.getEntry("manifest.json") ?: return null
return InputStreamReader(ZipSecurity.getInputStreamForEntry(zip, manifestEntry)).use {
Gson().fromJson(it, XapkManifest::class.java)
}
}
} catch (e: Exception) {
return null
}
}
fun isSupported(manifest: XapkManifest): Boolean {
return manifest.xapkVersion == 2 && manifest.splitApks.isNotEmpty()
}
}

View File

@ -0,0 +1 @@
jadx.plugins.input.xapk.XapkInputPlugin

View File

@ -18,6 +18,7 @@ include("jadx-plugins:jadx-smali-input")
include("jadx-plugins:jadx-java-convert")
include("jadx-plugins:jadx-rename-mappings")
include("jadx-plugins:jadx-kotlin-metadata")
include("jadx-plugins:jadx-xapk-input")
include("jadx-plugins:jadx-script:jadx-script-plugin")
include("jadx-plugins:jadx-script:jadx-script-runtime")