Skip to content

Commit

Permalink
feat: move AAB support to separate plugin (PR #2165)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
AndreiKud and skylot authored Apr 26, 2024
1 parent 37a42d1 commit b85900a
Show file tree
Hide file tree
Showing 29 changed files with 500 additions and 133 deletions.
1 change: 1 addition & 0 deletions jadx-cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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
Expand Down Expand Up @@ -54,7 +54,7 @@ public static void main(String[] args) throws IOException {
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())) {
Expand Down
8 changes: 0 additions & 8 deletions jadx-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
19 changes: 7 additions & 12 deletions jadx-core/src/main/java/jadx/api/JadxDecompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
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;

/**
Expand Down Expand Up @@ -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<>();
Expand All @@ -124,7 +123,7 @@ public void load() {
root.mergePasses(customPasses);
root.loadClasses(loadedInputs);
root.initClassPath();
root.loadResources(getResources());
root.loadResources(resourcesLoader, getResources());
root.runPreDecompileStage();
root.initPasses();
loadFinished();
Expand Down Expand Up @@ -170,7 +169,6 @@ private void reset() {
classes = null;
resources = null;
binaryXmlParser = null;
protoXmlParser = null;
events.reset();
}

Expand Down Expand Up @@ -430,7 +428,7 @@ public synchronized List<ResourceFile> getResources() {
if (root == null) {
return Collections.emptyList();
}
resources = new ResourcesLoader(this).load();
resources = resourcesLoader.load(root);
}
return resources;
}
Expand Down Expand Up @@ -476,13 +474,6 @@ synchronized BinaryXMLParser getBinaryXmlParser() {
return binaryXmlParser;
}

synchronized ProtoXMLParser getProtoXmlParser() {
if (protoXmlParser == null) {
protoXmlParser = new ProtoXMLParser(root);
}
return protoXmlParser;
}

/**
* Get JavaClass by ClassNode without loading and decompilation
*/
Expand Down Expand Up @@ -704,6 +695,10 @@ public void addCustomPass(JadxPass pass) {
customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass);
}

public ResourcesLoader getResourcesLoader() {
return resourcesLoader;
}

@Override
public String toString() {
return "jadx decompiler " + getVersion();
Expand Down
109 changes: 84 additions & 25 deletions jadx-core/src/main/java/jadx/api/ResourcesLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,42 @@
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) {
Expand All @@ -49,10 +61,37 @@ List<ResourceFile> load() {
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();
Expand Down Expand Up @@ -82,7 +121,8 @@ public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) th

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();
Expand All @@ -92,34 +132,46 @@ static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) {
}
}

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) {
Expand Down Expand Up @@ -184,4 +236,11 @@ public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
copyStream(is, baos);
return new SimpleCodeInfo(baos.toString("UTF-8"));
}

private synchronized BinaryXMLParser loadBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(jadxRef.getRoot());
}
return binaryXmlParser;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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 {

Expand All @@ -32,6 +33,11 @@ public interface JadxPluginContext {
*/
void registerInputsHashSupplier(Supplier<String> supplier);

/**
* Customize resource loading
*/
IResourcesLoader getResourcesLoader();

/**
* Access to jadx-gui specific methods
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package jadx.api.plugins.resources;

public interface IResourcesLoader {

void addResContainerFactory(IResContainerFactory resContainerFactory);

void addResTableParserProvider(IResTableParserProvider resTableParserProvider);
}
Loading

0 comments on commit b85900a

Please sign in to comment.