diff --git a/README.md b/README.md index 967406d5..2aee634b 100644 --- a/README.md +++ b/README.md @@ -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-.zip` ### Usage ``` -jadx[-gui] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc) +jadx[-gui] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab) options: -d, --output-dir - output directory -ds, --output-dir-src - output directory for sources diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index c28d7074..40eacd3d 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -19,7 +19,7 @@ import jadx.core.utils.files.FileUtils; public class JadxCLIArgs { - @Parameter(description = " (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc)") + @Parameter(description = " (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)") protected List files = new ArrayList<>(1); @Parameter(names = { "-d", "--output-dir" }, description = "output directory") diff --git a/jadx-core/build.gradle b/jadx-core/build.gradle index 4bb43839..c1421c14 100644 --- a/jadx-core/build.gradle +++ b/jadx-core/build.gradle @@ -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' diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 30b1d3a5..bab7a996 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -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 classes; private List resources; - private BinaryXMLParser xmlParser; + private BinaryXMLParser binaryXmlParser; + private ProtoXMLParser protoXmlParser; private final Map classesMap = new ConcurrentHashMap<>(); private final Map 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) { diff --git a/jadx-core/src/main/java/jadx/api/ResourceType.java b/jadx-core/src/main/java/jadx/api/ResourceType.java index d139c07e..29055ed7 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceType.java +++ b/jadx-core/src/main/java/jadx/api/ResourceType.java @@ -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); diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 8787b3ed..3cbd7d4c 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -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); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index 32cbf648..e8cb426d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -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 loadedInputs) { @@ -502,4 +504,8 @@ public class RootNode { public TypeUtils getTypeUtils() { return typeUtils; } + + public boolean isProto() { + return isProto; + } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ProtoXMLParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ProtoXMLParser.java new file mode 100644 index 00000000..e49d647b --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ProtoXMLParser.java @@ -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 nsMap; + private final Map 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(""); + 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 oldNsMap = new HashMap<>(nsMap); + decode(e.getChild(i)); + nsMap = oldNsMap; + } + writer.decIndent(); + writer.startLine("'); + } 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)); + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResProtoParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResProtoParser.java new file mode 100644 index 00000000..56aaab1d --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResProtoParser.java @@ -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 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 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 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 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 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 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 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)); + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java index 1d2075c1..8b8c797a 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -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 xmlFiles = resGen.makeResourcesXml(); return ResContainer.resourceTable("res", xmlFiles, content); } - public ICodeInfo makeXmlDump() { - ICodeWriter writer = root.makeCodeWriter(); - writer.startLine(""); - writer.startLine(""); - writer.incIndent(); - - Set addedValues = new HashSet<>(); - for (ResourceEntry ri : resStorage.getResources()) { - if (addedValues.add(ri.getTypeName() + '.' + ri.getKeyName())) { - String format = String.format("", - ri.getTypeName(), ri.getKeyName(), ri.getId()); - writer.startLine(format); - } - } - writer.decIndent(); - writer.startLine(""); - return writer.finish(); - } - public ResourceStorage getResStorage() { return resStorage; } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java index ada5106c..dc6b512c 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -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("'); + } + } 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) { diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/XmlDeobf.java b/jadx-core/src/main/java/jadx/core/xmlgen/XmlDeobf.java index 3eb2e0ac..d5e6c134 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/XmlDeobf.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/XmlDeobf.java @@ -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; } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/XmlGenUtils.java b/jadx-core/src/main/java/jadx/core/xmlgen/XmlGenUtils.java new file mode 100644 index 00000000..5369e853 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/XmlGenUtils.java @@ -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(""); + writer.startLine(""); + writer.incIndent(); + + Set addedValues = new HashSet<>(); + for (ResourceEntry ri : resStorage.getResources()) { + if (addedValues.add(ri.getTypeName() + '.' + ri.getKeyName())) { + String format = String.format("", + ri.getTypeName(), ri.getKeyName(), ri.getId()); + writer.startLine(format); + } + } + writer.decIndent(); + writer.startLine(""); + 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); + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/ProtoValue.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ProtoValue.java new file mode 100644 index 00000000..5c14fe09 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ProtoValue.java @@ -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 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 namedValues) { + this.namedValues = namedValues; + return this; + } + + public List getNamedValues() { + return namedValues; + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java index c9916bee..8deb7d83 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java @@ -11,6 +11,7 @@ public final class ResourceEntry { private final String config; private int parentRef; + private ProtoValue protoValue; private RawValue simpleValue; private List 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; } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java index da6121ae..3fd36cf8 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java @@ -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 values = protoValue.getNamedValues(); + List 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); - } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java index 3555b2e8..429902b6 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java @@ -190,7 +190,8 @@ public class ApkSignature extends JNode { builder.append("
"); // Unprotected Zip entry issues are very common, handle them separately List 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("

"); builder.escape(NLS.str("apkSignature.unprotectedEntry")); @@ -202,7 +203,8 @@ public class ApkSignature extends JNode { builder.append("

"); } List 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("
\n");
 				for (ApkVerifier.IssueWithParams issue : remainingIssues) {
diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java
index b7dab37d..d6d693e1 100644
--- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java
+++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java
@@ -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 {
 
 	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 {
 			case CODE:
 			case FONT:
 			case LIB:
+			case MEDIA:
 				return false;
 
 			case MANIFEST:
diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
index fd7678ab..49f8f8e4 100644
--- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
+++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
@@ -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) + ')';