mirror of
https://github.com/skylot/jadx.git
synced 2024-11-23 04:39:46 +00:00
* 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:
parent
238fe17df0
commit
f5accc8464
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -32,7 +32,6 @@ public class DexFileLoader {
|
||||
|
||||
public DexFileLoader(DexInputOptions options) {
|
||||
this.options = options;
|
||||
resetDexUniqId();
|
||||
}
|
||||
|
||||
public List<DexReader> collectDexFiles(List<Path> pathsList) {
|
||||
|
11
jadx-plugins/jadx-xapk-input/build.gradle.kts
Normal file
11
jadx-plugins/jadx-xapk-input/build.gradle.kts
Normal 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")
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
jadx.plugins.input.xapk.XapkInputPlugin
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user