feat: move AAB support to separate plugin (PR #2165)

* wip: finished with factories

* wip: bundleconfig.pb

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

* wip: removed test apks

* wip: proto xml pretty print

* wip: fixed getNamedValues NPE

* minor fixes

* spotless

* enabled zip64 for gui shadow jar

* spotless

* spotless

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

* replace static methods with new API methods

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
Andrei Kudryavtsev 2024-04-27 01:54:54 +05:00 committed by GitHub
parent 37a42d1418
commit b85900aa3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 500 additions and 133 deletions

View File

@ -18,6 +18,7 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
implementation("org.jcommander:jcommander:1.83")
implementation("ch.qos.logback:logback-classic:1.5.6")

View File

@ -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())) {

View File

@ -7,14 +7,6 @@ dependencies {
implementation("com.google.code.gson:gson:2.10.1")
// TODO: move resources decoding to separate plugin module
implementation("com.android.tools.build:aapt2-proto:8.3.2-10880808")
implementation("com.google.protobuf:protobuf-java") {
version {
require("3.25.3") // version 4 conflict with `aapt2-proto`
}
}
testImplementation("org.apache.commons:commons-lang3:3.14.0")
testImplementation(project(":jadx-plugins:jadx-dex-input"))

View File

@ -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<>();
@ -124,7 +123,7 @@ public final class JadxDecompiler implements Closeable {
root.mergePasses(customPasses);
root.loadClasses(loadedInputs);
root.initClassPath();
root.loadResources(getResources());
root.loadResources(resourcesLoader, getResources());
root.runPreDecompileStage();
root.initPasses();
loadFinished();
@ -170,7 +169,6 @@ public final class JadxDecompiler implements Closeable {
classes = null;
resources = null;
binaryXmlParser = null;
protoXmlParser = null;
events.reset();
}
@ -430,7 +428,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 +474,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 +695,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();

View File

@ -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;
}
}

View File

@ -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
*/

View File

@ -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;
}

View File

@ -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);
}

View File

@ -0,0 +1,8 @@
package jadx.api.plugins.resources;
public interface IResourcesLoader {
void addResContainerFactory(IResContainerFactory resContainerFactory);
void addResTableParserProvider(IResTableParserProvider resTableParserProvider);
}

View File

@ -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,7 +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() {
@ -203,25 +200,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 +254,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 +712,6 @@ public class RootNode {
return attributes;
}
public boolean isProto() {
return isProto;
}
public GradleInfoStorage getGradleInfoStorage() {
return gradleInfoStorage;
}

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -85,6 +85,7 @@ tasks.jar {
}
tasks.shadowJar {
isZip64 = true
mergeServiceFiles()
manifest {
from(project.tasks.jar.get().manifest)

View File

@ -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;

View File

@ -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")
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1 @@
jadx.plugins.input.aab.AabInputPlugin

View File

@ -19,6 +19,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")