feat: implement Android App Bundle support (#1129) (PR #1131)

* Implement proto parse
* fix code formatting
* fix tests with empty input
* revert not needed code style changes
* Implement parse of resources.pb for AAB

Co-authored-by: bagipro <bugi@MacBook-Pro-2.local>
Co-authored-by: Skylot <skylot@gmail.com>
This commit is contained in:
bagipro 2021-03-08 21:34:52 +03:00 committed by GitHub
parent 4e5fac4b88
commit 9ef99a2b92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 725 additions and 138 deletions

View File

@ -12,7 +12,7 @@
Command line and GUI tools for producing Java source code from Android Dex and Apk files
**Main features:**
- decompile Dalvik bytecode to java classes from APK, dex, aar and zip files
- decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files
- decode `AndroidManifest.xml` and other resources from `resources.arsc`
- deobfuscator included
@ -65,7 +65,7 @@ and also packed to `build/jadx-<version>.zip`
### Usage
```
jadx[-gui] [options] <input file> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc)
jadx[-gui] [options] <input file> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)
options:
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources

View File

@ -19,7 +19,7 @@ import jadx.core.utils.files.FileUtils;
public class JadxCLIArgs {
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc)")
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)")
protected List<String> files = new ArrayList<>(1);
@Parameter(names = { "-d", "--output-dir" }, description = "output directory")

View File

@ -6,6 +6,7 @@ dependencies {
api(project(':jadx-plugins:jadx-plugins-api'))
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.android.tools.build:aapt2-proto:4.1.2-6503028'
testImplementation 'org.apache.commons:commons-lang3:3.11'

View File

@ -40,6 +40,7 @@ import jadx.core.export.ExportGradleProject;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.ProtoXMLParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResourcesSaver;
@ -79,7 +80,8 @@ public final class JadxDecompiler implements Closeable {
private List<JavaClass> classes;
private List<ResourceFile> resources;
private BinaryXMLParser xmlParser;
private BinaryXMLParser binaryXmlParser;
private ProtoXMLParser protoXmlParser;
private final Map<ClassNode, JavaClass> classesMap = new ConcurrentHashMap<>();
private final Map<MethodNode, JavaMethod> methodsMap = new ConcurrentHashMap<>();
@ -122,7 +124,8 @@ public final class JadxDecompiler implements Closeable {
root = null;
classes = null;
resources = null;
xmlParser = null;
binaryXmlParser = null;
protoXmlParser = null;
classesMap.clear();
methodsMap.clear();
@ -341,11 +344,18 @@ public final class JadxDecompiler implements Closeable {
return root;
}
synchronized BinaryXMLParser getXmlParser() {
if (xmlParser == null) {
xmlParser = new BinaryXMLParser(root);
synchronized BinaryXMLParser getBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(root);
}
return xmlParser;
return binaryXmlParser;
}
synchronized ProtoXMLParser getProtoXmlParser() {
if (protoXmlParser == null) {
protoXmlParser = new ProtoXMLParser(root);
}
return protoXmlParser;
}
private void loadJavaClass(JavaClass javaClass) {

View File

@ -41,6 +41,9 @@ public enum ResourceType {
}
public static ResourceType getFileType(String fileName) {
if (fileName.matches("[^/]+/resources.pb")) {
return ARSC;
}
int dot = fileName.lastIndexOf('.');
if (dot != -1) {
String ext = fileName.substring(dot).toLowerCase(Locale.ROOT);

View File

@ -17,11 +17,13 @@ import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile.ZipRef;
import jadx.api.impl.SimpleCodeInfo;
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.files.FileUtils;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResProtoParser;
import jadx.core.xmlgen.ResTableParser;
import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE;
@ -91,14 +93,25 @@ 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 = jadxRef.getXmlParser().parse(inputStream);
case XML: {
ICodeInfo content;
if (root.isProto()) {
content = jadxRef.getProtoXmlParser().parse(inputStream);
} else {
content = jadxRef.getBinaryXmlParser().parse(inputStream);
}
return ResContainer.textResource(rf.getDeobfName(), content);
}
case ARSC:
return new ResTableParser(jadxRef.getRoot()).decodeFiles(inputStream);
if (root.isProto()) {
return new ResProtoParser(root).decodeFiles(inputStream);
} else {
return new ResTableParser(root).decodeFiles(inputStream);
}
case IMG:
return decodeImage(rf, inputStream);

View File

@ -71,6 +71,7 @@ public class RootNode {
private String appPackage;
@Nullable
private ClassNode appResClass;
private boolean isProto;
public RootNode(JadxArgs args) {
this.args = args;
@ -82,6 +83,7 @@ public class RootNode {
this.codeCache = args.getCodeCache();
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 loadClasses(List<ILoadResult> loadedInputs) {
@ -502,4 +504,8 @@ public class RootNode {
public TypeUtils getTypeUtils() {
return typeUtils;
}
public boolean isProto() {
return isProto;
}
}

View File

@ -0,0 +1,141 @@
package jadx.core.xmlgen;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import com.android.aapt.Resources.XmlAttribute;
import com.android.aapt.Resources.XmlElement;
import com.android.aapt.Resources.XmlNamespace;
import com.android.aapt.Resources.XmlNode;
import com.google.protobuf.InvalidProtocolBufferException;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.StringUtils;
public class ProtoXMLParser {
private Map<String, String> nsMap;
private final Map<String, String> tagAttrDeobfNames = new HashMap<>();
private ICodeWriter writer;
private final RootNode rootNode;
private String currentTag;
private String appPackageName;
public ProtoXMLParser(RootNode rootNode) {
this.rootNode = rootNode;
}
public synchronized ICodeInfo parse(InputStream inputStream) throws IOException {
nsMap = new HashMap<>();
writer = rootNode.makeCodeWriter();
writer.add("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
decode(decodeProto(inputStream));
nsMap = null;
return writer.finish();
}
private void decode(XmlNode n) throws IOException {
if (n.hasSource()) {
writer.attachSourceLine(n.getSource().getLineNumber());
}
writer.add(StringUtils.escapeXML(n.getText().trim()));
if (n.hasElement()) {
decode(n.getElement());
}
}
private void decode(XmlElement e) throws IOException {
String tag = deobfClassName(e.getName());
tag = getValidTagAttributeName(tag);
currentTag = tag;
writer.startLine('<').add(tag);
for (int i = 0; i < e.getNamespaceDeclarationCount(); i++) {
decode(e.getNamespaceDeclaration(i));
}
for (int i = 0; i < e.getAttributeCount(); i++) {
decode(e.getAttribute(i));
}
if (e.getChildCount() > 0) {
writer.add('>');
writer.incIndent();
for (int i = 0; i < e.getChildCount(); i++) {
Map<String, String> oldNsMap = new HashMap<>(nsMap);
decode(e.getChild(i));
nsMap = oldNsMap;
}
writer.decIndent();
writer.startLine("</").add(tag).add('>');
} else {
writer.add("/>");
}
}
private void decode(XmlAttribute a) {
writer.add(' ');
String namespace = a.getNamespaceUri();
if (!namespace.isEmpty()) {
writer.add(nsMap.get(namespace)).add(':');
}
String name = a.getName();
String value = deobfClassName(a.getValue());
writer.add(name).add("=\"").add(value).add('\"');
memorizePackageName(name, value);
}
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;
}
}
private String deobfClassName(String className) {
String newName = XmlDeobf.deobfClassName(rootNode, className, appPackageName);
if (newName != null) {
return newName;
}
return className;
}
private String getValidTagAttributeName(String originalName) {
if (XMLChar.isValidName(originalName)) {
return originalName;
}
if (tagAttrDeobfNames.containsKey(originalName)) {
return tagAttrDeobfNames.get(originalName);
}
String generated;
do {
generated = generateTagAttrName();
} while (tagAttrDeobfNames.containsValue(generated));
tagAttrDeobfNames.put(originalName, generated);
return generated;
}
private static String generateTagAttrName() {
final int length = 6;
Random r = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= length; i++) {
sb.append((char) (r.nextInt(26) + 'a'));
}
return sb.toString();
}
private XmlNode decodeProto(InputStream inputStream)
throws InvalidProtocolBufferException, IOException {
return XmlNode.parseFrom(XmlGenUtils.readData(inputStream));
}
}

View File

@ -0,0 +1,248 @@
package jadx.core.xmlgen;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import com.android.aapt.ConfigurationOuterClass.Configuration;
import com.android.aapt.Resources.Array;
import com.android.aapt.Resources.Attribute;
import com.android.aapt.Resources.CompoundValue;
import com.android.aapt.Resources.ConfigValue;
import com.android.aapt.Resources.Entry;
import com.android.aapt.Resources.Item;
import com.android.aapt.Resources.Package;
import com.android.aapt.Resources.Plural;
import com.android.aapt.Resources.Primitive;
import com.android.aapt.Resources.ResourceTable;
import com.android.aapt.Resources.Style;
import com.android.aapt.Resources.Styleable;
import com.android.aapt.Resources.Type;
import com.android.aapt.Resources.Value;
import com.google.protobuf.InvalidProtocolBufferException;
import jadx.api.ICodeInfo;
import jadx.core.dex.nodes.RootNode;
import jadx.core.xmlgen.entry.EntryConfig;
import jadx.core.xmlgen.entry.ProtoValue;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
public class ResProtoParser {
private final RootNode root;
private final ResourceStorage resStorage = new ResourceStorage();
public ResProtoParser(RootNode root) {
this.root = root;
}
public ResContainer decodeFiles(InputStream inputStream) throws IOException {
ResourceTable table = decodeProto(inputStream);
for (Package p : table.getPackageList()) {
parse(p);
}
resStorage.finish();
ValuesParser vp = new ValuesParser(new String[0], resStorage.getResourcesNames());
ResXmlGen resGen = new ResXmlGen(resStorage, vp);
ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage);
List<ResContainer> xmlFiles = resGen.makeResourcesXml();
return ResContainer.resourceTable("res", xmlFiles, content);
}
private void parse(Package p) {
String name = p.getPackageName();
resStorage.setAppPackage(name);
parse(name, p.getTypeList());
}
private void parse(String packageName, List<Type> types) {
for (Type type : types) {
String typeName = type.getName();
for (Entry entry : type.getEntryList()) {
int id = entry.getEntryId().getId();
String entryName = entry.getName();
for (ConfigValue configValue : entry.getConfigValueList()) {
String config = parse(configValue.getConfig());
ResourceEntry resEntry = new ResourceEntry(id, packageName, typeName, entryName, config);
resStorage.add(resEntry);
ProtoValue protoValue;
if (configValue.getValue().getValueCase() == Value.ValueCase.ITEM) {
protoValue = new ProtoValue(parse(configValue.getValue().getItem()));
} else {
protoValue = parse(configValue.getValue().getCompoundValue());
}
resEntry.setProtoValue(protoValue);
}
}
}
}
private ProtoValue parse(Style s) {
List<ProtoValue> namedValues = new ArrayList<>(s.getEntryCount());
String parent = s.getParent().getName();
if (parent.isEmpty()) {
parent = null;
} else {
parent = '@' + parent;
}
for (int i = 0; i < s.getEntryCount(); i++) {
Style.Entry entry = s.getEntry(i);
String name = entry.getKey().getName();
String value = parse(entry.getItem());
namedValues.add(new ProtoValue(value).setName(name));
}
return new ProtoValue().setNamedValues(namedValues).setParent(parent);
}
private ProtoValue parse(Styleable s) {
List<ProtoValue> namedValues = new ArrayList<>(s.getEntryCount());
for (int i = 0; i < s.getEntryCount(); i++) {
Styleable.Entry e = s.getEntry(i);
namedValues.add(new ProtoValue('@' + e.getAttr().getName()));
}
return new ProtoValue().setNamedValues(namedValues);
}
private ProtoValue parse(Array a) {
List<ProtoValue> namedValues = new ArrayList<>(a.getElementCount());
for (int i = 0; i < a.getElementCount(); i++) {
Array.Element e = a.getElement(i);
String value = parse(e.getItem());
namedValues.add(new ProtoValue(value));
}
return new ProtoValue().setNamedValues(namedValues);
}
private ProtoValue parse(Attribute a) {
String format = XmlGenUtils.getAttrTypeAsString(a.getFormatFlags());
List<ProtoValue> namedValues = new ArrayList<>(a.getSymbolCount());
for (int i = 0; i < a.getSymbolCount(); i++) {
Attribute.Symbol s = a.getSymbol(i);
int type = s.getType();
String name = s.getName().getName();
String value = String.valueOf(s.getValue());
namedValues.add(new ProtoValue(value).setName(name).setType(type));
}
return new ProtoValue(format).setNamedValues(namedValues);
}
private ProtoValue parse(Plural p) {
List<ProtoValue> namedValues = new ArrayList<>(p.getEntryCount());
for (int i = 0; i < p.getEntryCount(); i++) {
Plural.Entry e = p.getEntry(i);
String name = e.getArity().name();
String value = parse(e.getItem());
namedValues.add(new ProtoValue(value).setName(name));
}
return new ProtoValue().setNamedValues(namedValues);
}
private ProtoValue parse(CompoundValue c) {
switch (c.getValueCase()) {
case STYLE:
return parse(c.getStyle());
case STYLEABLE:
return parse(c.getStyleable());
case ARRAY:
return parse(c.getArray());
case ATTR:
return parse(c.getAttr());
case PLURAL:
return parse(c.getPlural());
default:
return new ProtoValue("Unresolved value");
}
}
private String parse(Configuration c) {
char[] language = c.getLocale().toCharArray();
if (language.length == 0) {
language = new char[] { '\00' };
}
short mcc = (short) c.getMcc();
short mnc = (short) c.getMnc();
byte orientation = (byte) c.getOrientationValue();
short screenWidth = (short) c.getScreenWidth();
short screenHeight = (short) c.getScreenHeight();
short screenWidthDp = (short) c.getScreenWidthDp();
short screenHeightDp = (short) c.getScreenHeightDp();
short smallestScreenWidthDp = (short) c.getSmallestScreenWidthDp();
short sdkVersion = (short) c.getSdkVersion();
byte keyboard = (byte) c.getKeyboardValue();
byte touchscreen = (byte) c.getTouchscreenValue();
int density = c.getDensity();
byte screenLayout = (byte) c.getScreenLayoutLongValue();
byte colorMode = (byte) (c.getHdrValue() | c.getWideColorGamutValue());
byte screenLayout2 = (byte) (c.getLayoutDirectionValue() | c.getScreenRoundValue());
byte navigation = (byte) c.getNavigationValue();
byte inputFlags = (byte) (c.getKeysHiddenValue() | c.getNavHiddenValue());
int size = c.getSerializedSize();
byte uiMode = (byte) (c.getUiModeNightValue() | c.getUiModeTypeValue());
c.getScreenLayoutSize(); // unknown field
c.getProduct(); // unknown field
return new EntryConfig(mcc, mnc, language, new char[] { '\00' },
orientation, touchscreen, density, keyboard, navigation,
inputFlags, screenWidth, screenHeight, sdkVersion,
screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp,
screenHeightDp, new char[] { '\00' }, new char[] { '\00' }, screenLayout2,
colorMode, false, size).getQualifiers();
}
private String parse(Item i) {
if (i.hasRawStr()) {
return i.getRawStr().getValue();
}
if (i.hasStr()) {
return i.getStr().getValue();
}
if (i.hasStyledStr()) {
return i.getStyledStr().getValue();
}
if (i.hasPrim()) {
Primitive prim = i.getPrim();
switch (prim.getOneofValueCase()) {
case NULL_VALUE:
return null;
case INT_DECIMAL_VALUE:
return String.valueOf(prim.getIntDecimalValue());
case INT_HEXADECIMAL_VALUE:
return Integer.toHexString(prim.getIntHexadecimalValue());
case BOOLEAN_VALUE:
return String.valueOf(prim.getBooleanValue());
case FLOAT_VALUE:
return String.valueOf(prim.getFloatValue());
case COLOR_ARGB4_VALUE:
return String.format("#%04x", prim.getColorArgb4Value());
case COLOR_ARGB8_VALUE:
return String.format("#%08x", prim.getColorArgb8Value());
case COLOR_RGB4_VALUE:
return String.format("#%03x", prim.getColorRgb4Value());
case COLOR_RGB8_VALUE:
return String.format("#%06x", prim.getColorRgb8Value());
case DIMENSION_VALUE:
return XmlGenUtils.decodeComplex(prim.getDimensionValue(), false);
case FRACTION_VALUE:
return XmlGenUtils.decodeComplex(prim.getDimensionValue(), true);
case EMPTY_VALUE:
default:
return "";
}
}
if (i.hasRef()) {
return '@' + i.getRef().getName();
}
if (i.hasFile()) {
return i.getFile().getPath();
}
return "";
}
private ResourceTable decodeProto(InputStream inputStream)
throws InvalidProtocolBufferException, IOException {
return ResourceTable.parseFrom(XmlGenUtils.readData(inputStream));
}
}

View File

@ -14,7 +14,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.FieldNode;
@ -89,30 +88,11 @@ public class ResTableParser extends CommonBinaryParser {
ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames());
ResXmlGen resGen = new ResXmlGen(resStorage, vp);
ICodeInfo content = makeXmlDump();
ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage);
List<ResContainer> xmlFiles = resGen.makeResourcesXml();
return ResContainer.resourceTable("res", xmlFiles, content);
}
public ICodeInfo makeXmlDump() {
ICodeWriter writer = root.makeCodeWriter();
writer.startLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
writer.startLine("<resources>");
writer.incIndent();
Set<String> addedValues = new HashSet<>();
for (ResourceEntry ri : resStorage.getResources()) {
if (addedValues.add(ri.getTypeName() + '.' + ri.getKeyName())) {
String format = String.format("<public type=\"%s\" name=\"%s\" id=\"%s\" />",
ri.getTypeName(), ri.getKeyName(), ri.getId());
writer.startLine(format);
}
}
writer.decIndent();
writer.startLine("</resources>");
return writer.finish();
}
public ResourceStorage getResStorage() {
return resStorage;
}

View File

@ -13,6 +13,7 @@ import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.impl.SimpleCodeWriter;
import jadx.core.utils.StringUtils;
import jadx.core.xmlgen.entry.ProtoValue;
import jadx.core.xmlgen.entry.RawNamedValue;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
@ -67,7 +68,31 @@ public class ResXmlGen {
}
private void addValue(ICodeWriter cw, ResourceEntry ri) {
if (ri.getSimpleValue() != null) {
if (ri.getProtoValue() != null) {
ProtoValue protoValue = ri.getProtoValue();
if (protoValue.getValue() != null && protoValue.getNamedValues() == null) {
addSimpleValue(cw, ri.getTypeName(), ri.getTypeName(), "name", ri.getKeyName(), protoValue.getValue());
} else {
cw.startLine();
cw.add('<').add(ri.getTypeName()).add(' ');
String itemTag = "item";
cw.add("name=\"").add(ri.getKeyName()).add('\"');
if (ri.getTypeName().equals("attr") && protoValue.getValue() != null) {
cw.add(" format=\"").add(protoValue.getValue()).add('\"');
}
if (protoValue.getParent() != null) {
cw.add(" parent=\"").add(protoValue.getParent()).add('\"');
}
cw.add(">");
cw.incIndent();
for (ProtoValue value : protoValue.getNamedValues()) {
addProtoItem(cw, itemTag, ri.getTypeName(), value);
}
cw.decIndent();
cw.startLine().add("</").add(ri.getTypeName()).add('>');
}
} else if (ri.getSimpleValue() != null) {
String valueStr = vp.decodeValue(ri.getSimpleValue());
addSimpleValue(cw, ri.getTypeName(), ri.getTypeName(), "name", ri.getKeyName(), valueStr);
} else {
@ -82,7 +107,7 @@ public class ResXmlGen {
} else if ((type & ValuesParser.ATTR_TYPE_FLAGS) != 0) {
itemTag = "flag";
}
String formatValue = getTypeAsString(type);
String formatValue = XmlGenUtils.getAttrTypeAsString(type);
if (formatValue != null) {
cw.add("\" format=\"").add(formatValue);
}
@ -105,36 +130,27 @@ public class ResXmlGen {
}
}
private String getTypeAsString(int type) {
String s = "";
if ((type & ValuesParser.ATTR_TYPE_REFERENCE) != 0) {
s += "|reference";
private void addProtoItem(ICodeWriter cw, String itemTag, String typeName, ProtoValue protoValue) {
String name = protoValue.getName();
String value = protoValue.getValue();
switch (typeName) {
case "attr":
if (name != null) {
addSimpleValue(cw, typeName, itemTag, name, value, "");
}
break;
case "style":
if (name != null) {
addSimpleValue(cw, typeName, itemTag, name, "", value);
}
break;
case "plurals":
addSimpleValue(cw, typeName, itemTag, "quantity", name, value);
break;
default:
addSimpleValue(cw, typeName, itemTag, null, null, value);
break;
}
if ((type & ValuesParser.ATTR_TYPE_STRING) != 0) {
s += "|string";
}
if ((type & ValuesParser.ATTR_TYPE_INTEGER) != 0) {
s += "|integer";
}
if ((type & ValuesParser.ATTR_TYPE_BOOLEAN) != 0) {
s += "|boolean";
}
if ((type & ValuesParser.ATTR_TYPE_COLOR) != 0) {
s += "|color";
}
if ((type & ValuesParser.ATTR_TYPE_FLOAT) != 0) {
s += "|float";
}
if ((type & ValuesParser.ATTR_TYPE_DIMENSION) != 0) {
s += "|dimension";
}
if ((type & ValuesParser.ATTR_TYPE_FRACTION) != 0) {
s += "|fraction";
}
if (s.isEmpty()) {
return null;
}
return s.substring(1);
}
private void addItem(ICodeWriter cw, String itemTag, String typeName, RawNamedValue value) {

View File

@ -21,6 +21,7 @@ public class XmlDeobf {
@Nullable
public static String deobfClassName(RootNode rootNode, String potencialClassName, String packageName) {
potencialClassName = potencialClassName.replace('$', '.');
if (packageName != null && potencialClassName.startsWith(".")) {
potencialClassName = packageName + potencialClassName;
}

View File

@ -0,0 +1,141 @@
package jadx.core.xmlgen;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.NumberFormat;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
public class XmlGenUtils {
private XmlGenUtils() {
}
public static byte[] readData(InputStream i) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[16384];
int read;
while ((read = i.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, read);
}
return buffer.toByteArray();
}
public static ICodeInfo makeXmlDump(ICodeWriter writer, ResourceStorage resStorage) {
writer.startLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
writer.startLine("<resources>");
writer.incIndent();
Set<String> addedValues = new HashSet<>();
for (ResourceEntry ri : resStorage.getResources()) {
if (addedValues.add(ri.getTypeName() + '.' + ri.getKeyName())) {
String format = String.format("<public type=\"%s\" name=\"%s\" id=\"%s\" />",
ri.getTypeName(), ri.getKeyName(), ri.getId());
writer.startLine(format);
}
}
writer.decIndent();
writer.startLine("</resources>");
return writer.finish();
}
public static String decodeComplex(int data, boolean isFraction) {
double value = (data & ParserConstants.COMPLEX_MANTISSA_MASK << ParserConstants.COMPLEX_MANTISSA_SHIFT)
* ParserConstants.RADIX_MULTS[data >> ParserConstants.COMPLEX_RADIX_SHIFT & ParserConstants.COMPLEX_RADIX_MASK];
int unitType = data & ParserConstants.COMPLEX_UNIT_MASK;
String unit;
if (isFraction) {
value *= 100;
switch (unitType) {
case ParserConstants.COMPLEX_UNIT_FRACTION:
unit = "%";
break;
case ParserConstants.COMPLEX_UNIT_FRACTION_PARENT:
unit = "%p";
break;
default:
unit = "?f" + Integer.toHexString(unitType);
}
} else {
switch (unitType) {
case ParserConstants.COMPLEX_UNIT_PX:
unit = "px";
break;
case ParserConstants.COMPLEX_UNIT_DIP:
unit = "dp";
break;
case ParserConstants.COMPLEX_UNIT_SP:
unit = "sp";
break;
case ParserConstants.COMPLEX_UNIT_PT:
unit = "pt";
break;
case ParserConstants.COMPLEX_UNIT_IN:
unit = "in";
break;
case ParserConstants.COMPLEX_UNIT_MM:
unit = "mm";
break;
default:
unit = "?d" + Integer.toHexString(unitType);
}
}
return doubleToString(value) + unit;
}
public static String doubleToString(double value) {
if (Double.compare(value, Math.floor(value)) == 0
&& !Double.isInfinite(value)) {
return Integer.toString((int) value);
}
// remove trailing zeroes
NumberFormat f = NumberFormat.getInstance(Locale.ROOT);
f.setMaximumFractionDigits(4);
f.setMinimumIntegerDigits(1);
return f.format(value);
}
public static String floatToString(float value) {
return doubleToString(value);
}
public static String getAttrTypeAsString(int type) {
String s = "";
if ((type & ValuesParser.ATTR_TYPE_REFERENCE) != 0) {
s += "|reference";
}
if ((type & ValuesParser.ATTR_TYPE_STRING) != 0) {
s += "|string";
}
if ((type & ValuesParser.ATTR_TYPE_INTEGER) != 0) {
s += "|integer";
}
if ((type & ValuesParser.ATTR_TYPE_BOOLEAN) != 0) {
s += "|boolean";
}
if ((type & ValuesParser.ATTR_TYPE_COLOR) != 0) {
s += "|color";
}
if ((type & ValuesParser.ATTR_TYPE_FLOAT) != 0) {
s += "|float";
}
if ((type & ValuesParser.ATTR_TYPE_DIMENSION) != 0) {
s += "|dimension";
}
if ((type & ValuesParser.ATTR_TYPE_FRACTION) != 0) {
s += "|fraction";
}
if (s.isEmpty()) {
return null;
}
return s.substring(1);
}
}

View File

@ -0,0 +1,58 @@
package jadx.core.xmlgen.entry;
import java.util.List;
public class ProtoValue {
private String parent;
private String name;
private String value;
private int type;
private List<ProtoValue> namedValues;
public ProtoValue(String value) {
this.value = value;
}
public ProtoValue() {
}
public int getType() {
return type;
}
public ProtoValue setType(int type) {
this.type = type;
return this;
}
public String getValue() {
return value;
}
public String getParent() {
return parent;
}
public ProtoValue setParent(String parent) {
this.parent = parent;
return this;
}
public ProtoValue setName(String name) {
this.name = name;
return this;
}
public String getName() {
return name;
}
public ProtoValue setNamedValues(List<ProtoValue> namedValues) {
this.namedValues = namedValues;
return this;
}
public List<ProtoValue> getNamedValues() {
return namedValues;
}
}

View File

@ -11,6 +11,7 @@ public final class ResourceEntry {
private final String config;
private int parentRef;
private ProtoValue protoValue;
private RawValue simpleValue;
private List<RawNamedValue> namedValues;
@ -25,6 +26,7 @@ public final class ResourceEntry {
public ResourceEntry copy(String newKeyName) {
ResourceEntry copy = new ResourceEntry(id, pkgName, typeName, newKeyName, config);
copy.parentRef = this.parentRef;
copy.protoValue = this.protoValue;
copy.simpleValue = this.simpleValue;
copy.namedValues = this.namedValues;
return copy;
@ -62,6 +64,14 @@ public final class ResourceEntry {
return parentRef;
}
public ProtoValue getProtoValue() {
return protoValue;
}
public void setProtoValue(ProtoValue protoValue) {
this.protoValue = protoValue;
}
public RawValue getSimpleValue() {
return simpleValue;
}

View File

@ -1,10 +1,8 @@
package jadx.core.xmlgen.entry;
import java.io.InputStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
@ -14,6 +12,7 @@ import org.slf4j.LoggerFactory;
import jadx.core.utils.android.TextResMapFile;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ParserConstants;
import jadx.core.xmlgen.XmlGenUtils;
public class ValuesParser extends ParserConstants {
private static final Logger LOG = LoggerFactory.getLogger(ValuesParser.class);
@ -46,6 +45,10 @@ public class ValuesParser extends ParserConstants {
@Nullable
public String getSimpleValueString(ResourceEntry ri) {
ProtoValue protoValue = ri.getProtoValue();
if (protoValue != null) {
return protoValue.getValue();
}
RawValue simpleValue = ri.getSimpleValue();
if (simpleValue == null) {
return null;
@ -55,6 +58,22 @@ public class ValuesParser extends ParserConstants {
@Nullable
public String getValueString(ResourceEntry ri) {
ProtoValue protoValue = ri.getProtoValue();
if (protoValue != null) {
if (protoValue.getValue() != null) {
return protoValue.getValue();
}
List<ProtoValue> values = protoValue.getNamedValues();
List<String> strList = new ArrayList<>(values.size());
for (ProtoValue value : values) {
if (value.getName() == null) {
strList.add(value.getValue());
} else {
strList.add(value.getName() + '=' + value.getValue());
}
}
return strList.toString();
}
RawValue simpleValue = ri.getSimpleValue();
if (simpleValue != null) {
return decodeValue(simpleValue);
@ -94,8 +113,7 @@ public class ValuesParser extends ParserConstants {
case TYPE_INT_BOOLEAN:
return data == 0 ? "false" : "true";
case TYPE_FLOAT:
return floatToString(Float.intBitsToFloat(data));
return XmlGenUtils.floatToString(Float.intBitsToFloat(data));
case TYPE_INT_COLOR_ARGB8:
return String.format("#%08x", data);
case TYPE_INT_COLOR_RGB8:
@ -134,9 +152,9 @@ public class ValuesParser extends ParserConstants {
}
case TYPE_DIMENSION:
return decodeComplex(data, false);
return XmlGenUtils.decodeComplex(data, false);
case TYPE_FRACTION:
return decodeComplex(data, true);
return XmlGenUtils.decodeComplex(data, true);
case TYPE_DYNAMIC_ATTRIBUTE:
LOG.warn("Data type TYPE_DYNAMIC_ATTRIBUTE not yet supported: {}", data);
return " TYPE_DYNAMIC_ATTRIBUTE: " + data;
@ -166,66 +184,4 @@ public class ValuesParser extends ParserConstants {
}
return "?0x" + Integer.toHexString(nameRef);
}
private String decodeComplex(int data, boolean isFraction) {
double value = (data & COMPLEX_MANTISSA_MASK << COMPLEX_MANTISSA_SHIFT)
* RADIX_MULTS[data >> COMPLEX_RADIX_SHIFT & COMPLEX_RADIX_MASK];
int unitType = data & COMPLEX_UNIT_MASK;
String unit;
if (isFraction) {
value *= 100;
switch (unitType) {
case COMPLEX_UNIT_FRACTION:
unit = "%";
break;
case COMPLEX_UNIT_FRACTION_PARENT:
unit = "%p";
break;
default:
unit = "?f" + Integer.toHexString(unitType);
}
} else {
switch (unitType) {
case COMPLEX_UNIT_PX:
unit = "px";
break;
case COMPLEX_UNIT_DIP:
unit = "dp";
break;
case COMPLEX_UNIT_SP:
unit = "sp";
break;
case COMPLEX_UNIT_PT:
unit = "pt";
break;
case COMPLEX_UNIT_IN:
unit = "in";
break;
case COMPLEX_UNIT_MM:
unit = "mm";
break;
default:
unit = "?d" + Integer.toHexString(unitType);
}
}
return doubleToString(value) + unit;
}
private static String doubleToString(double value) {
if (Double.compare(value, Math.floor(value)) == 0
&& !Double.isInfinite(value)) {
return Integer.toString((int) value);
}
// remove trailing zeroes
NumberFormat f = NumberFormat.getInstance(Locale.ROOT);
f.setMaximumFractionDigits(4);
f.setMinimumIntegerDigits(1);
return f.format(value);
}
private static String floatToString(float value) {
return doubleToString(value);
}
}

View File

@ -190,7 +190,8 @@ public class ApkSignature extends JNode {
builder.append("<blockquote>");
// Unprotected Zip entry issues are very common, handle them separately
List<ApkVerifier.IssueWithParams> unprotIssues = issueList.stream()
.filter(i -> i.getIssue() == ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList());
.filter(i -> i.getIssue() == ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY)
.collect(Collectors.toList());
if (!unprotIssues.isEmpty()) {
builder.append("<h4>");
builder.escape(NLS.str("apkSignature.unprotectedEntry"));
@ -202,7 +203,8 @@ public class ApkSignature extends JNode {
builder.append("</blockquote>");
}
List<ApkVerifier.IssueWithParams> remainingIssues = issueList.stream()
.filter(i -> i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList());
.filter(i -> i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY)
.collect(Collectors.toList());
if (!remainingIssues.isEmpty()) {
builder.append("<pre>\n");
for (ApkVerifier.IssueWithParams issue : remainingIssues) {

View File

@ -5,7 +5,8 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import javax.swing.*;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
@ -63,8 +64,7 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
public final void update() {
if (files.isEmpty()) {
if (type == JResType.DIR
|| type == JResType.ROOT
if (type == JResType.DIR || type == JResType.ROOT
|| resFile.getType() == ResourceType.ARSC) {
// fake leaf to force show expand button
// real sub nodes will load on expand in loadNode() method
@ -278,6 +278,7 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
case CODE:
case FONT:
case LIB:
case MEDIA:
return false;
case MANIFEST:

View File

@ -279,7 +279,7 @@ public class MainWindow extends JFrame {
if (addFiles) {
exts = new String[] { "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc" };
} else {
exts = new String[] { JadxProject.PROJECT_EXTENSION, "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc" };
exts = new String[] { JadxProject.PROJECT_EXTENSION, "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "aab" };
}
String description = "Supported files: (" + Utils.arrayToStr(exts) + ')';