Commit 9cd46e74 authored by binjia.zhou's avatar binjia.zhou Committed by skylot

fix some xml generate issues

parent 57812204
/**
* Copyright 2014 Ryszard Wiśniewski <brut.alll@gmail.com>
*
* 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 <brut.alll@gmail.com>
*/
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
/**
* Copyright 2014 Ryszard Wiśniewski <brut.alll@gmail.com>
*
* 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 <brut.alll@gmail.com>
*/
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
/**
* Copyright 2014 Ryszard Wiśniewski <brut.alll@gmail.com>
*
* 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 <brut.alll@gmail.com>
*/
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
...@@ -6,7 +6,6 @@ import jadx.core.dex.info.ConstStorage; ...@@ -6,7 +6,6 @@ import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.entry.ValuesParser; import jadx.core.xmlgen.entry.ValuesParser;
...@@ -35,27 +34,28 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -35,27 +34,28 @@ public class BinaryXMLParser extends CommonBinaryParser {
private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class); private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class);
private static final String ANDROID_R_STYLE_CLS = "android.R$style"; private static final String ANDROID_R_STYLE_CLS = "android.R$style";
private static final boolean ATTR_NEW_LINE = false; private static final boolean ATTR_NEW_LINE = false;
private final Map<Integer, String> styleMap = new HashMap<Integer, String>();
private final Map<Integer, FieldNode> localStyleMap = new HashMap<Integer, FieldNode>();
private final Map<Integer, String> resNames;
private final Map<String, String> nsMap = new HashMap<>();
private CodeWriter writer; private CodeWriter writer;
private String[] strings; private String[] strings;
private String nsPrefix = "ERROR";
private String nsURI = "ERROR";
private String currentTag = "ERROR"; private String currentTag = "ERROR";
private boolean firstElement; private boolean firstElement;
private boolean wasOneLiner = false;
private final Map<Integer, String> styleMap = new HashMap<>();
private final Map<Integer, FieldNode> localStyleMap = new HashMap<>();
private final Map<Integer, String> resNames;
private ValuesParser valuesParser; private ValuesParser valuesParser;
private boolean isLastEnd = true;
private final ManifestAttributes attributes; private boolean isOneLine = true;
public BinaryXMLParser(RootNode root) { public BinaryXMLParser(RootNode root) {
try { 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 // add application constants
ConstStorage constStorage = root.getConstValues(); ConstStorage constStorage = root.getConstValues();
Map<Object, FieldNode> constFields = constStorage.getGlobalConstFields(); Map<Object, FieldNode> constFields = constStorage.getGlobalConstFields();
...@@ -67,25 +67,11 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -67,25 +67,11 @@ public class BinaryXMLParser extends CommonBinaryParser {
} }
} }
resNames = constStorage.getResourcesNames(); resNames = constStorage.getResourcesNames();
attributes = new ManifestAttributes();
attributes.parseAll();
} catch (Exception e) { } catch (Exception e) {
throw new JadxRuntimeException("BinaryXMLParser init error", 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 { public synchronized CodeWriter parse(InputStream inputStream) throws IOException {
is = new ParserStream(inputStream); is = new ParserStream(inputStream);
if (!isBinaryXml()) { if (!isBinaryXml()) {
...@@ -126,12 +112,14 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -126,12 +112,14 @@ public class BinaryXMLParser extends CommonBinaryParser {
parseResourceMap(); parseResourceMap();
break; break;
case RES_XML_START_NAMESPACE_TYPE: case RES_XML_START_NAMESPACE_TYPE:
case RES_XML_END_NAMESPACE_TYPE:
parseNameSpace(); parseNameSpace();
break; break;
case RES_XML_CDATA_TYPE: case RES_XML_CDATA_TYPE:
parseCData(); parseCData();
break; break;
case RES_XML_END_NAMESPACE_TYPE:
parseNameSpaceEnd();
break;
case RES_XML_START_ELEMENT_TYPE: case RES_XML_START_ELEMENT_TYPE:
parseElement(); parseElement();
break; break;
...@@ -164,12 +152,25 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -164,12 +152,25 @@ public class BinaryXMLParser extends CommonBinaryParser {
if (is.readInt32() != 0x18) { if (is.readInt32() != 0x18) {
die("NAMESPACE header chunk is not 0x18 big"); die("NAMESPACE header chunk is not 0x18 big");
} }
int lineNumber = is.readInt32(); int beginLineNumber = is.readInt32();
int comment = is.readInt32(); int comment = is.readInt32();
int idPrefix = is.readInt32(); int beginPrefix = is.readInt32();
nsPrefix = strings[idPrefix]; int beginURI = is.readInt32();
int idURI = is.readInt32(); nsMap.computeIfAbsent(strings[beginURI], k -> strings[beginPrefix]);
nsURI = strings[idURI]; }
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 { private void parseCData() throws IOException {
...@@ -185,11 +186,13 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -185,11 +186,13 @@ public class BinaryXMLParser extends CommonBinaryParser {
int strIndex = is.readInt32(); int strIndex = is.readInt32();
String str = strings[strIndex]; 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); is.skip(size - 2);
} }
...@@ -208,11 +211,11 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -208,11 +211,11 @@ public class BinaryXMLParser extends CommonBinaryParser {
int comment = is.readInt32(); int comment = is.readInt32();
int startNS = is.readInt32(); int startNS = is.readInt32();
int startNSName = is.readInt32(); // actually is elementName... int startNSName = is.readInt32(); // actually is elementName...
if (!wasOneLiner && !"ERROR".equals(currentTag) if (!isLastEnd && !"ERROR".equals(currentTag)) {
&& !currentTag.equals(strings[startNSName])) {
writer.add(">"); writer.add(">");
} }
wasOneLiner = false; isOneLine = true;
isLastEnd = false;
currentTag = strings[startNSName]; currentTag = strings[startNSName];
writer.startLine("<").add(currentTag); writer.startLine("<").add(currentTag);
writer.attachSourceLine(elementBegLineNumber); writer.attachSourceLine(elementBegLineNumber);
...@@ -229,9 +232,11 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -229,9 +232,11 @@ public class BinaryXMLParser extends CommonBinaryParser {
int classIndex = is.readInt16(); int classIndex = is.readInt16();
int styleIndex = is.readInt16(); int styleIndex = is.readInt16();
if ("manifest".equals(currentTag) || writer.getIndent() == 0) { if ("manifest".equals(currentTag) || writer.getIndent() == 0) {
writer.add(" xmlns:android=\"").add(nsURI).add("\""); for (Map.Entry<String, String> 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++) { for (int i = 0; i < attributeCount; i++) {
parseAttribute(i, attrNewLine); parseAttribute(i, attrNewLine);
} }
...@@ -258,10 +263,10 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -258,10 +263,10 @@ public class BinaryXMLParser extends CommonBinaryParser {
writer.add(' '); writer.add(' ');
} }
if (attributeNS != -1) { if (attributeNS != -1) {
writer.add(nsPrefix).add(':'); writer.add(nsMap.get(strings[attributeNS])).add(':');
} }
writer.add(attrName).add("=\""); writer.add(attrName).add("=\"");
String decodedAttr = attributes.decode(attrName, attrValData); String decodedAttr = ManifestAttributes.getInstance().decode(attrName, attrValData);
if (decodedAttr != null) { if (decodedAttr != null) {
writer.add(decodedAttr); writer.add(decodedAttr);
} else { } else {
...@@ -275,10 +280,11 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -275,10 +280,11 @@ public class BinaryXMLParser extends CommonBinaryParser {
// reference custom processing // reference custom processing
String name = styleMap.get(attrValData); String name = styleMap.get(attrValData);
if (name != null) { if (name != null) {
writer.add("@*"); writer.add("@");
if (attributeNS != -1) { 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("_", ".")); writer.add("style/").add(name.replaceAll("_", "."));
} else { } else {
FieldNode field = localStyleMap.get(attrValData); FieldNode field = localStyleMap.get(attrValData);
...@@ -292,9 +298,20 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -292,9 +298,20 @@ public class BinaryXMLParser extends CommonBinaryParser {
} else { } else {
String resName = resNames.get(attrValData); String resName = resNames.get(attrValData);
if (resName != null) { if (resName != null) {
writer.add("@").add(resName); writer.add("@");
if (resName.startsWith("id/")) {
writer.add("+");
}
writer.add(resName);
} else { } 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 { ...@@ -315,9 +332,8 @@ public class BinaryXMLParser extends CommonBinaryParser {
int comment = is.readInt32(); int comment = is.readInt32();
int elementNS = is.readInt32(); int elementNS = is.readInt32();
int elementName = is.readInt32(); int elementName = is.readInt32();
if (currentTag.equals(strings[elementName])) { if (currentTag.equals(strings[elementName]) && isOneLine && !isLastEnd) {
writer.add(" />"); writer.add(" />");
wasOneLiner = true;
} else { } else {
writer.startLine("</"); writer.startLine("</");
writer.attachSourceLine(endLineNumber); writer.attachSourceLine(endLineNumber);
...@@ -326,6 +342,7 @@ public class BinaryXMLParser extends CommonBinaryParser { ...@@ -326,6 +342,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
} }
writer.add(strings[elementName]).add(">"); writer.add(strings[elementName]).add(">");
} }
isLastEnd = true;
if (writer.getIndent() != 0) { if (writer.getIndent() != 0) {
writer.decIndent(); writer.decIndent();
} }
......
...@@ -49,7 +49,24 @@ public class ManifestAttributes { ...@@ -49,7 +49,24 @@ public class ManifestAttributes {
private final Map<String, MAttr> attrMap = new HashMap<>(); private final Map<String, MAttr> 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(ATTR_XML));
parse(loadXML(MANIFEST_ATTR_XML)); parse(loadXML(MANIFEST_ATTR_XML));
LOG.debug("Loaded android attributes count: {}", attrMap.size()); LOG.debug("Loaded android attributes count: {}", attrMap.size());
...@@ -158,7 +175,10 @@ public class ManifestAttributes { ...@@ -158,7 +175,10 @@ public class ManifestAttributes {
} else if (attr.getType() == MAttrType.FLAG) { } else if (attr.getType() == MAttrType.FLAG) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (Map.Entry<Long, String> entry : attr.getValues().entrySet()) { for (Map.Entry<Long, String> 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('|'); sb.append(entry.getValue()).append('|');
} }
} }
...@@ -166,6 +186,6 @@ public class ManifestAttributes { ...@@ -166,6 +186,6 @@ public class ManifestAttributes {
return sb.deleteCharAt(sb.length() - 1).toString(); return sb.deleteCharAt(sb.length() - 1).toString();
} }
} }
return "UNKNOWN_DATA_0x" + Long.toHexString(value); return null;
} }
} }
package jadx.core.xmlgen; package jadx.core.xmlgen;
import jadx.core.codegen.CodeWriter; import jadx.core.codegen.CodeWriter;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -37,8 +41,19 @@ public class ResContainer implements Comparable<ResContainer> { ...@@ -37,8 +41,19 @@ public class ResContainer implements Comparable<ResContainer> {
public static ResContainer singleImageFile(String name, InputStream content) { public static ResContainer singleImageFile(String name, InputStream content) {
ResContainer resContainer = new ResContainer(name, Collections.<ResContainer>emptyList()); ResContainer resContainer = new ResContainer(name, Collections.<ResContainer>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 { try {
resContainer.image = ImageIO.read(content); resContainer.image = ImageIO.read(newContent);
} catch (Exception e) { } catch (Exception e) {
throw new JadxRuntimeException("Image load error", e); throw new JadxRuntimeException("Image load error", e);
} }
......
...@@ -87,6 +87,10 @@ public class ResTableParser extends CommonBinaryParser { ...@@ -87,6 +87,10 @@ public class ResTableParser extends CommonBinaryParser {
return resStorage; return resStorage;
} }
public String[] getStrings() {
return strings;
}
void decodeTableChunk() throws IOException { void decodeTableChunk() throws IOException {
is.checkInt16(RES_TABLE_TYPE, "Not a table chunk"); is.checkInt16(RES_TABLE_TYPE, "Not a table chunk");
is.checkInt16(0x000c, "Unexpected table header size"); is.checkInt16(0x000c, "Unexpected table header size");
...@@ -250,29 +254,109 @@ public class ResTableParser extends CommonBinaryParser { ...@@ -250,29 +254,109 @@ public class ResTableParser extends CommonBinaryParser {
int orientation = is.readInt8(); int orientation = is.readInt8();
int touchscreen = is.readInt8(); int touchscreen = is.readInt8();
int density = is.readInt16(); int density = is.readInt16();
/*
if (density != 0) {
config.setDensity(parseDensity(density));
}
is.readInt8(); // keyboard is.readInt8(); // keyboard
is.readInt8(); // navigation is.readInt8(); // navigation
is.readInt8(); // inputFlags is.readInt8(); // inputFlags
is.readInt8(); // inputPad0 is.readInt8(); // inputPad0
is.readInt16(); // screenWidth int screenWidth = is.readInt16();
is.readInt16(); // screenHeight 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 if (screenWidthDp != 0) {
is.readInt16(); // minorVersion config.setScreenWidthDp("w" + screenWidthDp + "dp");
}
is.readInt8(); // screenLayout if (screenHeightDp != 0) {
is.readInt8(); // uiMode config.setScreenHeightDp("h" + screenHeightDp + "dp");
is.readInt16(); // smallestScreenWidthDp }
is.readInt16(); // screenWidthDp
is.readInt16(); // screenHeightDp
*/
is.skipToPos(start + size, "Skip config parsing"); is.skipToPos(start + size, "Skip config parsing");
return config; 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 { private String parseLocale() throws IOException {
int b1 = is.readInt8(); int b1 = is.readInt8();
int b2 = is.readInt8(); int b2 = is.readInt8();
......
...@@ -6,14 +6,7 @@ import jadx.core.xmlgen.entry.RawNamedValue; ...@@ -6,14 +6,7 @@ import jadx.core.xmlgen.entry.RawNamedValue;
import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser; import jadx.core.xmlgen.entry.ValuesParser;
import java.util.ArrayList; import java.util.*;
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;
public class ResXmlGen { public class ResXmlGen {
...@@ -66,47 +59,127 @@ public class ResXmlGen { ...@@ -66,47 +59,127 @@ public class ResXmlGen {
private void addValue(CodeWriter cw, ResourceEntry ri) { private void addValue(CodeWriter cw, ResourceEntry ri) {
if (ri.getSimpleValue() != null) { if (ri.getSimpleValue() != null) {
String valueStr = vp.decodeValue(ri.getSimpleValue()); 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 { } else {
cw.startLine(); cw.startLine();
cw.add('<').add(ri.getTypeName()).add(' '); 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(); cw.incIndent();
for (RawNamedValue value : ri.getNamedValues()) { for (RawNamedValue value : ri.getNamedValues()) {
addItem(cw, value); addItem(cw, itemTag, ri.getTypeName(), value);
} }
cw.decIndent(); cw.decIndent();
cw.startLine().add("</").add(ri.getTypeName()).add('>'); cw.startLine().add("</").add(ri.getTypeName()).add('>');
} }
} }
private void addItem(CodeWriter cw, RawNamedValue value) { private String getTypeAsString(int type) {
String keyName = null; String s = "";
String keyValue = null; if ((type & ValuesParser.ATTR_TYPE_REFERENCE) != 0) {
int nameRef = value.getNameRef(); s += "|reference";
if (ParserConstants.isResInternalId(nameRef)) { }
keyValue = ParserConstants.PLURALS_MAP.get(nameRef); if ((type & ValuesParser.ATTR_TYPE_STRING) != 0) {
if (keyValue != null) { s += "|string";
keyName = "quantity"; }
} 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()); 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.startLine();
cw.add('<').add(typeName); cw.add('<').add(itemTag);
if (attrName != null && attrValue != null) { 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 (valueStr.equals("")) {
if (typeName.equals("string")) { cw.add(" />");
cw.add(StringUtils.escapeResStrValue(valueStr));
} else { } 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("</").add(itemTag).add('>');
} }
cw.add("</").add(typeName).add('>');
} }
private String getFileName(ResourceEntry ri) { private String getFileName(ResourceEntry ri) {
......
...@@ -3,34 +3,93 @@ package jadx.core.xmlgen.entry; ...@@ -3,34 +3,93 @@ package jadx.core.xmlgen.entry;
public class EntryConfig { public class EntryConfig {
private String language; private String language;
private String country; private String country;
private String density;
public void setLanguage(String language) { private String screenSize;
this.language = language; private String sdkVersion;
} private String screenLayout;
private String smallestScreenWidthDp;
private String orientation;
private String screenWidthDp;
private String screenHeightDp;
public String getLanguage() { public String getLanguage() {
return language; return language;
} }
public void setCountry(String country) { public void setLanguage(String language) {
this.country = country; this.language = language;
} }
public String getCountry() { public String getCountry() {
return country; return country;
} }
public void setCountry(String country) {
this.country = country;
}
public String getLocale() { public String getLocale() {
StringBuilder sb = new StringBuilder(); 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 (language != null) {
if (sb.length() != 0) {
sb.append("-");
}
sb.append(language); sb.append(language);
} }
if (country != null) { if (country != null) {
sb.append("-r").append(country); 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(); return sb.toString();
} }
public String getDensity() {
return density;
}
public void setDensity(String density) {
this.density = density;
}
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
...@@ -41,4 +100,60 @@ public class EntryConfig { ...@@ -41,4 +100,60 @@ public class EntryConfig {
} }
return sb.toString(); 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;
}
} }
package jadx.core.xmlgen.entry; package jadx.core.xmlgen.entry;
import jadx.core.xmlgen.ParserConstants; 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.text.NumberFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -16,9 +20,28 @@ public class ValuesParser extends ParserConstants { ...@@ -16,9 +20,28 @@ public class ValuesParser extends ParserConstants {
private final String[] strings; private final String[] strings;
private final Map<Integer, String> resMap; private final Map<Integer, String> resMap;
public static String[] androidStrings;
public static Map<Integer, String> androidResMap;
public ValuesParser(String[] strings, Map<Integer, String> resMap) { public ValuesParser(String[] strings, Map<Integer, String> resMap) {
this.strings = strings; this.strings = strings;
this.resMap = resMap; 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) { public String getValueString(ResourceEntry ri) {
...@@ -73,6 +96,11 @@ public class ValuesParser extends ParserConstants { ...@@ -73,6 +96,11 @@ public class ValuesParser extends ParserConstants {
case TYPE_REFERENCE: { case TYPE_REFERENCE: {
String ri = resMap.get(data); String ri = resMap.get(data);
if (ri == null) { 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 "?unknown_ref: " + Integer.toHexString(data);
} }
return "@" + ri; return "@" + ri;
...@@ -81,6 +109,10 @@ public class ValuesParser extends ParserConstants { ...@@ -81,6 +109,10 @@ public class ValuesParser extends ParserConstants {
case TYPE_ATTRIBUTE: { case TYPE_ATTRIBUTE: {
String ri = resMap.get(data); String ri = resMap.get(data);
if (ri == null) { if (ri == null) {
String androidRi = androidResMap.get(data);
if (androidRi != null) {
return "?android:" + androidRi;
}
return "?unknown_attr_ref: " + Integer.toHexString(data); return "?unknown_attr_ref: " + Integer.toHexString(data);
} }
return "?" + ri; return "?" + ri;
...@@ -97,7 +129,7 @@ public class ValuesParser extends ParserConstants { ...@@ -97,7 +129,7 @@ public class ValuesParser extends ParserConstants {
} }
} }
private String decodeNameRef(int nameRef) { public String decodeNameRef(int nameRef) {
int ref = nameRef; int ref = nameRef;
if (isResInternalId(nameRef)) { if (isResInternalId(nameRef)) {
ref = nameRef & ATTR_TYPE_ANY; ref = nameRef & ATTR_TYPE_ANY;
...@@ -108,6 +140,11 @@ public class ValuesParser extends ParserConstants { ...@@ -108,6 +140,11 @@ public class ValuesParser extends ParserConstants {
String ri = resMap.get(ref); String ri = resMap.get(ref);
if (ri != null) { if (ri != null) {
return ri.replace('/', '.'); return ri.replace('/', '.');
} else {
String androidRi = androidResMap.get(ref);
if (androidRi != null) {
return "android:" + androidRi.replace('/', '.');
}
} }
return "?0x" + Integer.toHexString(nameRef); return "?0x" + Integer.toHexString(nameRef);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment