diff --git a/jadx-core/src/main/java/jadx/core/utils/android/DataInputDelegate.java b/jadx-core/src/main/java/jadx/core/utils/android/DataInputDelegate.java new file mode 100644 index 0000000000000000000000000000000000000000..6597fb1b26f33c156552eeda48b96b250345a649 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/android/DataInputDelegate.java @@ -0,0 +1,91 @@ +/** + * Copyright 2014 Ryszard Wiśniewski + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jadx.core.utils.android; + +import java.io.DataInput; +import java.io.IOException; + +/** + * @author Ryszard Wiśniewski + */ +abstract public class DataInputDelegate implements DataInput { + protected final DataInput mDelegate; + + public DataInputDelegate(DataInput delegate) { + this.mDelegate = delegate; + } + + public int skipBytes(int n) throws IOException { + return mDelegate.skipBytes(n); + } + + public int readUnsignedShort() throws IOException { + return mDelegate.readUnsignedShort(); + } + + public int readUnsignedByte() throws IOException { + return mDelegate.readUnsignedByte(); + } + + public String readUTF() throws IOException { + return mDelegate.readUTF(); + } + + public short readShort() throws IOException { + return mDelegate.readShort(); + } + + public long readLong() throws IOException { + return mDelegate.readLong(); + } + + public String readLine() throws IOException { + return mDelegate.readLine(); + } + + public int readInt() throws IOException { + return mDelegate.readInt(); + } + + public void readFully(byte[] b, int off, int len) throws IOException { + mDelegate.readFully(b, off, len); + } + + public void readFully(byte[] b) throws IOException { + mDelegate.readFully(b); + } + + public float readFloat() throws IOException { + return mDelegate.readFloat(); + } + + public double readDouble() throws IOException { + return mDelegate.readDouble(); + } + + public char readChar() throws IOException { + return mDelegate.readChar(); + } + + public byte readByte() throws IOException { + return mDelegate.readByte(); + } + + public boolean readBoolean() throws IOException { + return mDelegate.readBoolean(); + } +} \ No newline at end of file diff --git a/jadx-core/src/main/java/jadx/core/utils/android/ExtDataInput.java b/jadx-core/src/main/java/jadx/core/utils/android/ExtDataInput.java new file mode 100644 index 0000000000000000000000000000000000000000..5a17a6d11946d2730091eaa67198fd5a8b50d7b4 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/android/ExtDataInput.java @@ -0,0 +1,111 @@ +/** + * Copyright 2014 Ryszard Wiśniewski + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jadx.core.utils.android; + +import java.io.*; + +/** + * @author Ryszard Wiśniewski + */ +public class ExtDataInput extends DataInputDelegate { + public ExtDataInput(InputStream in) { + this((DataInput) new DataInputStream(in)); + } + + public ExtDataInput(DataInput delegate) { + super(delegate); + } + + public int[] readIntArray(int length) throws IOException { + int[] array = new int[length]; + for(int i = 0; i < length; i++) { + array[i] = readInt(); + } + return array; + } + + public void skipInt() throws IOException { + skipBytes(4); + } + + public void skipCheckInt(int expected) throws IOException { + int got = readInt(); + if (got != expected) { + throw new IOException(String.format( + "Expected: 0x%08x, got: 0x%08x", expected, got)); + } + } + + public void skipCheckShort(short expected) throws IOException { + short got = readShort(); + if (got != expected) { + throw new IOException(String.format( + "Expected: 0x%08x, got: 0x%08x", expected, got)); + } + } + + public void skipCheckByte(byte expected) throws IOException { + byte got = readByte(); + if (got != expected) { + throw new IOException(String.format( + "Expected: 0x%08x, got: 0x%08x", expected, got)); + } + } + + public void skipCheckChunkTypeInt(int expected, int possible) throws IOException { + int got = readInt(); + + if (got == possible) { + skipCheckChunkTypeInt(expected, -1); + } else if (got != expected) { + throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); + } + } + + /** + * The general contract of DataInput doesn't guarantee all the bytes requested will be skipped + * and failure can occur for many reasons. We override this to try harder to skip all the bytes + * requested (this is similar to DataInputStream's wrapper). + */ + public final int skipBytes(int n) throws IOException { + int total = 0; + int cur = 0; + + while ((total < n) && ((cur = (int) super.skipBytes(n - total)) > 0)) { + total += cur; + } + + return total; + } + + public String readNullEndedString(int length, boolean fixed) + throws IOException { + StringBuilder string = new StringBuilder(16); + while(length-- != 0) { + short ch = readShort(); + if (ch == 0) { + break; + } + string.append((char) ch); + } + if (fixed) { + skipBytes(length * 2); + } + + return string.toString(); + } +} \ No newline at end of file diff --git a/jadx-core/src/main/java/jadx/core/utils/android/Res9patchStreamDecoder.java b/jadx-core/src/main/java/jadx/core/utils/android/Res9patchStreamDecoder.java new file mode 100644 index 0000000000000000000000000000000000000000..9d845c42107522a9a256c1ec0a2b2e596ae68214 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/android/Res9patchStreamDecoder.java @@ -0,0 +1,138 @@ +/** + * Copyright 2014 Ryszard Wiśniewski + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jadx.core.utils.android; + +import org.apache.commons.io.IOUtils; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.DataInput; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.imageio.ImageIO; + +import jadx.core.utils.exceptions.JadxException; + +/** + * @author Ryszard Wiśniewski + */ +public class Res9patchStreamDecoder { + + public void decode(InputStream in, OutputStream out) throws JadxException { + try { + byte[] data = IOUtils.toByteArray(in); + + BufferedImage im = ImageIO.read(new ByteArrayInputStream(data)); + int w = im.getWidth(), h = im.getHeight(); + + BufferedImage im2 = new BufferedImage(w+2, h+2, BufferedImage.TYPE_INT_ARGB); + im2.createGraphics().drawImage(im, 1, 1, w, h, null); + + NinePatch np = getNinePatch(data); + drawHLine(im2, h + 1, np.padLeft + 1, w - np.padRight); + drawVLine(im2, w + 1, np.padTop + 1, h - np.padBottom); + + int[] xDivs = np.xDivs; + for (int i = 0; i < xDivs.length; i += 2) { + drawHLine(im2, 0, xDivs[i] + 1, xDivs[i + 1]); + } + + int[] yDivs = np.yDivs; + for (int i = 0; i < yDivs.length; i += 2) { + drawVLine(im2, 0, yDivs[i] + 1, yDivs[i + 1]); + } + + ImageIO.write(im2, "png", out); + } catch (IOException | NullPointerException ex) { + throw new JadxException(ex.toString()); + } + } + + private NinePatch getNinePatch(byte[] data) throws JadxException, + IOException { + ExtDataInput di = new ExtDataInput(new ByteArrayInputStream(data)); + find9patchChunk(di); + return NinePatch.decode(di); + } + + private void find9patchChunk(DataInput di) throws JadxException, + IOException { + di.skipBytes(8); + while (true) { + int size; + try { + size = di.readInt(); + } catch (IOException ex) { + throw new JadxException("Cant find nine patch chunk", ex); + } + if (di.readInt() == NP_CHUNK_TYPE) { + return; + } + di.skipBytes(size + 4); + } + } + + private void drawHLine(BufferedImage im, int y, int x1, int x2) { + for (int x = x1; x <= x2; x++) { + im.setRGB(x, y, NP_COLOR); + } + } + + private void drawVLine(BufferedImage im, int x, int y1, int y2) { + for (int y = y1; y <= y2; y++) { + im.setRGB(x, y, NP_COLOR); + } + } + + private static final int NP_CHUNK_TYPE = 0x6e705463; // npTc + private static final int NP_COLOR = 0xff000000; + + private static class NinePatch { + public final int padLeft, padRight, padTop, padBottom; + public final int[] xDivs, yDivs; + + public NinePatch(int padLeft, int padRight, int padTop, int padBottom, + int[] xDivs, int[] yDivs) { + this.padLeft = padLeft; + this.padRight = padRight; + this.padTop = padTop; + this.padBottom = padBottom; + this.xDivs = xDivs; + this.yDivs = yDivs; + } + + public static NinePatch decode(ExtDataInput di) throws IOException { + di.skipBytes(1); + byte numXDivs = di.readByte(); + byte numYDivs = di.readByte(); + di.skipBytes(1); + di.skipBytes(8); + int padLeft = di.readInt(); + int padRight = di.readInt(); + int padTop = di.readInt(); + int padBottom = di.readInt(); + di.skipBytes(4); + int[] xDivs = di.readIntArray(numXDivs); + int[] yDivs = di.readIntArray(numYDivs); + + return new NinePatch(padLeft, padRight, padTop, padBottom, xDivs, + yDivs); + } + } +} \ No newline at end of file diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java index 0d2e8ea3adf2982459ffc964585907d39527b987..e0ddb3c44cd332eda75bceb55745ecba3c510453 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java @@ -6,7 +6,6 @@ import jadx.core.dex.info.ConstStorage; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.RootNode; -import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.entry.ValuesParser; @@ -35,27 +34,28 @@ public class BinaryXMLParser extends CommonBinaryParser { private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class); private static final String ANDROID_R_STYLE_CLS = "android.R$style"; private static final boolean ATTR_NEW_LINE = false; - + private final Map styleMap = new HashMap(); + private final Map localStyleMap = new HashMap(); + private final Map resNames; + private final Map nsMap = new HashMap<>(); private CodeWriter writer; private String[] strings; - - private String nsPrefix = "ERROR"; - private String nsURI = "ERROR"; private String currentTag = "ERROR"; - private boolean firstElement; - private boolean wasOneLiner = false; - - private final Map styleMap = new HashMap<>(); - private final Map localStyleMap = new HashMap<>(); - private final Map resNames; private ValuesParser valuesParser; - - private final ManifestAttributes attributes; + private boolean isLastEnd = true; + private boolean isOneLine = true; public BinaryXMLParser(RootNode root) { try { - loadStyles(); + try { + Class rStyleCls = Class.forName(ANDROID_R_STYLE_CLS); + for (Field f : rStyleCls.getFields()) { + styleMap.put(f.getInt(f.getType()), f.getName()); + } + } catch (Throwable th) { + LOG.error("R class loading failed", th); + } // add application constants ConstStorage constStorage = root.getConstValues(); Map constFields = constStorage.getGlobalConstFields(); @@ -67,25 +67,11 @@ public class BinaryXMLParser extends CommonBinaryParser { } } resNames = constStorage.getResourcesNames(); - - attributes = new ManifestAttributes(); - attributes.parseAll(); } catch (Exception e) { throw new JadxRuntimeException("BinaryXMLParser init error", e); } } - private void loadStyles() { - try { - Class rStyleCls = Class.forName(ANDROID_R_STYLE_CLS); - for (Field f : rStyleCls.getFields()) { - styleMap.put(f.getInt(f.getType()), f.getName()); - } - } catch (Exception th) { - LOG.error("R class loading failed", th); - } - } - public synchronized CodeWriter parse(InputStream inputStream) throws IOException { is = new ParserStream(inputStream); if (!isBinaryXml()) { @@ -126,12 +112,14 @@ public class BinaryXMLParser extends CommonBinaryParser { parseResourceMap(); break; case RES_XML_START_NAMESPACE_TYPE: - case RES_XML_END_NAMESPACE_TYPE: parseNameSpace(); break; case RES_XML_CDATA_TYPE: parseCData(); break; + case RES_XML_END_NAMESPACE_TYPE: + parseNameSpaceEnd(); + break; case RES_XML_START_ELEMENT_TYPE: parseElement(); break; @@ -164,12 +152,25 @@ public class BinaryXMLParser extends CommonBinaryParser { if (is.readInt32() != 0x18) { die("NAMESPACE header chunk is not 0x18 big"); } - int lineNumber = is.readInt32(); + int beginLineNumber = is.readInt32(); int comment = is.readInt32(); - int idPrefix = is.readInt32(); - nsPrefix = strings[idPrefix]; - int idURI = is.readInt32(); - nsURI = strings[idURI]; + int beginPrefix = is.readInt32(); + int beginURI = is.readInt32(); + nsMap.computeIfAbsent(strings[beginURI], k -> strings[beginPrefix]); + } + + private void parseNameSpaceEnd() throws IOException { + if (is.readInt16() != 0x10) { + die("NAMESPACE header is not 0x0010"); + } + if (is.readInt32() != 0x18) { + die("NAMESPACE header chunk is not 0x18 big"); + } + int endLineNumber = is.readInt32(); + int comment = is.readInt32(); + int endPrefix = is.readInt32(); + int endURI = is.readInt32(); + nsMap.computeIfAbsent(strings[endURI], k -> strings[endPrefix]); } private void parseCData() throws IOException { @@ -185,11 +186,13 @@ public class BinaryXMLParser extends CommonBinaryParser { int strIndex = is.readInt32(); String str = strings[strIndex]; - writer.startLine().addIndent(); - writer.attachSourceLine(lineNumber); - writer.add(StringUtils.escapeXML(str.trim())); // TODO: wrap into CDATA for easier reading - long size = is.readInt16(); + //TODO: what's this for? + /*writer.startLine().addIndent(); + writer.attachSourceLine(lineNumber); + writer.add(StringUtils.escapeXML(str.trim()));*/ + + int size = is.readInt16(); is.skip(size - 2); } @@ -208,11 +211,11 @@ public class BinaryXMLParser extends CommonBinaryParser { int comment = is.readInt32(); int startNS = is.readInt32(); int startNSName = is.readInt32(); // actually is elementName... - if (!wasOneLiner && !"ERROR".equals(currentTag) - && !currentTag.equals(strings[startNSName])) { + if (!isLastEnd && !"ERROR".equals(currentTag)) { writer.add(">"); } - wasOneLiner = false; + isOneLine = true; + isLastEnd = false; currentTag = strings[startNSName]; writer.startLine("<").add(currentTag); writer.attachSourceLine(elementBegLineNumber); @@ -229,9 +232,11 @@ public class BinaryXMLParser extends CommonBinaryParser { int classIndex = is.readInt16(); int styleIndex = is.readInt16(); if ("manifest".equals(currentTag) || writer.getIndent() == 0) { - writer.add(" xmlns:android=\"").add(nsURI).add("\""); + for (Map.Entry entry : nsMap.entrySet()) { + writer.add(" xmlns:" + entry.getValue() + "=\"").add(entry.getKey()).add("\""); + } } - boolean attrNewLine = attributeCount != 1 && ATTR_NEW_LINE; + boolean attrNewLine = attributeCount == 1 ? false : ATTR_NEW_LINE; for (int i = 0; i < attributeCount; i++) { parseAttribute(i, attrNewLine); } @@ -258,10 +263,10 @@ public class BinaryXMLParser extends CommonBinaryParser { writer.add(' '); } if (attributeNS != -1) { - writer.add(nsPrefix).add(':'); + writer.add(nsMap.get(strings[attributeNS])).add(':'); } writer.add(attrName).add("=\""); - String decodedAttr = attributes.decode(attrName, attrValData); + String decodedAttr = ManifestAttributes.getInstance().decode(attrName, attrValData); if (decodedAttr != null) { writer.add(decodedAttr); } else { @@ -275,10 +280,11 @@ public class BinaryXMLParser extends CommonBinaryParser { // reference custom processing String name = styleMap.get(attrValData); if (name != null) { - writer.add("@*"); + writer.add("@"); if (attributeNS != -1) { - writer.add(nsPrefix).add(':'); + writer.add(nsMap.get(strings[attributeNS])).add(':'); } + LOG.debug("decodeAttribute: " + attributeNS + " " + name); writer.add("style/").add(name.replaceAll("_", ".")); } else { FieldNode field = localStyleMap.get(attrValData); @@ -292,9 +298,20 @@ public class BinaryXMLParser extends CommonBinaryParser { } else { String resName = resNames.get(attrValData); if (resName != null) { - writer.add("@").add(resName); + writer.add("@"); + if (resName.startsWith("id/")) { + writer.add("+"); + } + writer.add(resName); } else { - writer.add("0x").add(Integer.toHexString(attrValData)); + resName = ValuesParser.androidResMap.get(attrValData); + if (resName != null) { + writer.add("@android:").add(resName); + } else if (attrValData == 0) { + writer.add("@null"); + } else { + writer.add("0x").add(Integer.toHexString(attrValData)); + } } } } @@ -315,9 +332,8 @@ public class BinaryXMLParser extends CommonBinaryParser { int comment = is.readInt32(); int elementNS = is.readInt32(); int elementName = is.readInt32(); - if (currentTag.equals(strings[elementName])) { + if (currentTag.equals(strings[elementName]) && isOneLine && !isLastEnd) { writer.add(" />"); - wasOneLiner = true; } else { writer.startLine(""); } + isLastEnd = true; if (writer.getIndent() != 0) { writer.decIndent(); } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java b/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java index 16fcd936d2bd8b17ca81ea20965248d4ac5ebeaa..b0bd20e84c15106e6c543ef14b9331433362eeb1 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java @@ -49,7 +49,24 @@ public class ManifestAttributes { private final Map attrMap = new HashMap<>(); - public void parseAll() { + private static ManifestAttributes instance; + + public static ManifestAttributes getInstance() { + if (instance == null) { + try { + instance = new ManifestAttributes(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return instance; + } + + private ManifestAttributes() { + parseAll(); + } + + private void parseAll() { parse(loadXML(ATTR_XML)); parse(loadXML(MANIFEST_ATTR_XML)); LOG.debug("Loaded android attributes count: {}", attrMap.size()); @@ -158,7 +175,10 @@ public class ManifestAttributes { } else if (attr.getType() == MAttrType.FLAG) { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : attr.getValues().entrySet()) { - if ((value & entry.getKey()) != 0) { + if (value == entry.getKey()) { + sb = new StringBuilder(entry.getValue() + "|"); + break; + } else if ((value & entry.getKey()) == entry.getKey()) { sb.append(entry.getValue()).append('|'); } } @@ -166,6 +186,6 @@ public class ManifestAttributes { return sb.deleteCharAt(sb.length() - 1).toString(); } } - return "UNKNOWN_DATA_0x" + Long.toHexString(value); + return null; } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java index b2b3043d367c127cf6ee9dfbd9e65d89c66b72ee..73b3e505ec7105691341c2fa816a03ab5903fb33 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java @@ -1,10 +1,14 @@ package jadx.core.xmlgen; import jadx.core.codegen.CodeWriter; +import jadx.core.utils.android.Res9patchStreamDecoder; +import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.util.ArrayList; @@ -37,8 +41,19 @@ public class ResContainer implements Comparable { public static ResContainer singleImageFile(String name, InputStream content) { ResContainer resContainer = new ResContainer(name, Collections.emptyList()); + InputStream newContent = content; + if (name.endsWith(".9.png")) { + Res9patchStreamDecoder decoder = new Res9patchStreamDecoder(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + decoder.decode(content, os); + } catch (JadxException e) { + e.printStackTrace(); + } + newContent = new ByteArrayInputStream(os.toByteArray()); + } try { - resContainer.image = ImageIO.read(content); + resContainer.image = ImageIO.read(newContent); } catch (Exception e) { throw new JadxRuntimeException("Image load error", e); } 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 8e776923ef74d551b85a0988bfab7ca851752620..9b4f560cfd70642fc8b9566ccd4f5305959451ae 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -87,6 +87,10 @@ public class ResTableParser extends CommonBinaryParser { return resStorage; } + public String[] getStrings() { + return strings; + } + void decodeTableChunk() throws IOException { is.checkInt16(RES_TABLE_TYPE, "Not a table chunk"); is.checkInt16(0x000c, "Unexpected table header size"); @@ -250,29 +254,109 @@ public class ResTableParser extends CommonBinaryParser { int orientation = is.readInt8(); int touchscreen = is.readInt8(); int density = is.readInt16(); - /* + + if (density != 0) { + config.setDensity(parseDensity(density)); + } + is.readInt8(); // keyboard is.readInt8(); // navigation is.readInt8(); // inputFlags is.readInt8(); // inputPad0 - is.readInt16(); // screenWidth - is.readInt16(); // screenHeight + int screenWidth = is.readInt16(); + int screenHeight = is.readInt16(); + + if (screenWidth != 0 && screenHeight != 0) { + config.setScreenSize(screenWidth + "x" + screenHeight); + } + + int sdkVersion = is.readInt16(); + + if (sdkVersion != 0) { + config.setSdkVersion("v" + sdkVersion); + } + + int minorVersion = is.readInt16(); + + int screenLayout = is.readInt8(); + int uiMode = is.readInt8(); + int smallestScreenWidthDp = is.readInt16(); + + int screenWidthDp = is.readInt16(); + int screenHeightDp = is.readInt16(); + + if (screenLayout != 0) { + config.setScreenLayout(parseScreenLayout(screenLayout)); + } + + if (smallestScreenWidthDp != 0) { + config.setSmallestScreenWidthDp("sw" + smallestScreenWidthDp + "dp"); + } + + if (orientation != 0) { + config.setOrientation(parseOrientation(orientation)); + } - is.readInt16(); // sdkVersion - is.readInt16(); // minorVersion + if (screenWidthDp != 0) { + config.setScreenWidthDp("w" + screenWidthDp + "dp"); + } - is.readInt8(); // screenLayout - is.readInt8(); // uiMode - is.readInt16(); // smallestScreenWidthDp + if (screenHeightDp != 0) { + config.setScreenHeightDp("h" + screenHeightDp + "dp"); + } - is.readInt16(); // screenWidthDp - is.readInt16(); // screenHeightDp - */ is.skipToPos(start + size, "Skip config parsing"); return config; } + private String parseOrientation(int orientation) { + if (orientation == 1) { + return "port"; + } else if (orientation == 2) { + return "land"; + } else { + return "o" + orientation; + } + } + + private String parseScreenLayout(int screenLayout) { + switch (screenLayout) { + case 1: + return "small"; + case 2: + return "normal"; + case 3: + return "large"; + case 4: + return "xlarge"; + case 64: + return "ldltr"; + case 128: + return "ldrtl"; + default: + return "sl" + screenLayout; + } + } + + private String parseDensity(int density) { + if (density == 120) { + return "ldpi"; + } else if (density == 160) { + return "mdpi"; + } else if (density == 240) { + return "hdpi"; + } else if (density == 320) { + return "xhdpi"; + } else if (density == 480) { + return "xxhdpi"; + } else if (density == 640) { + return "xxxhdpi"; + } else { + return density + "dpi"; + } + } + private String parseLocale() throws IOException { int b1 = is.readInt8(); int b2 = is.readInt8(); 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 cdd757791c270a4d39bf3ec1c6d4988dcee81822..bf8f5ea7cced5338070440da2c15b6d49af9d726 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -6,14 +6,7 @@ import jadx.core.xmlgen.entry.RawNamedValue; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; public class ResXmlGen { @@ -66,47 +59,127 @@ public class ResXmlGen { private void addValue(CodeWriter cw, ResourceEntry ri) { if (ri.getSimpleValue() != null) { String valueStr = vp.decodeValue(ri.getSimpleValue()); - addSimpleValue(cw, ri.getTypeName(), "name", ri.getKeyName(), valueStr); + addSimpleValue(cw, ri.getTypeName(), ri.getTypeName(), "name", ri.getKeyName(), valueStr); } else { cw.startLine(); cw.add('<').add(ri.getTypeName()).add(' '); - cw.add("name=\"").add(ri.getKeyName()).add("\">"); + String itemTag = "item"; + if (ri.getTypeName().equals("attr") && ri.getNamedValues().size() > 0) { + cw.add("name=\"").add(ri.getKeyName()); + int type = ri.getNamedValues().get(0).getRawValue().getData(); + if ((type & ValuesParser.ATTR_TYPE_ENUM) != 0) { + itemTag = "enum"; + } else if ((type & ValuesParser.ATTR_TYPE_FLAGS) != 0) { + itemTag = "flag"; + } + String formatValue = getTypeAsString(type); + if (formatValue != null) { + cw.add("\" format=\"").add(formatValue); + } + cw.add("\">"); + } else { + cw.add("name=\"").add(ri.getKeyName()).add("\">"); + } cw.incIndent(); for (RawNamedValue value : ri.getNamedValues()) { - addItem(cw, value); + addItem(cw, itemTag, ri.getTypeName(), value); } cw.decIndent(); cw.startLine().add("'); } } - private void addItem(CodeWriter cw, RawNamedValue value) { - String keyName = null; - String keyValue = null; - int nameRef = value.getNameRef(); - if (ParserConstants.isResInternalId(nameRef)) { - keyValue = ParserConstants.PLURALS_MAP.get(nameRef); - if (keyValue != null) { - keyName = "quantity"; - } + private String getTypeAsString(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); + } + + + private void addItem(CodeWriter cw, String itemTag, String typeName, RawNamedValue value) { + String nameStr = vp.decodeNameRef(value.getNameRef()); String valueStr = vp.decodeValue(value.getRawValue()); - addSimpleValue(cw, "item", keyName, keyValue, valueStr); + if (!typeName.equals("attr")) { + if (valueStr.equals("0")) { + valueStr = "@null"; + } + if (nameStr != null) { + try { + int intVal = Integer.parseInt(valueStr); + String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:attr.", ""), intVal); + if (newVal != null) { + valueStr = newVal; + } + } catch (NumberFormatException ignored) { + } + } + } + if (typeName.equals("attr")) { + if (nameStr != null) { + addSimpleValue(cw, typeName, itemTag, nameStr, valueStr, ""); + } + } else if (typeName.equals("style")) { + if (nameStr != null) { + addSimpleValue(cw, typeName, itemTag, nameStr, "", valueStr); + } + } else { + addSimpleValue(cw, typeName, itemTag, null, null, valueStr); + } } - private void addSimpleValue(CodeWriter cw, String typeName, String attrName, String attrValue, String valueStr) { + private void addSimpleValue(CodeWriter cw, String typeName, String itemTag, String attrName, String attrValue, String valueStr) { + if (valueStr.startsWith("res/")) { + // remove duplicated resources. + return; + } cw.startLine(); - cw.add('<').add(typeName); + cw.add('<').add(itemTag); if (attrName != null && attrValue != null) { - cw.add(' ').add(attrName).add("=\"").add(attrValue).add('"'); + if (typeName.equals("attr")) { + cw.add(' ').add("name=\"").add(attrName.replace("id.", "")).add("\" value=\"").add(attrValue).add("\""); + } else if (typeName.equals("style")) { + cw.add(' ').add("name=\"").add(attrName.replace("attr.", "")).add("\""); + } else { + cw.add(' ').add(attrName).add("=\"").add(attrValue).add('"'); + } } - cw.add('>'); - if (typeName.equals("string")) { - cw.add(StringUtils.escapeResStrValue(valueStr)); + if (valueStr.equals("")) { + cw.add(" />"); } else { - cw.add(StringUtils.escapeResValue(valueStr)); + cw.add('>'); + if (itemTag.equals("string")) { + cw.add(StringUtils.escapeResStrValue(valueStr)); + } else { + cw.add(StringUtils.escapeResValue(valueStr)); + } + cw.add("'); } - cw.add("'); } private String getFileName(ResourceEntry ri) { diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java index 91ab3550950c0ef63a3d2c8be3fe61529593b436..d19b84666dd152e3ee7aa788727b17365f8f2410 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java @@ -3,34 +3,93 @@ package jadx.core.xmlgen.entry; public class EntryConfig { private String language; private String country; - - public void setLanguage(String language) { - this.language = language; - } + private String density; + private String screenSize; + private String sdkVersion; + private String screenLayout; + private String smallestScreenWidthDp; + private String orientation; + private String screenWidthDp; + private String screenHeightDp; public String getLanguage() { return language; } - public void setCountry(String country) { - this.country = country; + public void setLanguage(String language) { + this.language = language; } public String getCountry() { return country; } + public void setCountry(String country) { + this.country = country; + } + public String getLocale() { StringBuilder sb = new StringBuilder(); + if (screenSize != null) { + if (sb.length() != 0) { + sb.append("-"); + } + sb.append(screenSize); + } else if (screenHeightDp != null) { + if (sb.length() != 0) { + sb.append("-"); + } + sb.append(screenHeightDp); + } else if (screenWidthDp != null) { + if (sb.length() != 0) { + sb.append("-"); + } + sb.append(screenWidthDp); + } else if (screenLayout != null) { + if (sb.length() != 0) { + sb.append("-"); + } + sb.append(screenLayout); + } else if (smallestScreenWidthDp != null) { + if (sb.length() != 0) { + sb.append("-"); + } + sb.append(smallestScreenWidthDp); + } else if (density != null) { + sb.append(density); + } if (language != null) { + if (sb.length() != 0) { + sb.append("-"); + } sb.append(language); } if (country != null) { sb.append("-r").append(country); } + if (orientation != null) { + if (sb.length() != 0) { + sb.append("-"); + } + sb.append(orientation); + } + if (sdkVersion != null) { + if (sb.length() != 0) { + sb.append("-"); + } + sb.append(sdkVersion); + } return sb.toString(); } + public String getDensity() { + return density; + } + + public void setDensity(String density) { + this.density = density; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -41,4 +100,60 @@ public class EntryConfig { } return sb.toString(); } + + public void setScreenSize(String screenSize) { + this.screenSize = screenSize; + } + + public String getScreenSize() { + return screenSize; + } + + public void setSdkVersion(String sdkVersion) { + this.sdkVersion = sdkVersion; + } + + public String getSdkVersion() { + return sdkVersion; + } + + public void setScreenLayout(String screenLayout) { + this.screenLayout = screenLayout; + } + + public String getScreenLayout() { + return screenLayout; + } + + public void setSmallestScreenWidthDp(String smallestScreenWidthDp) { + this.smallestScreenWidthDp = smallestScreenWidthDp; + } + + public String getSmallestScreenWidthDp() { + return smallestScreenWidthDp; + } + + public void setOrientation(String orientation) { + this.orientation = orientation; + } + + public String getOrientation() { + return orientation; + } + + public void setScreenWidthDp(String screenWidthDp) { + this.screenWidthDp = screenWidthDp; + } + + public String getScreenWidthDp() { + return screenWidthDp; + } + + public void setScreenHeightDp(String screenHeightDp) { + this.screenHeightDp = screenHeightDp; + } + + public String getScreenHeightDp() { + return screenHeightDp; + } } 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 bc236f0eaf5b6d3ae586bbfa6f71431a08a18114..6984e0d3698176c50e055398d28db5ab103e1563 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,7 +1,11 @@ package jadx.core.xmlgen.entry; import jadx.core.xmlgen.ParserConstants; +import jadx.core.xmlgen.ResTableParser; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; @@ -16,9 +20,28 @@ public class ValuesParser extends ParserConstants { private final String[] strings; private final Map resMap; + public static String[] androidStrings; + public static Map androidResMap; + public ValuesParser(String[] strings, Map resMap) { this.strings = strings; this.resMap = resMap; + + if (androidStrings == null && androidResMap == null) { + try { + decodeAndroid(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private void decodeAndroid() throws IOException { + InputStream inputStream = new BufferedInputStream(getClass().getResourceAsStream("/resources.arsc")); + ResTableParser androidParser = new ResTableParser(); + androidParser.decode(inputStream); + androidStrings = androidParser.getStrings(); + androidResMap = androidParser.getResStorage().getResourcesNames(); } public String getValueString(ResourceEntry ri) { @@ -73,6 +96,11 @@ public class ValuesParser extends ParserConstants { case TYPE_REFERENCE: { String ri = resMap.get(data); if (ri == null) { + String androidRi = androidResMap.get(data); + if (androidRi != null) { + return "@android:" + androidRi; + } + if (data == 0) return "0"; return "?unknown_ref: " + Integer.toHexString(data); } return "@" + ri; @@ -81,6 +109,10 @@ public class ValuesParser extends ParserConstants { case TYPE_ATTRIBUTE: { String ri = resMap.get(data); if (ri == null) { + String androidRi = androidResMap.get(data); + if (androidRi != null) { + return "?android:" + androidRi; + } return "?unknown_attr_ref: " + Integer.toHexString(data); } return "?" + ri; @@ -97,7 +129,7 @@ public class ValuesParser extends ParserConstants { } } - private String decodeNameRef(int nameRef) { + public String decodeNameRef(int nameRef) { int ref = nameRef; if (isResInternalId(nameRef)) { ref = nameRef & ATTR_TYPE_ANY; @@ -108,6 +140,11 @@ public class ValuesParser extends ParserConstants { String ri = resMap.get(ref); if (ri != null) { return ri.replace('/', '.'); + } else { + String androidRi = androidResMap.get(ref); + if (androidRi != null) { + return "android:" + androidRi.replace('/', '.'); + } } return "?0x" + Integer.toHexString(nameRef); } diff --git a/jadx-core/src/main/resources/resources.arsc b/jadx-core/src/main/resources/resources.arsc new file mode 100644 index 0000000000000000000000000000000000000000..ae33d6327d5ad7a2ff30332b7a341be9d574066a Binary files /dev/null and b/jadx-core/src/main/resources/resources.arsc differ