Compare commits

...

12 Commits

Author SHA1 Message Date
Skylot 5640f3b931 fix test 2024-06-02 18:21:21 +01:00
Skylot 02bc27e887 build exclude list in init method to improve performance, add default values migration 2024-06-02 18:21:04 +01:00
bagipro 794e5adb7f Change deobf-min default to 2 2024-05-09 18:22:02 +07:00
bagipro a81cec7701 Improve deobf whitelist 2024-05-08 16:16:08 +07:00
Skylot 09fa35f144 feat: allow to change config and cache dirs with env vars (#2159) 2024-04-27 21:48:42 +01:00
Skylot f2a6a1e942 build: update JDK to 21 for windows artifacts 2024-04-27 17:40:06 +01:00
Andrei Kudryavtsev b85900aa3d feat: move AAB support to separate plugin (PR #2165)
* wip: finished with factories

* wip: bundleconfig.pb

* wip: jadx-aab-input, separate BundleConfig parser

* wip: removed test apks

* wip: proto xml pretty print

* wip: fixed getNamedValues NPE

* minor fixes

* spotless

* enabled zip64 for gui shadow jar

* spotless

* spotless

* reverted manifest identification since signature parsing not working at the moment

* replace static methods with new API methods

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
2024-04-26 21:54:54 +01:00
Jan S 37a42d1418 fix(gui): show correct content of binary resources in hex view (#2160) (PR #2166) 2024-04-24 19:29:52 +01:00
Jan S 07dde05337 fix(build): configure launch4j to not change current directory (#2162) (PR #2163) 2024-04-24 18:52:08 +01:00
Nick 8618214c7f docs: improve installation section (PR #2161)
* Update README.md

* Update README.md
2024-04-24 18:43:05 +01:00
Skylot b80f32a36f fix(smali-input): compile one smali file at a time to avoid 64k limit (#2158) 2024-04-23 22:14:59 +01:00
Skylot ce527ed753 fix(build): add missing files in bundle 2024-04-20 21:04:17 +01:00
57 changed files with 989 additions and 265 deletions
+8 -8
View File
@@ -20,8 +20,8 @@ jobs:
- name: Set jadx version
run: |
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
JADX_REV=$(git rev-list --count HEAD)
JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Build with Gradle
@@ -37,7 +37,7 @@ jobs:
# Upload unpacked files for now
path: build/jadx/**/*
if-no-files-found: error
retention-days: 30
retention-days: 14
- name: Save exe artifact
uses: actions/upload-artifact@v4
@@ -45,7 +45,7 @@ jobs:
name: ${{ format('jadx-gui-{0}-no-jre-win.exe', env.JADX_VERSION) }}
path: build/*.exe
if-no-files-found: error
retention-days: 30
retention-days: 14
build-win-bundle:
runs-on: windows-latest
@@ -57,7 +57,7 @@ jobs:
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
release: 17
release: 21
- name: Print Java version
shell: bash
@@ -66,8 +66,8 @@ jobs:
- name: Set jadx version
shell: bash
run: |
JADX_LAST_TAG=$(git describe --abbrev=0 --tags)
JADX_VERSION="${JADX_LAST_TAG:1}.$GITHUB_RUN_NUMBER-${GITHUB_SHA:0:8}"
JADX_REV=$(git rev-list --count HEAD)
JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}"
echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV
- name: Build with Gradle
@@ -81,4 +81,4 @@ jobs:
name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }}
path: jadx-gui/build/*-with-jre-win/*
if-no-files-found: error
retention-days: 30
retention-days: 14
+19 -13
View File
@@ -51,18 +51,22 @@ On Windows run `.bat` files with double-click\
For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer).
### Install
1. Arch linux ![Arch Linux package](https://img.shields.io/archlinux/v/extra/any/jadx?label=)
```bash
sudo pacman -S jadx
```
2. macOS ![homebrew version](https://img.shields.io/homebrew/v/jadx?label=)
```bash
brew install jadx
```
3. [Flathub ![Flathub](https://img.shields.io/flathub/v/com.github.skylot.jadx?label=)](https://flathub.org/apps/details/com.github.skylot.jadx)
```bash
flatpak install flathub com.github.skylot.jadx
```
- Arch Linux
[![Arch Linux package](https://img.shields.io/archlinux/v/extra/any/jadx)](https://archlinux.org/packages/extra/any/jadx/)
[![AUR Version](https://img.shields.io/aur/version/jadx-git)](https://aur.archlinux.org/packages/jadx-git)
```bash
sudo pacman -S jadx
```
- macOS
[![homebrew version](https://img.shields.io/homebrew/v/jadx)](https://formulae.brew.sh/formula/jadx)
```bash
brew install jadx
```
- Flathub
[![Flathub Version](https://img.shields.io/flathub/v/com.github.skylot.jadx)](https://flathub.org/apps/com.github.skylot.jadx)
```bash
flatpak install flathub com.github.skylot.jadx
```
### Use jadx as a library
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
@@ -124,7 +128,7 @@ options:
--deobf - activate deobfuscation
--deobf-min - min length of name, renamed if shorter, default: 3
--deobf-max - max length of name, renamed if longer, default: 64
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px
--deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.* android.os.* androidx.core.os.* androidx.annotation.*
--deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension
--deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file:
'read' - read if found, don't save (default)
@@ -181,6 +185,8 @@ Environment variables:
JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files
JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files
JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)
JADX_CONFIG_DIR - custom config directory, using system by default
JADX_CACHE_DIR - custom cache directory, using system by default
JADX_TMP_DIR - custom temp directory, using system by default
Examples:
+4
View File
@@ -89,6 +89,10 @@ val copyArtifacts by tasks.registering(Copy::class) {
include("**/*.jar")
rename("jadx-gui-(.*)-all.jar", "jadx-$1-all.jar")
}
from(layout.projectDirectory) {
include("README.md")
include("LICENSE")
}
into(layout.buildDirectory.dir("jadx"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
+1
View File
@@ -18,6 +18,7 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
implementation("org.jcommander:jcommander:1.83")
implementation("ch.qos.logback:logback-classic:1.5.6")
@@ -111,6 +111,8 @@ public class JCommanderWrapper<T> {
out.println(" JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files");
out.println(" JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files");
out.println(" JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)");
out.println(" JADX_CONFIG_DIR - custom config directory, using system by default");
out.println(" JADX_CACHE_DIR - custom cache directory, using system by default");
out.println(" JADX_TMP_DIR - custom temp directory, using system by default");
out.println();
out.println("Examples:");
@@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.TextResMapFile;
import jadx.core.xmlgen.ResTableParser;
import jadx.core.xmlgen.ResTableBinaryParser;
/**
* Utility class for convert '.arsc' to simple text file with mapping id to resource name
@@ -54,7 +54,7 @@ public class ConvertArscFile {
rewritesCount = 0;
for (Path resFile : inputPaths) {
LOG.info("Processing {}", resFile);
ResTableParser resTableParser = new ResTableParser(root, true);
ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true);
if (resFile.getFileName().toString().endsWith(".jar")) {
// Load resources.arsc from android.jar
try (ZipFile zip = new ZipFile(resFile.toFile())) {
+6
View File
@@ -0,0 +1,6 @@
## jadx app commons
This module contains common utilities used in jadx apps (cli and gui) and not needed in jadx-code module:
- `JadxCommonFiles` - wrapper for `dev.dirs:directories` lib to get
'config' and 'cache' directories in cross-platform way
- `JadxCommonEnv` - utils for work with environment variables
@@ -0,0 +1,7 @@
plugins {
id("jadx-library")
}
dependencies {
implementation("dev.dirs:directories:26")
}
@@ -0,0 +1,29 @@
package jadx.commons.app;
public class JadxCommonEnv {
public static String get(String varName, String defValue) {
String strValue = System.getenv(varName);
return isNullOrEmpty(strValue) ? defValue : strValue;
}
public static boolean getBool(String varName, boolean defValue) {
String strValue = System.getenv(varName);
if (isNullOrEmpty(strValue)) {
return defValue;
}
return strValue.equalsIgnoreCase("true");
}
public static int getInt(String varName, int defValue) {
String strValue = System.getenv(varName);
if (isNullOrEmpty(strValue)) {
return defValue;
}
return Integer.parseInt(strValue);
}
private static boolean isNullOrEmpty(String value) {
return value == null || value.isEmpty();
}
}
@@ -0,0 +1,73 @@
package jadx.commons.app;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.jetbrains.annotations.Nullable;
import dev.dirs.ProjectDirectories;
public class JadxCommonFiles {
private static final Path CONFIG_DIR;
private static final Path CACHE_DIR;
public static Path getConfigDir() {
return CONFIG_DIR;
}
public static Path getCacheDir() {
return CACHE_DIR;
}
static {
DirsLoader loader = new DirsLoader();
loader.init();
CONFIG_DIR = loader.getConfigDir();
CACHE_DIR = loader.getCacheDir();
}
private static final class DirsLoader {
private @Nullable ProjectDirectories dirs;
private Path configDir;
private Path cacheDir;
public void init() {
try {
configDir = loadEnvDir("JADX_CONFIG_DIR");
cacheDir = loadEnvDir("JADX_CACHE_DIR");
} catch (Exception e) {
throw new RuntimeException("Failed to init common directories", e);
}
}
private Path loadEnvDir(String envVar) throws IOException {
String envDir = JadxCommonEnv.get(envVar, null);
String dirStr;
if (envDir != null) {
dirStr = envDir;
} else {
dirStr = loadDirs().configDir;
}
Path path = Path.of(dirStr).toAbsolutePath();
Files.createDirectories(path);
return path;
}
private synchronized ProjectDirectories loadDirs() {
if (dirs == null) {
dirs = ProjectDirectories.from("io.github", "skylot", "jadx");
}
return dirs;
}
public Path getCacheDir() {
return cacheDir;
}
public Path getConfigDir() {
return configDir;
}
}
}
-8
View File
@@ -7,14 +7,6 @@ dependencies {
implementation("com.google.code.gson:gson:2.10.1")
// TODO: move resources decoding to separate plugin module
implementation("com.android.tools.build:aapt2-proto:8.3.2-10880808")
implementation("com.google.protobuf:protobuf-java") {
version {
require("3.25.3") // version 4 conflict with `aapt2-proto`
}
}
testImplementation("org.apache.commons:commons-lang3:3.14.0")
testImplementation(project(":jadx-plugins:jadx-dex-input"))
@@ -110,7 +110,7 @@ public class JadxArgs implements Closeable {
/**
* List of classes and packages (ends with '.*') to exclude from deobfuscation
*/
private List<String> deobfuscationWhitelist = DeobfWhitelist.DEFAULT_LIST;
private List<String> deobfuscationWhitelist = new ArrayList<>(DeobfWhitelist.DEFAULT_LIST);
/**
* Nodes alias provider for deobfuscator and rename visitor
@@ -52,7 +52,6 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.tasks.TaskExecutor;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.ProtoXMLParser;
import jadx.core.xmlgen.ResourcesSaver;
/**
@@ -94,10 +93,10 @@ public final class JadxDecompiler implements Closeable {
private List<ResourceFile> resources;
private BinaryXMLParser binaryXmlParser;
private ProtoXMLParser protoXmlParser;
private final IDecompileScheduler decompileScheduler = new DecompilerScheduler();
private final JadxEventsImpl events = new JadxEventsImpl();
private final ResourcesLoader resourcesLoader = new ResourcesLoader(this);
private final List<ICodeLoader> customCodeLoaders = new ArrayList<>();
private final List<CustomResourcesLoader> customResourcesLoaders = new ArrayList<>();
@@ -119,12 +118,11 @@ public final class JadxDecompiler implements Closeable {
loadInputFiles();
root = new RootNode(args);
root.init();
root.setDecompilerRef(this);
root.mergePasses(customPasses);
root.loadClasses(loadedInputs);
root.initClassPath();
root.loadResources(getResources());
root.loadResources(resourcesLoader, getResources());
root.runPreDecompileStage();
root.initPasses();
loadFinished();
@@ -170,7 +168,6 @@ public final class JadxDecompiler implements Closeable {
classes = null;
resources = null;
binaryXmlParser = null;
protoXmlParser = null;
events.reset();
}
@@ -430,7 +427,7 @@ public final class JadxDecompiler implements Closeable {
if (root == null) {
return Collections.emptyList();
}
resources = new ResourcesLoader(this).load();
resources = resourcesLoader.load(root);
}
return resources;
}
@@ -476,13 +473,6 @@ public final class JadxDecompiler implements Closeable {
return binaryXmlParser;
}
synchronized ProtoXMLParser getProtoXmlParser() {
if (protoXmlParser == null) {
protoXmlParser = new ProtoXMLParser(root);
}
return protoXmlParser;
}
/**
* Get JavaClass by ClassNode without loading and decompilation
*/
@@ -704,6 +694,10 @@ public final class JadxDecompiler implements Closeable {
customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass);
}
public ResourcesLoader getResourcesLoader() {
return resourcesLoader;
}
@Override
public String toString() {
return "jadx decompiler " + getVersion();
@@ -17,30 +17,42 @@ import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile.ZipRef;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.CustomResourcesLoader;
import jadx.api.plugins.resources.IResContainerFactory;
import jadx.api.plugins.resources.IResTableParserProvider;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.IResTableParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResProtoParser;
import jadx.core.xmlgen.ResTableParser;
import jadx.core.xmlgen.ResTableBinaryParserProvider;
import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE;
import static jadx.core.utils.files.FileUtils.copyStream;
// TODO: move to core package
public final class ResourcesLoader {
public final class ResourcesLoader implements IResourcesLoader {
private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class);
private final JadxDecompiler jadxRef;
private final List<IResTableParserProvider> resTableParserProviders = new ArrayList<>();
private final List<IResContainerFactory> resContainerFactories = new ArrayList<>();
private BinaryXMLParser binaryXmlParser;
ResourcesLoader(JadxDecompiler jadxRef) {
this.jadxRef = jadxRef;
this.resTableParserProviders.add(new ResTableBinaryParserProvider());
}
List<ResourceFile> load() {
List<ResourceFile> load(RootNode root) {
init(root);
List<File> inputFiles = jadxRef.getArgs().getInputFiles();
List<ResourceFile> list = new ArrayList<>(inputFiles.size());
for (File file : inputFiles) {
@@ -49,10 +61,37 @@ public final class ResourcesLoader {
return list;
}
private void init(RootNode root) {
for (IResTableParserProvider resTableParserProvider : resTableParserProviders) {
try {
resTableParserProvider.init(root);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to init res table provider: " + resTableParserProvider);
}
}
for (IResContainerFactory resContainerFactory : resContainerFactories) {
try {
resContainerFactory.init(root);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to init res container factory: " + resContainerFactory);
}
}
}
public interface ResourceDecoder<T> {
T decode(long size, InputStream is) throws IOException;
}
@Override
public void addResContainerFactory(IResContainerFactory resContainerFactory) {
resContainerFactories.add(resContainerFactory);
}
@Override
public void addResTableParserProvider(IResTableParserProvider resTableParserProvider) {
resTableParserProviders.add(resTableParserProvider);
}
public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) throws JadxException {
try {
ZipRef zipRef = rf.getZipRef();
@@ -82,7 +121,8 @@ public final class ResourcesLoader {
static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) {
try {
return decodeStream(rf, (size, is) -> loadContent(jadxRef, rf, is));
ResourcesLoader resLoader = jadxRef.getResourcesLoader();
return decodeStream(rf, (size, is) -> resLoader.loadContent(rf, is));
} catch (JadxException e) {
LOG.error("Decode error", e);
ICodeWriter cw = jadxRef.getRoot().makeCodeWriter();
@@ -92,36 +132,48 @@ public final class ResourcesLoader {
}
}
private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf,
InputStream inputStream) throws IOException {
RootNode root = jadxRef.getRoot();
switch (rf.getType()) {
case MANIFEST:
case XML: {
ICodeInfo content;
if (root.isProto()) {
content = jadxRef.getProtoXmlParser().parse(inputStream);
} else {
content = jadxRef.getBinaryXmlParser().parse(inputStream);
}
return ResContainer.textResource(rf.getDeobfName(), content);
private ResContainer loadContent(ResourceFile resFile, InputStream inputStream) throws IOException {
for (IResContainerFactory customFactory : resContainerFactories) {
ResContainer resContainer = customFactory.create(resFile, inputStream);
if (resContainer != null) {
return resContainer;
}
}
switch (resFile.getType()) {
case MANIFEST:
case XML:
ICodeInfo content = loadBinaryXmlParser().parse(inputStream);
return ResContainer.textResource(resFile.getDeobfName(), content);
case ARSC:
if (root.isProto()) {
return new ResProtoParser(root).decodeFiles(inputStream);
} else {
return new ResTableParser(root).decodeFiles(inputStream);
}
return decodeTable(resFile, inputStream).decodeFiles();
case IMG:
return decodeImage(rf, inputStream);
return decodeImage(resFile, inputStream);
default:
return ResContainer.resourceFileLink(rf);
return ResContainer.resourceFileLink(resFile);
}
}
public IResTableParser decodeTable(ResourceFile resFile, InputStream is) throws IOException {
if (resFile.getType() != ResourceType.ARSC) {
throw new IllegalArgumentException("Unexpected resource type for decode: " + resFile.getType() + ", expect '.pb'/'.arsc'");
}
IResTableParser parser = null;
for (IResTableParserProvider provider : resTableParserProviders) {
parser = provider.getParser(resFile);
if (parser != null) {
break;
}
}
if (parser == null) {
throw new JadxRuntimeException("Unknown type of resource file: " + resFile.getOriginalName());
}
parser.decode(is);
return parser;
}
private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
String name = rf.getOriginalName();
if (name.endsWith(".9.png")) {
@@ -184,4 +236,11 @@ public final class ResourcesLoader {
copyStream(is, baos);
return new SimpleCodeInfo(baos.toString("UTF-8"));
}
private synchronized BinaryXMLParser loadBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(jadxRef.getRoot());
}
return binaryXmlParser;
}
}
@@ -12,6 +12,7 @@ import jadx.api.plugins.gui.JadxGuiContext;
import jadx.api.plugins.input.JadxCodeInput;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.pass.JadxPass;
import jadx.api.plugins.resources.IResourcesLoader;
public interface JadxPluginContext {
@@ -32,6 +33,11 @@ public interface JadxPluginContext {
*/
void registerInputsHashSupplier(Supplier<String> supplier);
/**
* Customize resource loading
*/
IResourcesLoader getResourcesLoader();
/**
* Access to jadx-gui specific methods
*/
@@ -64,6 +64,14 @@ public abstract class BasePluginOptionsBuilder implements JadxPluginOptions {
.parser(v -> v));
}
public OptionBuilder<Integer> intOption(String name) {
return addOption(
new OptionData<Integer>(name)
.type(OptionType.NUMBER)
.formatter(Object::toString)
.parser(Integer::parseInt));
}
public <E extends Enum<?>> OptionBuilder<E> enumOption(String name, E[] values, Function<String, E> valueOf) {
return addOption(
new OptionData<E>(name)
@@ -0,0 +1,33 @@
package jadx.api.plugins.resources;
import java.io.IOException;
import java.io.InputStream;
import org.jetbrains.annotations.Nullable;
import jadx.api.ResourceFile;
import jadx.core.dex.nodes.RootNode;
import jadx.core.xmlgen.ResContainer;
/**
* Factory for {@link ResContainer}. Can be used in plugins via
* {@code context.getResourcesLoader().addResContainerFactory()} to implement content parsing in
* files with
* different formats.
*/
public interface IResContainerFactory {
/**
* Optional init method
*/
default void init(RootNode root) {
}
/**
* Checks if resource file is of expected format and tries to parse its content.
*
* @return {@link ResContainer} if file is of expected format, {@code null} otherwise.
*/
@Nullable
ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException;
}
@@ -0,0 +1,30 @@
package jadx.api.plugins.resources;
import org.jetbrains.annotations.Nullable;
import jadx.api.ResourceFile;
import jadx.core.dex.nodes.RootNode;
import jadx.core.xmlgen.IResTableParser;
/**
* Provides the resource table parser instance for specific resource table file format. Can be used
* in plugins via {@code context.getResourcesLoader().addResTableParserProvider()} to parse
* resources from tables
* in different formats.
*/
public interface IResTableParserProvider {
/**
* Optional init method
*/
default void init(RootNode root) {
}
/**
* Checks a file format and provides the instance if the format is expected.
*
* @return {@link IResTableParser} if resource table is of expected format, {@code null} otherwise.
*/
@Nullable
IResTableParser getParser(ResourceFile resFile);
}
@@ -0,0 +1,8 @@
package jadx.api.plugins.resources;
public interface IResourcesLoader {
void addResContainerFactory(IResContainerFactory resContainerFactory);
void addResTableParserProvider(IResTableParserProvider resTableParserProvider);
}
@@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.CommentsLevel;
import jadx.api.JadxArgs;
import jadx.core.deobf.DeobfuscatorVisitor;
import jadx.core.deobf.InitRenameProviders;
import jadx.core.deobf.SaveDeobfMapping;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.visitors.AnonymousClassVisitor;
@@ -102,6 +103,7 @@ public class Jadx {
passes.add(new CollectConstValues());
// rename and deobfuscation
passes.add(new InitRenameProviders());
passes.add(new DeobfuscatorVisitor());
passes.add(new SourceFileRename());
passes.add(new RenameVisitor());
@@ -0,0 +1,20 @@
package jadx.core.deobf;
import jadx.api.JadxArgs;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.utils.exceptions.JadxException;
public class InitRenameProviders extends AbstractVisitor {
@Override
public void init(RootNode root) throws JadxException {
JadxArgs args = root.getArgs();
if (args.isDeobfuscationOn() || !args.getRenameFlags().isEmpty()) {
args.getAliasProvider().init(root);
}
if (args.isDeobfuscationOn()) {
args.getRenameCondition().init(root);
}
}
}
@@ -5,39 +5,81 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
public class DeobfWhitelist extends AbstractDeobfCondition {
private static final Logger LOG = LoggerFactory.getLogger(DeobfWhitelist.class);
public static final List<String> DEFAULT_LIST = Arrays.asList(
"android.support.v4.*",
"android.support.v7.*",
"android.support.v4.os.*",
"android.support.annotation.Px",
"android.support.*",
"android.os.*",
"androidx.core.os.*",
"androidx.annotation.Px");
"androidx.annotation.*");
public static final String DEFAULT_STR = Utils.listToString(DEFAULT_LIST, " ");
private final Set<String> packages = new HashSet<>();
private final Set<String> classes = new HashSet<>();
private final Set<ClassNode> classes = new HashSet<>();
private boolean reportMissingItems = false;
@Override
public void init(RootNode root) {
packages.clear();
classes.clear();
for (String whitelistItem : root.getArgs().getDeobfuscationWhitelist()) {
if (!whitelistItem.isEmpty()) {
if (whitelistItem.endsWith(".*")) {
packages.add(whitelistItem.substring(0, whitelistItem.length() - 2));
} else {
classes.add(whitelistItem);
}
List<String> excludeList = root.getArgs().getDeobfuscationWhitelist();
reportMissingItems = !excludeList.equals(DEFAULT_LIST);
for (String name : excludeList) {
if (name.isEmpty()) {
continue;
}
if (name.endsWith(".*")) {
excludePackage(root, name.substring(0, name.length() - 2));
} else {
excludeClass(root, name);
}
}
LOG.debug("Excluded from deobfuscation: {} packages, {} classes", packages.size(), classes.size());
}
private void excludeClass(RootNode root, String clsFullName) {
ClassNode cls = root.resolveClass(clsFullName);
if (cls == null) {
if (reportMissingItems) {
LOG.info("Can't exclude from deobfuscation: class '{}' not found", clsFullName);
}
return;
}
excludeClsNode(cls);
}
private void excludeClsNode(ClassNode cls) {
classes.add(cls);
cls.addInfoComment("Class excluded from deobfuscation");
}
private void excludePackage(RootNode root, String fullPkgName) {
PackageNode pkg = root.resolvePackage(fullPkgName);
if (pkg == null) {
if (reportMissingItems) {
LOG.info("Can't exclude from deobfuscation: package '{}' not found", fullPkgName);
}
return;
}
excludePkgNode(pkg);
}
private void excludePkgNode(PackageNode pkg) {
packages.add(pkg.getFullName());
pkg.getClasses().forEach(this::excludeClsNode);
pkg.getSubPackages().forEach(this::excludePkgNode);
}
@Override
@@ -50,9 +92,19 @@ public class DeobfWhitelist extends AbstractDeobfCondition {
@Override
public Action check(ClassNode cls) {
if (classes.contains(cls.getClassInfo().getFullName())) {
if (classes.contains(cls)) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
@Override
public Action check(FieldNode fld) {
return check(fld.getParentClass());
}
@Override
public Action check(MethodNode mth) {
return check(mth.getParentClass());
}
}
@@ -54,9 +54,8 @@ import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.android.AndroidResourcesUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.IResParser;
import jadx.core.xmlgen.IResTableParser;
import jadx.core.xmlgen.ManifestAttributes;
import jadx.core.xmlgen.ResDecoder;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
@@ -93,7 +92,6 @@ public class RootNode {
private String appPackage;
@Nullable
private ClassNode appResClass;
private boolean isProto;
/**
* Optional decompiler reference
@@ -109,16 +107,6 @@ public class RootNode {
this.typeUpdate = new TypeUpdate(this);
this.methodUtils = new MethodUtils(this);
this.typeUtils = new TypeUtils(this);
this.isProto = args.getInputFiles().size() > 0 && args.getInputFiles().get(0).getName().toLowerCase().endsWith(".aab");
}
public void init() {
if (args.isDeobfuscationOn() || !args.getRenameFlags().isEmpty()) {
args.getAliasProvider().init(this);
}
if (args.isDeobfuscationOn()) {
args.getRenameCondition().init(this);
}
}
public void loadClasses(List<ICodeLoader> loadedInputs) {
@@ -203,25 +191,25 @@ public class RootNode {
rawClsMap.put(clsNode.getRawName(), clsNode);
}
public void loadResources(List<ResourceFile> resources) {
public void loadResources(ResourcesLoader resLoader, List<ResourceFile> resources) {
ResourceFile arsc = getResourceFile(resources);
if (arsc == null) {
LOG.debug("'.arsc' file not found");
LOG.debug("'resources.arsc' or 'resources.pb' file not found");
return;
}
try {
IResParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> ResDecoder.decode(this, arsc, is));
IResTableParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> resLoader.decodeTable(arsc, is));
if (parser != null) {
processResources(parser.getResStorage());
updateObfuscatedFiles(parser, resources);
updateManifestAttribMap(parser);
}
} catch (Exception e) {
LOG.error("Failed to parse '.arsc' file", e);
LOG.error("Failed to parse 'resources.pb'/'.arsc' file", e);
}
}
private void updateManifestAttribMap(IResParser parser) {
private void updateManifestAttribMap(IResTableParser parser) {
ManifestAttributes manifestAttributes = ManifestAttributes.getInstance();
manifestAttributes.updateAttributes(parser);
}
@@ -257,7 +245,7 @@ public class RootNode {
}
}
private void updateObfuscatedFiles(IResParser parser, List<ResourceFile> resources) {
private void updateObfuscatedFiles(IResTableParser parser, List<ResourceFile> resources) {
if (args.isSkipResources()) {
return;
}
@@ -715,10 +703,6 @@ public class RootNode {
return attributes;
}
public boolean isProto() {
return isProto;
}
public GradleInfoStorage getGradleInfoStorage() {
return gradleInfoStorage;
}
@@ -26,6 +26,7 @@ import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.options.OptionDescription;
import jadx.api.plugins.options.OptionFlag;
import jadx.api.plugins.pass.JadxPass;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
@@ -131,6 +132,11 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData,
return decompiler.events();
}
@Override
public IResourcesLoader getResourcesLoader() {
return decompiler.getResourcesLoader();
}
@Override
public @Nullable JadxGuiContext getGuiContext() {
return guiContext;
@@ -3,10 +3,12 @@ package jadx.core.xmlgen;
import java.io.IOException;
import java.io.InputStream;
public interface IResParser {
public interface IResTableParser {
void decode(InputStream inputStream) throws IOException;
ResContainer decodeFiles();
ResourceStorage getResStorage();
BinaryXMLStrings getStrings();
@@ -199,13 +199,18 @@ public class ManifestAttributes {
return null;
}
public void updateAttributes(IResParser parser) {
public void updateAttributes(IResTableParser parser) {
appAttrMap.clear();
ResourceStorage resStorage = parser.getResStorage();
ValuesParser vp = new ValuesParser(parser.getStrings(), resStorage.getResourcesNames());
for (ResourceEntry ri : resStorage.getResources()) {
if (ri.getProtoValue() != null) {
// Aapt proto decoder resolves attributes by itself.
continue;
}
if (ri.getTypeName().equals("attr") && ri.getNamedValues().size() > 1) {
RawNamedValue first = ri.getNamedValues().get(0);
MAttrType attrTyp;
@@ -1,31 +1,4 @@
package jadx.core.xmlgen;
import java.io.IOException;
import java.io.InputStream;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class ResDecoder {
public static IResParser decode(RootNode root, ResourceFile resFile, InputStream is) throws IOException {
if (resFile.getType() != ResourceType.ARSC) {
throw new IllegalArgumentException("Unexpected resource type for decode: " + resFile.getType() + ", expect ARSC");
}
IResParser parser = null;
String fileName = resFile.getOriginalName();
if (fileName.endsWith(".arsc")) {
parser = new ResTableParser(root);
}
if (fileName.endsWith(".pb")) {
parser = new ResProtoParser(root);
}
if (parser == null) {
throw new JadxRuntimeException("Unknown type of resource file: " + fileName);
}
parser.decode(is);
return parser;
}
}
@@ -32,8 +32,8 @@ import jadx.core.xmlgen.entry.RawValue;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
public class ResTableParser extends CommonBinaryParser implements IResParser {
private static final Logger LOG = LoggerFactory.getLogger(ResTableParser.class);
public class ResTableBinaryParser extends CommonBinaryParser implements IResTableParser {
private static final Logger LOG = LoggerFactory.getLogger(ResTableBinaryParser.class);
private static final Pattern VALID_RES_KEY_PATTERN = Pattern.compile("[\\w\\d_]+");
@@ -75,11 +75,11 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
private final ResourceStorage resStorage = new ResourceStorage();
private BinaryXMLStrings strings;
public ResTableParser(RootNode root) {
public ResTableBinaryParser(RootNode root) {
this(root, false);
}
public ResTableParser(RootNode root, boolean useRawResNames) {
public ResTableBinaryParser(RootNode root, boolean useRawResNames) {
this.root = root;
this.useRawResName = useRawResNames;
}
@@ -96,9 +96,8 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
}
}
public ResContainer decodeFiles(InputStream inputStream) throws IOException {
decode(inputStream);
@Override
public ResContainer decodeFiles() {
ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames());
ResXmlGen resGen = new ResXmlGen(resStorage, vp);
@@ -0,0 +1,25 @@
package jadx.core.xmlgen;
import org.jetbrains.annotations.Nullable;
import jadx.api.ResourceFile;
import jadx.api.plugins.resources.IResTableParserProvider;
import jadx.core.dex.nodes.RootNode;
public class ResTableBinaryParserProvider implements IResTableParserProvider {
private IResTableParser parser;
@Override
public void init(RootNode root) {
parser = new ResTableBinaryParser(root);
}
@Override
public synchronized @Nullable IResTableParser getParser(ResourceFile resFile) {
String fileName = resFile.getOriginalName();
if (!fileName.endsWith(".arsc")) {
return null;
}
return parser;
}
}
@@ -1,9 +1,14 @@
package jadx.tests.integration.deobf.a;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.api.deobf.IDeobfCondition;
import jadx.api.deobf.impl.CombineDeobfConditions;
import jadx.core.deobf.conditions.AvoidClsAndPkgNamesCollision;
import jadx.core.deobf.conditions.JadxRenameConditions;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@@ -34,6 +39,11 @@ public class TestNegativeRenameCondition extends IntegrationTest {
// disable all renaming options
args.setRenameFlags(Collections.emptySet());
// disable rename by collision between class and package names
List<IDeobfCondition> list = JadxRenameConditions.buildDefaultDeobfConditions();
list.removeIf(c -> c.getClass().equals(AvoidClsAndPkgNamesCollision.class));
args.setRenameCondition(CombineDeobfConditions.combine(list));
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("renamed from")
+3 -1
View File
@@ -10,6 +10,7 @@ dependencies {
implementation(project(":jadx-core"))
implementation(project(":jadx-cli"))
implementation(project(":jadx-plugins-tools"))
implementation(project(":jadx-commons:jadx-app-commons"))
// import mappings
implementation(project(":jadx-plugins:jadx-rename-mappings"))
@@ -26,7 +27,6 @@ dependencies {
implementation("org.jcommander:jcommander:1.83")
implementation("ch.qos.logback:logback-classic:1.5.6")
implementation("dev.dirs:directories:26")
implementation("com.fifesoft:rsyntaxtextarea:3.4.0")
implementation(files("libs/jfontchooser-1.0.5.jar"))
@@ -85,6 +85,7 @@ tasks.jar {
}
tasks.shadowJar {
isZip64 = true
mergeServiceFiles()
manifest {
from(project.tasks.jar.get().manifest)
@@ -111,6 +112,7 @@ launch4j {
windowTitle.set("jadx")
companyName.set("jadx")
jreMinVersion.set("11")
chdir.set("")
jvmOptions.set(application.applicationDefaultJvmArgs.toSet())
requires64Bit.set(true)
initialHeapPercent.set(5)
@@ -35,6 +35,7 @@ import jadx.api.args.ResourceNameSource;
import jadx.api.args.UserRenamesMappingsMode;
import jadx.cli.JadxCLIArgs;
import jadx.cli.LogHelper;
import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.gui.cache.code.CodeCacheMode;
import jadx.gui.cache.usage.UsageCacheMode;
import jadx.gui.settings.data.ShortcutsWrapper;
@@ -53,7 +54,7 @@ public class JadxSettings extends JadxCLIArgs {
private static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
private static final int RECENT_PROJECTS_COUNT = 30;
private static final int CURRENT_SETTINGS_VERSION = 20;
private static final int CURRENT_SETTINGS_VERSION = 21;
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
@@ -805,6 +806,10 @@ public class JadxSettings extends JadxCLIArgs {
tabDndGhostType = TabDndGhostType.OUTLINE;
fromVersion++;
}
if (fromVersion == 20) {
deobfuscationWhitelistStr = DeobfWhitelist.DEFAULT_STR;
fromVersion++;
}
if (fromVersion != CURRENT_SETTINGS_VERSION) {
LOG.warn("Incorrect settings upgrade. Expected version: {}, got: {}", CURRENT_SETTINGS_VERSION, fromVersion);
}
@@ -15,7 +15,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.ResourcesLoader;
import jadx.core.utils.exceptions.JadxException;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.UiUtils;
@@ -48,7 +51,18 @@ public class HexArea extends AbstractCodeArea {
@Override
public void load() {
byte[] bytes = binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.UTF_8);
byte[] bytes = null;
if (binaryNode instanceof JResource) {
JResource jResource = ((JResource) binaryNode);
try {
bytes = ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes());
} catch (JadxException e) {
LOG.error("Failed to directly load resource binary data {}: {}", jResource.getName(), e.getMessage());
}
}
if (bytes == null) {
bytes = binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.UTF_8);
}
setBytes(bytes);
if (getBytes().length > 0) {
// We set the caret after the first byte to prevent it from being highlighted
@@ -83,7 +83,8 @@ public class FileDialogWrapper {
case ADD:
title = NLS.str("file.add_files_action");
fileExtList = OPEN_FILES_EXTS;
fileExtList = new ArrayList<>(OPEN_FILES_EXTS);
fileExtList.add("aab");
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
currentDir = mainWindow.getSettings().getLastOpenFilePath();
isOpen = true;
@@ -1,24 +1,15 @@
package jadx.gui.utils.files;
import java.nio.file.Path;
import java.nio.file.Paths;
import dev.dirs.ProjectDirectories;
import jadx.core.utils.files.FileUtils;
import jadx.commons.app.JadxCommonFiles;
public class JadxFiles {
private static final ProjectDirectories DIRS = ProjectDirectories.from("io.github", "skylot", "jadx");
private static final String CONFIG_DIR = DIRS.configDir;
private static final Path CONFIG_DIR = JadxCommonFiles.getConfigDir();
public static final Path GUI_CONF = CONFIG_DIR.resolve("gui.json");
public static final Path CACHES_LIST = CONFIG_DIR.resolve("caches.json");
public static final Path GUI_CONF = Paths.get(CONFIG_DIR, "gui.json");
public static final Path CACHES_LIST = Paths.get(CONFIG_DIR, "caches.json");
public static final Path CACHE_DIR = Paths.get(DIRS.cacheDir);
public static final Path CACHE_DIR = JadxCommonFiles.getCacheDir();
public static final Path PROJECTS_CACHE_DIR = CACHE_DIR.resolve("projects");
static {
FileUtils.makeDirs(Paths.get(CONFIG_DIR));
}
}
+2 -1
View File
@@ -5,6 +5,7 @@ plugins {
dependencies {
api(project(":jadx-core"))
implementation("dev.dirs:directories:26")
implementation(project(":jadx-commons:jadx-app-commons"))
implementation("com.google.code.gson:gson:2.10.1")
}
@@ -1,22 +1,19 @@
package jadx.plugins.tools.utils;
import java.nio.file.Path;
import java.nio.file.Paths;
import dev.dirs.ProjectDirectories;
import jadx.commons.app.JadxCommonFiles;
import static jadx.core.utils.files.FileUtils.makeDirs;
public class PluginFiles {
private static final ProjectDirectories DIRS = ProjectDirectories.from("io.github", "skylot", "jadx");
private static final Path PLUGINS_DIR = Paths.get(DIRS.configDir, "plugins");
private static final Path PLUGINS_DIR = JadxCommonFiles.getConfigDir().resolve("plugins");
public static final Path PLUGINS_JSON = PLUGINS_DIR.resolve("plugins.json");
public static final Path INSTALLED_DIR = PLUGINS_DIR.resolve("installed");
public static final Path DROPINS_DIR = PLUGINS_DIR.resolve("dropins");
private static final Path CACHE_DIR = Paths.get(DIRS.cacheDir);
public static final Path PLUGINS_LIST_CACHE = CACHE_DIR.resolve("plugin-list.json");
public static final Path PLUGINS_LIST_CACHE = JadxCommonFiles.getCacheDir().resolve("plugin-list.json");
static {
makeDirs(INSTALLED_DIR);
@@ -0,0 +1,26 @@
plugins {
id("jadx-library")
}
dependencies {
compileOnly(project(":jadx-core"))
implementation("com.android.tools.build:aapt2-proto:8.3.2-10880808")
implementation("com.google.protobuf:protobuf-java") {
version {
require("3.25.3") // version 4 conflict with `aapt2-proto`
}
}
implementation("com.android.tools.build:bundletool:1.15.6") {
// All of this is unnecessary for parsing BundleConfig.pb except for protobuf
exclude(group = "com.android.tools.build")
exclude(group = "com.google.protobuf")
exclude(group = "com.google.guava")
exclude(group = "org.bitbucket.b_c")
exclude(group = "org.slf4j")
exclude(group = "com.google.auto.value")
exclude(group = "com.google.dagger")
exclude(group = "com.google.errorprone")
}
}
@@ -0,0 +1,32 @@
package jadx.plugins.input.aab;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginContext;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.resources.IResourcesLoader;
import jadx.plugins.input.aab.factories.ProtoBundleConfigResContainerFactory;
import jadx.plugins.input.aab.factories.ProtoTableResContainerFactory;
import jadx.plugins.input.aab.factories.ProtoXmlResContainerFactory;
public class AabInputPlugin implements JadxPlugin {
public static final String PLUGIN_ID = "aab-input";
@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo(
PLUGIN_ID,
".AAB Input",
"Loads .AAB files.");
}
@Override
public synchronized void init(JadxPluginContext context) {
IResourcesLoader resourcesLoader = context.getResourcesLoader();
ResTableProtoParserProvider tableParserProvider = new ResTableProtoParserProvider();
resourcesLoader.addResTableParserProvider(tableParserProvider);
resourcesLoader.addResContainerFactory(new ProtoTableResContainerFactory(tableParserProvider));
resourcesLoader.addResContainerFactory(new ProtoXmlResContainerFactory());
resourcesLoader.addResContainerFactory(new ProtoBundleConfigResContainerFactory());
}
}
@@ -0,0 +1,27 @@
package jadx.plugins.input.aab;
import org.jetbrains.annotations.Nullable;
import jadx.api.ResourceFile;
import jadx.api.plugins.resources.IResTableParserProvider;
import jadx.core.dex.nodes.RootNode;
import jadx.core.xmlgen.IResTableParser;
import jadx.plugins.input.aab.parsers.ResTableProtoParser;
public class ResTableProtoParserProvider implements IResTableParserProvider {
private ResTableProtoParser parser;
@Override
public void init(RootNode root) {
parser = new ResTableProtoParser(root);
}
@Override
public synchronized @Nullable IResTableParser getParser(ResourceFile resFile) {
String fileName = resFile.getOriginalName();
if (!fileName.endsWith("resources.pb")) {
return null;
}
return parser;
}
}
@@ -0,0 +1,27 @@
package jadx.plugins.input.aab.factories;
import java.io.IOException;
import java.io.InputStream;
import org.jetbrains.annotations.Nullable;
import com.android.bundle.Config.BundleConfig;
import jadx.api.ICodeInfo;
import jadx.api.ResourceFile;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.resources.IResContainerFactory;
import jadx.core.xmlgen.ResContainer;
public class ProtoBundleConfigResContainerFactory implements IResContainerFactory {
@Override
public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException {
if (!resFile.getOriginalName().endsWith("BundleConfig.pb")) {
return null;
}
BundleConfig bundleConfig = BundleConfig.parseFrom(inputStream);
ICodeInfo content = new SimpleCodeInfo(bundleConfig.toString());
return ResContainer.textResource(resFile.getDeobfName(), content);
}
}
@@ -0,0 +1,33 @@
package jadx.plugins.input.aab.factories;
import java.io.IOException;
import java.io.InputStream;
import org.jetbrains.annotations.Nullable;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.plugins.resources.IResContainerFactory;
import jadx.api.plugins.resources.IResTableParserProvider;
import jadx.core.xmlgen.IResTableParser;
import jadx.core.xmlgen.ResContainer;
public class ProtoTableResContainerFactory implements IResContainerFactory {
private final IResTableParserProvider provider;
public ProtoTableResContainerFactory(IResTableParserProvider provider) {
this.provider = provider;
}
@Override
public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException {
if (!resFile.getOriginalName().endsWith(".pb") || resFile.getType() != ResourceType.ARSC) {
return null;
}
IResTableParser parser = provider.getParser(resFile);
if (parser == null) {
return null;
}
return parser.decodeFiles();
}
}
@@ -0,0 +1,41 @@
package jadx.plugins.input.aab.factories;
import java.io.IOException;
import java.io.InputStream;
import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeInfo;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.plugins.resources.IResContainerFactory;
import jadx.core.dex.nodes.RootNode;
import jadx.core.xmlgen.ResContainer;
import jadx.plugins.input.aab.parsers.ResXmlProtoParser;
public class ProtoXmlResContainerFactory implements IResContainerFactory {
private ResXmlProtoParser xmlParser;
@Override
public void init(RootNode root) {
xmlParser = new ResXmlProtoParser(root);
}
@Override
public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException {
ResourceType type = resFile.getType();
if (type != ResourceType.XML && type != ResourceType.MANIFEST) {
return null;
}
ResourceFile.ZipRef ref = resFile.getZipRef();
if (ref == null) {
return null;
}
boolean isFromAab = ref.getZipFile().getPath().contains(".aab");
if (!isFromAab) {
return null;
}
ICodeInfo content = xmlParser.parse(inputStream);
return ResContainer.textResource(resFile.getDeobfName(), content);
}
}
@@ -1,4 +1,4 @@
package jadx.core.xmlgen;
package jadx.plugins.input.aab.parsers;
import java.util.ArrayList;
import java.util.List;
@@ -6,10 +6,12 @@ import java.util.List;
import com.android.aapt.ConfigurationOuterClass;
import com.android.aapt.Resources;
import jadx.core.xmlgen.ParserConstants;
import jadx.core.xmlgen.XmlGenUtils;
import jadx.core.xmlgen.entry.EntryConfig;
import jadx.core.xmlgen.entry.ProtoValue;
public class CommonProtoParser {
public class CommonProtoParser extends ParserConstants {
protected ProtoValue parse(Resources.Style s) {
List<ProtoValue> namedValues = new ArrayList<>(s.getEntryCount());
String parent = s.getParent().getName();
@@ -1,4 +1,4 @@
package jadx.core.xmlgen;
package jadx.plugins.input.aab.parsers;
import java.io.IOException;
import java.io.InputStream;
@@ -14,27 +14,24 @@ import com.android.aapt.Resources.Value;
import jadx.api.ICodeInfo;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.BinaryXMLStrings;
import jadx.core.xmlgen.IResTableParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResXmlGen;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.XmlGenUtils;
import jadx.core.xmlgen.entry.ProtoValue;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
public class ResProtoParser extends CommonProtoParser implements IResParser {
public class ResTableProtoParser extends CommonProtoParser implements IResTableParser {
private final RootNode root;
private final ResourceStorage resStorage = new ResourceStorage();
public ResProtoParser(RootNode root) {
public ResTableProtoParser(RootNode root) {
this.root = root;
}
public ResContainer decodeFiles(InputStream inputStream) throws IOException {
decode(inputStream);
ValuesParser vp = new ValuesParser(new BinaryXMLStrings(), resStorage.getResourcesNames());
ResXmlGen resGen = new ResXmlGen(resStorage, vp);
ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage);
List<ResContainer> xmlFiles = resGen.makeResourcesXml(root.getArgs());
return ResContainer.resourceTable("res", xmlFiles, content);
}
@Override
public void decode(InputStream inputStream) throws IOException {
ResourceTable table = ResourceTable.parseFrom(FileUtils.streamToByteArray(inputStream));
@@ -44,6 +41,15 @@ public class ResProtoParser extends CommonProtoParser implements IResParser {
resStorage.finish();
}
@Override
public synchronized ResContainer decodeFiles() {
ValuesParser vp = new ValuesParser(new BinaryXMLStrings(), resStorage.getResourcesNames());
ResXmlGen resGen = new ResXmlGen(resStorage, vp);
ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage);
List<ResContainer> xmlFiles = resGen.makeResourcesXml(root.getArgs());
return ResContainer.resourceTable("res", xmlFiles, content);
}
private void parse(Package p) {
String name = p.getPackageName();
resStorage.setAppPackage(name);
@@ -1,4 +1,4 @@
package jadx.core.xmlgen;
package jadx.plugins.input.aab.parsers;
import java.io.IOException;
import java.io.InputStream;
@@ -18,8 +18,11 @@ import jadx.api.ICodeWriter;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.StringUtils;
import jadx.core.utils.android.AndroidResourcesMap;
import jadx.core.xmlgen.XMLChar;
import jadx.core.xmlgen.XmlDeobf;
import jadx.core.xmlgen.XmlGenUtils;
public class ProtoXMLParser extends CommonProtoParser {
public class ResXmlProtoParser extends CommonProtoParser {
private Map<String, String> nsMap;
private final Map<String, String> tagAttrDeobfNames = new HashMap<>();
@@ -28,9 +31,11 @@ public class ProtoXMLParser extends CommonProtoParser {
private final RootNode rootNode;
private String currentTag;
private String appPackageName;
private final boolean isPrettyPrint;
public ProtoXMLParser(RootNode rootNode) {
public ResXmlProtoParser(RootNode rootNode) {
this.rootNode = rootNode;
this.isPrettyPrint = !rootNode.getArgs().isSkipXmlPrettyPrint();
}
public synchronized ICodeInfo parse(InputStream inputStream) throws IOException {
@@ -57,14 +62,9 @@ public class ProtoXMLParser extends CommonProtoParser {
tag = getValidTagAttributeName(tag);
currentTag = tag;
writer.startLine('<').add(tag);
for (int i = 0; i < e.getNamespaceDeclarationCount(); i++) {
decode(e.getNamespaceDeclaration(i));
}
Set<String> attrCache = new HashSet<>();
for (int i = 0; i < e.getAttributeCount(); i++) {
decode(e.getAttribute(i), attrCache);
}
decodeNamespaces(e);
decodeAttributes(e);
if (e.getChildCount() > 0) {
writer.add('>');
@@ -77,18 +77,67 @@ public class ProtoXMLParser extends CommonProtoParser {
writer.decIndent();
writer.startLine("</").add(tag).add('>');
} else {
writer.add("/>");
writer.add(" />");
}
}
private void decode(XmlAttribute a, Set<String> attrCache) {
private void decodeNamespaces(XmlElement e) {
int nsCount = e.getNamespaceDeclarationCount();
boolean newLine = nsCount != 1 && isPrettyPrint;
if (nsCount > 0) {
writer.add(' ');
}
for (int i = 0; i < nsCount; i++) {
decodeNamespace(e.getNamespaceDeclaration(i), newLine, i == nsCount - 1);
}
}
private void decodeNamespace(XmlNamespace n, boolean newLine, boolean isLastElement) {
String prefix = n.getPrefix();
String uri = n.getUri();
nsMap.put(uri, prefix);
writer.add("xmlns:").add(prefix).add("=\"").add(uri).add('"');
if (isLastElement) {
return;
}
if (newLine) {
writer.startLine().addIndent();
} else {
writer.add(' ');
}
}
private void decodeAttributes(XmlElement e) {
int attrsCount = e.getAttributeCount();
boolean newLine = attrsCount != 1 && isPrettyPrint;
if (attrsCount > 0) {
writer.add(' ');
if (isPrettyPrint) {
writer.startLine().addIndent();
}
}
Set<String> attrCache = new HashSet<>();
for (int i = 0; i < attrsCount; i++) {
decodeAttribute(e.getAttribute(i), attrCache, newLine, i == attrsCount - 1);
}
}
private void decodeAttribute(XmlAttribute a, Set<String> attrCache, boolean newLine, boolean isLastElement) {
String name = getAttributeFullName(a);
if (XmlDeobf.isDuplicatedAttr(name, attrCache)) {
return;
}
String value = deobfClassName(getAttributeValue(a));
writer.add(' ').add(name).add("=\"").add(StringUtils.escapeXML(value)).add('\"');
writer.add(name).add("=\"").add(StringUtils.escapeXML(value)).add('\"');
memorizePackageName(name, value);
if (isLastElement) {
return;
}
if (newLine) {
writer.startLine().addIndent();
} else {
writer.add(' ');
}
}
private String getAttributeFullName(XmlAttribute a) {
@@ -104,7 +153,7 @@ public class ProtoXMLParser extends CommonProtoParser {
int resId = a.getResourceId();
String str = AndroidResourcesMap.getResName(resId);
if (str != null) {
namespace = nsMap.get(ParserConstants.ANDROID_NS_URL);
namespace = nsMap.get(ANDROID_NS_URL);
// cut type before /
int typeEnd = str.indexOf('/');
if (typeEnd != -1) {
@@ -127,13 +176,6 @@ public class ProtoXMLParser extends CommonProtoParser {
return parse(a.getCompiledItem());
}
private void decode(XmlNamespace n) {
String prefix = n.getPrefix();
String uri = n.getUri();
nsMap.put(uri, prefix);
writer.add(" xmlns:").add(prefix).add("=\"").add(uri).add('"');
}
private void memorizePackageName(String attrName, String attrValue) {
if ("manifest".equals(currentTag) && "package".equals(attrName)) {
appPackageName = attrValue;
@@ -0,0 +1 @@
jadx.plugins.input.aab.AabInputPlugin
@@ -5,6 +5,7 @@ import java.io.InputStream;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
@@ -14,6 +15,7 @@ import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.input.ICodeLoader;
import jadx.api.plugins.input.data.impl.EmptyCodeLoader;
import jadx.api.plugins.utils.CommonFileUtils;
import jadx.plugins.input.dex.utils.IDexData;
public class DexInputPlugin implements JadxPlugin {
public static final String PLUGIN_ID = "dex-input";
@@ -57,4 +59,11 @@ public class DexInputPlugin implements JadxPlugin {
throw new DexException("Failed to read input stream", e);
}
}
public ICodeLoader loadDexData(List<IDexData> list) {
List<DexReader> readers = list.stream()
.map(data -> loader.loadDexReader(data.getFileName(), data.getContent()))
.collect(Collectors.toList());
return new DexLoadResult(readers, null);
}
}
@@ -0,0 +1,8 @@
package jadx.plugins.input.dex.utils;
public interface IDexData {
String getFileName();
byte[] getContent();
}
@@ -0,0 +1,28 @@
package jadx.plugins.input.dex.utils;
import java.util.Objects;
public class SimpleDexData implements IDexData {
private final String fileName;
private final byte[] content;
public SimpleDexData(String fileName, byte[] content) {
this.fileName = Objects.requireNonNull(fileName);
this.content = Objects.requireNonNull(content);
}
@Override
public String getFileName() {
return fileName;
}
@Override
public byte[] getContent() {
return content;
}
@Override
public String toString() {
return "DexData{" + fileName + ", size=" + content.length + '}';
}
}
@@ -5,6 +5,7 @@ plugins {
dependencies {
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
implementation(project(":jadx-commons:jadx-app-commons"))
implementation(kotlin("scripting-common"))
implementation(kotlin("scripting-jvm"))
@@ -12,8 +13,5 @@ dependencies {
implementation("io.github.oshai:kotlin-logging-jvm:6.0.9")
// path for scripts cache
implementation("dev.dirs:directories:26")
testImplementation(project(":jadx-core"))
}
@@ -1,6 +1,6 @@
package jadx.plugins.script
import dev.dirs.ProjectDirectories
import jadx.commons.app.JadxCommonFiles
import java.io.File
import java.security.MessageDigest
import kotlin.script.experimental.api.CompiledScript
@@ -61,8 +61,7 @@ class ScriptCache {
}
private fun getCacheDir(): File {
val dirs = ProjectDirectories.from("io.github", "skylot", "jadx")
val cacheBaseDir = File(dirs.cacheDir, "scripts")
val cacheBaseDir = JadxCommonFiles.getCacheDir().resolve("scripts").toFile()
cacheBaseDir.mkdirs()
return cacheBaseDir
}
@@ -1,79 +1,100 @@
package jadx.plugins.input.smali;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.android.tools.smali.smali.Smali;
import com.android.tools.smali.smali.SmaliOptions;
public class SmaliConvert implements Closeable {
import jadx.plugins.input.dex.utils.IDexData;
import jadx.plugins.input.dex.utils.SimpleDexData;
public class SmaliConvert {
private static final Logger LOG = LoggerFactory.getLogger(SmaliConvert.class);
@Nullable
private Path tmpDex;
private final List<IDexData> dexData = new ArrayList<>();
public boolean execute(List<Path> input) {
public boolean execute(List<Path> input, SmaliInputOptions options) {
List<Path> smaliFiles = filterSmaliFiles(input);
if (smaliFiles.isEmpty()) {
return false;
}
LOG.debug("Compiling smali files: {}", smaliFiles.size());
try {
this.tmpDex = Files.createTempFile("jadx-", ".dex");
if (compileSmali(tmpDex, smaliFiles)) {
return true;
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
collectSystemErrors(out, () -> compile(smaliFiles, options));
boolean success = out.size() == 0;
if (!success) {
LOG.error("Smali error:\n{}", out);
}
} catch (Exception e) {
LOG.error("Smali process error", e);
}
close();
return false;
return !dexData.isEmpty();
}
private static boolean compileSmali(Path output, List<Path> inputFiles) throws IOException {
SmaliOptions options = new SmaliOptions();
options.outputDexFile = output.toAbsolutePath().toString();
options.verboseErrors = true;
options.apiLevel = 27; // TODO: add as plugin option
@SuppressWarnings("ResultOfMethodCallIgnored")
private void compile(List<Path> inputFiles, SmaliInputOptions options) {
SmaliOptions smaliOptions = new SmaliOptions();
smaliOptions.apiLevel = options.getApiLevel();
smaliOptions.verboseErrors = true;
smaliOptions.allowOdexOpcodes = false;
smaliOptions.printTokens = false;
List<String> inputFileNames = inputFiles.stream()
.map(p -> p.toAbsolutePath().toString())
.distinct()
.collect(Collectors.toList());
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
boolean result = collectSystemErrors(out, () -> Smali.assemble(options, inputFileNames));
if (!result) {
LOG.error("Smali compilation error:\n{}", out);
int threads = options.getThreads();
LOG.debug("Compiling smali files: {}, threads: {}", inputFiles.size(), threads);
long start = System.currentTimeMillis();
if (threads == 1) {
for (Path inputFile : inputFiles) {
assemble(inputFile, smaliOptions);
}
return result;
} else {
try {
ExecutorService executor = Executors.newFixedThreadPool(threads);
for (Path inputFile : inputFiles) {
executor.execute(() -> assemble(inputFile, smaliOptions));
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
LOG.error("Smali compile interrupted", e);
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Smali compile done in: {}ms", System.currentTimeMillis() - start);
}
}
private static boolean collectSystemErrors(OutputStream out, Callable<Boolean> exec) {
private void assemble(Path inputFile, SmaliOptions smaliOptions) {
String fileName = inputFile.toAbsolutePath().toString();
try (Reader reader = Files.newBufferedReader(inputFile)) {
byte[] assemble = SmaliUtils.assemble(reader, smaliOptions);
dexData.add(new SimpleDexData(fileName, assemble));
} catch (Exception e) {
throw new RuntimeException("Fail to compile: " + fileName, e);
}
}
private static void collectSystemErrors(OutputStream out, Runnable exec) {
PrintStream systemErr = System.err;
try (PrintStream err = new PrintStream(out)) {
System.setErr(err);
try {
return exec.call();
exec.run();
} catch (Exception e) {
e.printStackTrace(err);
return false;
}
} finally {
System.setErr(systemErr);
@@ -87,21 +108,7 @@ public class SmaliConvert implements Closeable {
.collect(Collectors.toList());
}
public List<Path> getDexFiles() {
if (tmpDex == null) {
return Collections.emptyList();
}
return Collections.singletonList(tmpDex);
}
@Override
public void close() {
try {
if (tmpDex != null) {
Files.deleteIfExists(tmpDex);
}
} catch (Exception e) {
LOG.error("Failed to remove tmp dex file: {}", tmpDex, e);
}
public List<IDexData> getDexData() {
return dexData;
}
}
@@ -0,0 +1,29 @@
package jadx.plugins.input.smali;
import jadx.api.plugins.options.impl.BasePluginOptionsBuilder;
public class SmaliInputOptions extends BasePluginOptionsBuilder {
private int apiLevel;
private int threads; // use jadx global threads count option
@Override
public void registerOptions() {
intOption(SmaliInputPlugin.PLUGIN_ID + ".api-level")
.description("Android API level")
.defaultValue(27)
.setter(v -> apiLevel = v);
}
public int getApiLevel() {
return apiLevel;
}
public int getThreads() {
return threads;
}
public void setThreads(int threads) {
this.threads = threads;
}
}
@@ -3,26 +3,31 @@ package jadx.plugins.input.smali;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginContext;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.data.JadxPluginRuntimeData;
import jadx.api.plugins.input.data.impl.EmptyCodeLoader;
import jadx.plugins.input.dex.DexInputPlugin;
public class SmaliInputPlugin implements JadxPlugin {
public static final String PLUGIN_ID = "smali-input";
private final SmaliInputOptions options = new SmaliInputOptions();
@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo("smali-input", "Smali Input", "Load .smali files");
return new JadxPluginInfo(PLUGIN_ID, "Smali Input", "Load .smali files");
}
@Override
public void init(JadxPluginContext context) {
JadxPluginRuntimeData dexInput = context.plugins().getById(DexInputPlugin.PLUGIN_ID);
context.registerOptions(options);
options.setThreads(context.getArgs().getThreadsCount());
DexInputPlugin dexInput = context.plugins().getInstance(DexInputPlugin.class);
context.addCodeInput(input -> {
SmaliConvert convert = new SmaliConvert();
if (!convert.execute(input)) {
if (!convert.execute(input, options)) {
return EmptyCodeLoader.INSTANCE;
}
return dexInput.loadCodeFiles(convert.getDexFiles(), convert);
return dexInput.loadDexData(convert.getDexData());
});
}
}
@@ -0,0 +1,54 @@
package jadx.plugins.input.smali;
import java.io.IOException;
import java.io.Reader;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.TokenSource;
import org.antlr.runtime.tree.CommonTreeNodeStream;
import com.android.tools.smali.dexlib2.Opcodes;
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder;
import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore;
import com.android.tools.smali.smali.LexerErrorInterface;
import com.android.tools.smali.smali.SmaliOptions;
import com.android.tools.smali.smali.smaliFlexLexer;
import com.android.tools.smali.smali.smaliParser;
import com.android.tools.smali.smali.smaliTreeWalker;
/**
* Utility methods to assemble smali to in-memory buffer.
* This implementation uses smali library internal classes.
*/
public class SmaliUtils {
@SuppressWarnings("ExtractMethodRecommender")
public static byte[] assemble(Reader reader, SmaliOptions options) throws IOException, RecognitionException {
LexerErrorInterface lexer = new smaliFlexLexer(reader, options.apiLevel);
CommonTokenStream tokens = new CommonTokenStream((TokenSource) lexer);
smaliParser parser = new smaliParser(tokens);
parser.setVerboseErrors(options.verboseErrors);
parser.setAllowOdex(options.allowOdexOpcodes);
parser.setApiLevel(options.apiLevel);
smaliParser.smali_file_return parseResult = parser.smali_file();
if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) {
throw new RuntimeException("Parse error");
}
CommonTreeNodeStream treeStream = new CommonTreeNodeStream(parseResult.getTree());
treeStream.setTokenStream(tokens);
DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(options.apiLevel));
smaliTreeWalker dexGen = new smaliTreeWalker(treeStream);
dexGen.setApiLevel(options.apiLevel);
dexGen.setVerboseErrors(options.verboseErrors);
dexGen.setDexBuilder(dexBuilder);
dexGen.smali_file();
if (dexGen.getNumberOfSyntaxErrors() > 0) {
throw new RuntimeException("Compile error");
}
MemoryDataStore dataStore = new MemoryDataStore();
dexBuilder.writeTo(dataStore);
return dataStore.getData();
}
}
+3
View File
@@ -10,6 +10,8 @@ include("jadx-gui")
include("jadx-plugins-tools")
include("jadx-commons:jadx-app-commons")
include("jadx-plugins:jadx-input-api")
include("jadx-plugins:jadx-dex-input")
include("jadx-plugins:jadx-java-input")
@@ -19,6 +21,7 @@ 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-aab-input")
include("jadx-plugins:jadx-script:jadx-script-plugin")
include("jadx-plugins:jadx-script:jadx-script-runtime")