Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5640f3b931 | |||
| 02bc27e887 | |||
| 794e5adb7f | |||
| a81cec7701 | |||
| 09fa35f144 | |||
| f2a6a1e942 | |||
| b85900aa3d | |||
| 37a42d1418 | |||
| 07dde05337 | |||
| 8618214c7f | |||
| b80f32a36f | |||
| ce527ed753 |
@@ -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
|
||||
|
||||
@@ -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 
|
||||
```bash
|
||||
sudo pacman -S jadx
|
||||
```
|
||||
2. macOS 
|
||||
```bash
|
||||
brew install jadx
|
||||
```
|
||||
3. [Flathub ](https://flathub.org/apps/details/com.github.skylot.jadx)
|
||||
```bash
|
||||
flatpak install flathub com.github.skylot.jadx
|
||||
```
|
||||
- Arch Linux
|
||||
[](https://archlinux.org/packages/extra/any/jadx/)
|
||||
[](https://aur.archlinux.org/packages/jadx-git)
|
||||
```bash
|
||||
sudo pacman -S jadx
|
||||
```
|
||||
- macOS
|
||||
[](https://formulae.brew.sh/formula/jadx)
|
||||
```bash
|
||||
brew install jadx
|
||||
```
|
||||
- Flathub
|
||||
[](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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -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;
|
||||
}
|
||||
}
|
||||
+10
@@ -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")
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
+33
@@ -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();
|
||||
}
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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();
|
||||
+18
-12
@@ -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);
|
||||
+63
-21
@@ -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;
|
||||
+1
@@ -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();
|
||||
}
|
||||
+28
@@ -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"))
|
||||
}
|
||||
|
||||
+2
-3
@@ -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
|
||||
}
|
||||
|
||||
+58
-51
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -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;
|
||||
}
|
||||
}
|
||||
+10
-5
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user