Skip to content

Commit

Permalink
feat: add support for xapk files (#1597)(PR #2064)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
iscle and skylot authored Dec 21, 2023
1 parent 238fe17 commit f5accc8
Show file tree
Hide file tree
Showing 16 changed files with 223 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions jadx-cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

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")
Expand Down
25 changes: 25 additions & 0 deletions jadx-core/src/main/java/jadx/api/JadxDecompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -170,6 +172,7 @@ private void reset() {
public void close() {
reset();
closeInputs();
closeLoaders();
args.close();
}

Expand All @@ -184,6 +187,17 @@ private void closeInputs() {
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());
Expand Down Expand Up @@ -678,6 +692,17 @@ public List<ICodeLoader> getCustomCodeLoaders() {
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);
}
Expand Down
21 changes: 18 additions & 3 deletions jadx-core/src/main/java/jadx/api/ResourcesLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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;
Expand Down Expand Up @@ -140,9 +141,23 @@ private void loadFile(List<ResourceFile> list, File file) {
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 {
Expand All @@ -151,13 +166,13 @@ private void loadFile(List<ResourceFile> list, File file) {
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public class DexFileLoader {

public DexFileLoader(DexInputOptions options) {
this.options = options;
resetDexUniqId();
}

public List<DexReader> collectDexFiles(List<Path> pathsList) {
Expand Down
11 changes: 11 additions & 0 deletions jadx-plugins/jadx-xapk-input/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
jadx.plugins.input.xapk.XapkInputPlugin
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit f5accc8

Please sign in to comment.