resources: initial version of .arsc file decode
This commit is contained in:
Binary file not shown.
@@ -3,7 +3,6 @@ package jadx.api;
|
||||
import jadx.core.Jadx;
|
||||
import jadx.core.ProcessClass;
|
||||
import jadx.core.codegen.CodeGen;
|
||||
import jadx.core.codegen.CodeWriter;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
@@ -15,6 +14,7 @@ import jadx.core.utils.exceptions.JadxException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.InputFile;
|
||||
import jadx.core.xmlgen.BinaryXMLParser;
|
||||
import jadx.core.xmlgen.ResourcesSaver;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -150,7 +150,7 @@ public final class JadxDecompiler {
|
||||
return getSaveExecutor(!args.isSkipSources(), !args.isSkipResources());
|
||||
}
|
||||
|
||||
private ExecutorService getSaveExecutor(boolean saveSources, boolean saveResources) {
|
||||
private ExecutorService getSaveExecutor(boolean saveSources, final boolean saveResources) {
|
||||
if (root == null) {
|
||||
throw new JadxRuntimeException("No loaded files");
|
||||
}
|
||||
@@ -172,17 +172,7 @@ public final class JadxDecompiler {
|
||||
}
|
||||
if (saveResources) {
|
||||
for (final ResourceFile resourceFile : getResources()) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (ResourceType.isSupportedForUnpack(resourceFile.getType())) {
|
||||
CodeWriter cw = resourceFile.getContent();
|
||||
if (cw != null) {
|
||||
cw.save(new File(outDir, resourceFile.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
executor.execute(new ResourcesSaver(outDir, resourceFile));
|
||||
}
|
||||
}
|
||||
return executor;
|
||||
@@ -294,7 +284,7 @@ public final class JadxDecompiler {
|
||||
return root;
|
||||
}
|
||||
|
||||
BinaryXMLParser getXmlParser() {
|
||||
synchronized BinaryXMLParser getXmlParser() {
|
||||
if (xmlParser == null) {
|
||||
xmlParser = new BinaryXMLParser(root);
|
||||
}
|
||||
@@ -321,4 +311,5 @@ public final class JadxDecompiler {
|
||||
public String toString() {
|
||||
return "jadx decompiler " + getVersion();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package jadx.api;
|
||||
|
||||
import jadx.core.codegen.CodeWriter;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class ResourceFile {
|
||||
return type;
|
||||
}
|
||||
|
||||
public CodeWriter getContent() {
|
||||
public ResContainer getContent() {
|
||||
return ResourcesLoader.loadContent(decompiler, this);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package jadx.api;
|
||||
|
||||
import jadx.core.codegen.CodeWriter;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
|
||||
public class ResourceFileContent extends ResourceFile {
|
||||
|
||||
private final CodeWriter content;
|
||||
|
||||
public ResourceFileContent(String name, ResourceType type, CodeWriter content) {
|
||||
super(null, name, type);
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResContainer getContent() {
|
||||
return ResContainer.singleFile(getName(), content);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ public enum ResourceType {
|
||||
public static boolean isSupportedForUnpack(ResourceType type) {
|
||||
switch (type) {
|
||||
case CODE:
|
||||
case ARSC:
|
||||
case LIB:
|
||||
case FONT:
|
||||
case IMG:
|
||||
@@ -43,6 +42,7 @@ public enum ResourceType {
|
||||
|
||||
case MANIFEST:
|
||||
case XML:
|
||||
case ARSC:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -5,6 +5,7 @@ import jadx.core.codegen.CodeWriter;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
import jadx.core.utils.files.InputFile;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
import jadx.core.xmlgen.ResTableParser;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
@@ -43,17 +44,17 @@ public final class ResourcesLoader {
|
||||
}
|
||||
|
||||
public interface ResourceDecoder {
|
||||
Object decode(long size, InputStream is) throws IOException;
|
||||
ResContainer decode(long size, InputStream is) throws IOException;
|
||||
}
|
||||
|
||||
public static Object decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException {
|
||||
public static ResContainer decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException {
|
||||
ZipRef zipRef = rf.getZipRef();
|
||||
if (zipRef == null) {
|
||||
return null;
|
||||
}
|
||||
ZipFile zipFile = null;
|
||||
InputStream inputStream = null;
|
||||
Object result = null;
|
||||
ResContainer result = null;
|
||||
try {
|
||||
zipFile = new ZipFile(zipRef.getZipFile());
|
||||
ZipEntry entry = zipFile.getEntry(zipRef.getEntryName());
|
||||
@@ -79,16 +80,17 @@ public final class ResourcesLoader {
|
||||
return result;
|
||||
}
|
||||
|
||||
static CodeWriter loadContent(final JadxDecompiler jadxRef, final ResourceFile rf) {
|
||||
static ResContainer loadContent(final JadxDecompiler jadxRef, final ResourceFile rf) {
|
||||
try {
|
||||
return (CodeWriter) decodeStream(rf, new ResourceDecoder() {
|
||||
return decodeStream(rf, new ResourceDecoder() {
|
||||
@Override
|
||||
public Object decode(long size, InputStream is) throws IOException {
|
||||
public ResContainer decode(long size, InputStream is) throws IOException {
|
||||
if (size > LOAD_SIZE_LIMIT) {
|
||||
return new CodeWriter().add("File too big, size: "
|
||||
+ String.format("%.2f KB", size / 1024.));
|
||||
return ResContainer.singleFile(rf.getName(),
|
||||
new CodeWriter().add("File too big, size: "
|
||||
+ String.format("%.2f KB", size / 1024.)));
|
||||
}
|
||||
return loadContent(jadxRef, rf.getType(), is);
|
||||
return loadContent(jadxRef, rf, is);
|
||||
}
|
||||
});
|
||||
} catch (JadxException e) {
|
||||
@@ -96,21 +98,22 @@ public final class ResourcesLoader {
|
||||
CodeWriter cw = new CodeWriter();
|
||||
cw.add("Error decode ").add(rf.getType().toString().toLowerCase());
|
||||
cw.startLine(Utils.getStackTrace(e.getCause()));
|
||||
return cw;
|
||||
return ResContainer.singleFile(rf.getName(), cw);
|
||||
}
|
||||
}
|
||||
|
||||
private static CodeWriter loadContent(JadxDecompiler jadxRef, ResourceType type,
|
||||
private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf,
|
||||
InputStream inputStream) throws IOException {
|
||||
switch (type) {
|
||||
switch (rf.getType()) {
|
||||
case MANIFEST:
|
||||
case XML:
|
||||
return jadxRef.getXmlParser().parse(inputStream);
|
||||
return ResContainer.singleFile(rf.getName(),
|
||||
jadxRef.getXmlParser().parse(inputStream));
|
||||
|
||||
case ARSC:
|
||||
return new ResTableParser().decodeToCodeWriter(inputStream);
|
||||
return new ResTableParser().decodeFiles(inputStream);
|
||||
}
|
||||
return loadToCodeWriter(inputStream);
|
||||
return ResContainer.singleFile(rf.getName(), loadToCodeWriter(inputStream));
|
||||
}
|
||||
|
||||
private void loadFile(List<ResourceFile> list, File file) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import jadx.core.utils.ErrorsCounter;
|
||||
import jadx.core.utils.exceptions.DecodeException;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
import jadx.core.utils.files.InputFile;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
import jadx.core.xmlgen.ResTableParser;
|
||||
import jadx.core.xmlgen.ResourceStorage;
|
||||
|
||||
@@ -74,7 +75,7 @@ public class RootNode {
|
||||
try {
|
||||
ResourcesLoader.decodeStream(arsc, new ResourcesLoader.ResourceDecoder() {
|
||||
@Override
|
||||
public Object decode(long size, InputStream is) throws IOException {
|
||||
public ResContainer decode(long size, InputStream is) throws IOException {
|
||||
parser.decode(is);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,30 +27,14 @@ public class StringUtils {
|
||||
|
||||
private static void processChar(int c, StringBuilder res) {
|
||||
switch (c) {
|
||||
case '\n':
|
||||
res.append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
res.append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
res.append("\\t");
|
||||
break;
|
||||
case '\b':
|
||||
res.append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
res.append("\\f");
|
||||
break;
|
||||
case '\'':
|
||||
res.append('\'');
|
||||
break;
|
||||
case '"':
|
||||
res.append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
res.append("\\\\");
|
||||
break;
|
||||
case '\n': res.append("\\n"); break;
|
||||
case '\r': res.append("\\r"); break;
|
||||
case '\t': res.append("\\t"); break;
|
||||
case '\b': res.append("\\b"); break;
|
||||
case '\f': res.append("\\f"); break;
|
||||
case '\'': res.append('\''); break;
|
||||
case '"': res.append("\\\""); break;
|
||||
case '\\': res.append("\\\\"); break;
|
||||
|
||||
default:
|
||||
if (32 <= c && c <= 126) {
|
||||
@@ -114,4 +98,29 @@ public class StringUtils {
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String escapeResValue(String str) {
|
||||
int len = str.length();
|
||||
StringBuilder sb = new StringBuilder(len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = str.charAt(i);
|
||||
switch (c) {
|
||||
case '&': sb.append("&"); break;
|
||||
case '<': sb.append("<"); break;
|
||||
case '>': sb.append(">"); break;
|
||||
case '"': sb.append("""); break;
|
||||
case '\'': sb.append("'"); break;
|
||||
|
||||
case '\n': sb.append("\\n"); break;
|
||||
case '\r': sb.append("\\r"); break;
|
||||
case '\t': sb.append("\\t"); break;
|
||||
case '\b': sb.append("\\b"); break;
|
||||
case '\f': sb.append("\\f"); break;
|
||||
default:
|
||||
sb.append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ 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 CodeWriter writer;
|
||||
private String[] strings;
|
||||
@@ -76,7 +77,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
|
||||
resNames = root.getResourcesNames();
|
||||
|
||||
attributes = new ManifestAttributes();
|
||||
attributes.parse();
|
||||
attributes.parseAll();
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("BinaryXMLParser init error", e);
|
||||
}
|
||||
@@ -221,12 +222,13 @@ 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 (!wasOneLiner && !"ERROR".equals(currentTag)
|
||||
&& !currentTag.equals(strings[startNSName])) {
|
||||
writer.add(">");
|
||||
}
|
||||
wasOneLiner = false;
|
||||
currentTag = strings[startNSName];
|
||||
writer.startLine("<").add(strings[startNSName]);
|
||||
writer.startLine("<").add(currentTag);
|
||||
writer.attachSourceLine(elementBegLineNumber);
|
||||
int attributeStart = is.readInt16();
|
||||
if (attributeStart != 0x14) {
|
||||
@@ -240,22 +242,16 @@ public class BinaryXMLParser extends CommonBinaryParser {
|
||||
int idIndex = is.readInt16();
|
||||
int classIndex = is.readInt16();
|
||||
int styleIndex = is.readInt16();
|
||||
if ("manifest".equals(strings[startNSName])) {
|
||||
writer.add(" xmlns:\"").add(nsURI).add("\"");
|
||||
}
|
||||
if (attributeCount > 0) {
|
||||
writer.add(" ");
|
||||
if ("manifest".equals(currentTag) || writer.getIndent() == 0) {
|
||||
writer.add(" xmlns:android=\"").add(nsURI).add("\"");
|
||||
}
|
||||
boolean attrNewLine = attributeCount == 1 ? false : ATTR_NEW_LINE;
|
||||
for (int i = 0; i < attributeCount; i++) {
|
||||
parseAttribute(i);
|
||||
writer.add('"');
|
||||
if (i + 1 < attributeCount) {
|
||||
writer.add(" ");
|
||||
}
|
||||
parseAttribute(i, attrNewLine);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseAttribute(int i) throws IOException {
|
||||
private void parseAttribute(int i, boolean newLine) throws IOException {
|
||||
int attributeNS = is.readInt32();
|
||||
int attributeName = is.readInt32();
|
||||
int attributeRawValue = is.readInt32();
|
||||
@@ -268,10 +264,16 @@ public class BinaryXMLParser extends CommonBinaryParser {
|
||||
}
|
||||
int attrValDataType = is.readInt8();
|
||||
int attrValData = is.readInt32();
|
||||
|
||||
String attrName = strings[attributeName];
|
||||
if (newLine) {
|
||||
writer.startLine().addIndent();
|
||||
} else {
|
||||
writer.add(' ');
|
||||
}
|
||||
if (attributeNS != -1) {
|
||||
writer.add(nsPrefix).add(':');
|
||||
}
|
||||
String attrName = strings[attributeName];
|
||||
writer.add(attrName).add("=\"");
|
||||
String decodedAttr = attributes.decode(attrName, attrValData);
|
||||
if (decodedAttr != null) {
|
||||
@@ -279,6 +281,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
|
||||
} else {
|
||||
decodeAttribute(attributeNS, attrValDataType, attrValData);
|
||||
}
|
||||
writer.add('"');
|
||||
}
|
||||
|
||||
private void decodeAttribute(int attributeNS, int attrValDataType, int attrValData) {
|
||||
@@ -295,7 +298,11 @@ public class BinaryXMLParser extends CommonBinaryParser {
|
||||
FieldNode field = localStyleMap.get(attrValData);
|
||||
if (field != null) {
|
||||
String cls = field.getParentClass().getShortName().toLowerCase();
|
||||
writer.add("@").add(cls).add("/").add(field.getName());
|
||||
writer.add("@");
|
||||
if ("id".equals(cls)) {
|
||||
writer.add('+');
|
||||
}
|
||||
writer.add(cls).add("/").add(field.getName());
|
||||
} else {
|
||||
String resName = resNames.get(attrValData);
|
||||
if (resName != null) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import jadx.core.utils.exceptions.JadxException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -15,10 +17,12 @@ import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
public class ManifestAttributes {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManifestAttributes.class);
|
||||
|
||||
private static final String ATTR_XML = "/android/attrs.xml";
|
||||
private static final String MANIFEST_ATTR_XML = "/android/attrs_manifest.xml";
|
||||
|
||||
private enum MAttrType {
|
||||
@@ -27,7 +31,7 @@ public class ManifestAttributes {
|
||||
|
||||
private static class MAttr {
|
||||
private final MAttrType type;
|
||||
private final Map<Integer, String> values = new LinkedHashMap<Integer, String>();
|
||||
private final Map<Long, String> values = new LinkedHashMap<Long, String>();
|
||||
|
||||
public MAttr(MAttrType type) {
|
||||
this.type = type;
|
||||
@@ -37,7 +41,7 @@ public class ManifestAttributes {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Map<Integer, String> getValues() {
|
||||
public Map<Long, String> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
@@ -47,15 +51,23 @@ public class ManifestAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
private final Document doc;
|
||||
private final Map<String, MAttr> attrMap = new HashMap<String, MAttr>();
|
||||
|
||||
public ManifestAttributes() throws Exception {
|
||||
InputStream xmlStream = null;
|
||||
}
|
||||
|
||||
public void parseAll() throws Exception {
|
||||
parse(loadXML(ATTR_XML));
|
||||
parse(loadXML(MANIFEST_ATTR_XML));
|
||||
LOG.debug("Loaded android attributes count: {}", attrMap.size());
|
||||
}
|
||||
|
||||
private Document loadXML(String xml) throws JadxException, ParserConfigurationException, SAXException, IOException {
|
||||
Document doc;InputStream xmlStream = null;
|
||||
try {
|
||||
xmlStream = ManifestAttributes.class.getResourceAsStream(MANIFEST_ATTR_XML);
|
||||
xmlStream = ManifestAttributes.class.getResourceAsStream(xml);
|
||||
if (xmlStream == null) {
|
||||
throw new JadxException(MANIFEST_ATTR_XML + " not found in classpath");
|
||||
throw new JadxException(xml + " not found in classpath");
|
||||
}
|
||||
DocumentBuilder dBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
doc = dBuilder.parse(xmlStream);
|
||||
@@ -64,9 +76,10 @@ public class ManifestAttributes {
|
||||
xmlStream.close();
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
public void parse() {
|
||||
private void parse(Document doc) {
|
||||
NodeList nodeList = doc.getChildNodes();
|
||||
for (int count = 0; count < nodeList.getLength(); count++) {
|
||||
Node node = nodeList.item(count);
|
||||
@@ -127,13 +140,13 @@ public class ManifestAttributes {
|
||||
Node valueNode = attributes.getNamedItem("value");
|
||||
if (valueNode != null) {
|
||||
try {
|
||||
int key;
|
||||
long key;
|
||||
String nodeValue = valueNode.getNodeValue();
|
||||
if (attr.getType() == MAttrType.ENUM) {
|
||||
key = Integer.parseInt(nodeValue);
|
||||
if (nodeValue.startsWith("0x")) {
|
||||
nodeValue = nodeValue.substring(2);
|
||||
key = Long.parseLong(nodeValue, 16);
|
||||
} else {
|
||||
nodeValue = nodeValue.replace("0x", "");
|
||||
key = Integer.parseInt(nodeValue, 16);
|
||||
key = Long.parseLong(nodeValue);
|
||||
}
|
||||
attr.getValues().put(key, nameNode.getNodeValue());
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -145,7 +158,7 @@ public class ManifestAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
public String decode(String attrName, int value) {
|
||||
public String decode(String attrName, long value) {
|
||||
MAttr attr = attrMap.get(attrName);
|
||||
if (attr == null) {
|
||||
return null;
|
||||
@@ -157,7 +170,7 @@ public class ManifestAttributes {
|
||||
}
|
||||
} else if (attr.getType() == MAttrType.FLAG) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<Integer, String> entry : attr.getValues().entrySet()) {
|
||||
for (Map.Entry<Long, String> entry : attr.getValues().entrySet()) {
|
||||
if ((value & entry.getKey()) != 0) {
|
||||
sb.append(entry.getValue()).append('|');
|
||||
}
|
||||
@@ -166,6 +179,6 @@ public class ManifestAttributes {
|
||||
return sb.deleteCharAt(sb.length() - 1).toString();
|
||||
}
|
||||
}
|
||||
return "UNKNOWN_DATA_0x" + Integer.toHexString(value);
|
||||
return "UNKNOWN_DATA_0x" + Long.toHexString(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package jadx.core.xmlgen;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ParserConstants {
|
||||
|
||||
/**
|
||||
@@ -141,6 +144,7 @@ public class ParserConstants {
|
||||
protected static final int ATTR_MAX = ResMakeInternal(2);
|
||||
// Localization of this resource is can be encouraged or required with an aapt flag if this is set
|
||||
protected static final int ATTR_L10N = ResMakeInternal(3);
|
||||
|
||||
// for plural support, see android.content.res.PluralRules#attrForQuantity(int)
|
||||
protected static final int ATTR_OTHER = ResMakeInternal(4);
|
||||
protected static final int ATTR_ZERO = ResMakeInternal(5);
|
||||
@@ -149,6 +153,17 @@ public class ParserConstants {
|
||||
protected static final int ATTR_FEW = ResMakeInternal(8);
|
||||
protected static final int ATTR_MANY = ResMakeInternal(9);
|
||||
|
||||
protected static final Map<Integer, String> PLURALS_MAP = new HashMap<Integer, String>() {
|
||||
{
|
||||
put(ATTR_OTHER, "other");
|
||||
put(ATTR_ZERO, "zero");
|
||||
put(ATTR_ONE, "one");
|
||||
put(ATTR_TWO, "two");
|
||||
put(ATTR_FEW, "few");
|
||||
put(ATTR_MANY, "many");
|
||||
}
|
||||
};
|
||||
|
||||
private static int ResMakeInternal(int entry) {
|
||||
return 0x01000000 | entry & 0xFFFF;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package jadx.core.xmlgen;
|
||||
|
||||
import jadx.core.codegen.CodeWriter;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class ResContainer implements Comparable<ResContainer> {
|
||||
|
||||
private final String name;
|
||||
@Nullable
|
||||
private CodeWriter content;
|
||||
|
||||
private final List<ResContainer> subFiles;
|
||||
|
||||
private ResContainer(String name, @Nullable CodeWriter content, List<ResContainer> subFiles) {
|
||||
this.name = name;
|
||||
this.content = content;
|
||||
this.subFiles = subFiles;
|
||||
}
|
||||
|
||||
public static ResContainer singleFile(String name, CodeWriter content) {
|
||||
return new ResContainer(name, content, Collections.<ResContainer>emptyList());
|
||||
}
|
||||
|
||||
public static ResContainer multiFile(String name) {
|
||||
return new ResContainer(name, null, new ArrayList<ResContainer>());
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return name.replace("/", File.separator);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CodeWriter getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(@Nullable CodeWriter content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public List<ResContainer> getSubFiles() {
|
||||
return subFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ResContainer o) {
|
||||
return name.compareTo(o.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ResContainer{" +
|
||||
"name='" + name + "'" +
|
||||
", content=" + content +
|
||||
", subFiles=" + subFiles +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -58,9 +58,19 @@ public class ResTableParser extends CommonBinaryParser {
|
||||
resStorage.finish();
|
||||
}
|
||||
|
||||
public CodeWriter decodeToCodeWriter(InputStream inputStream) throws IOException {
|
||||
public ResContainer decodeFiles(InputStream inputStream) throws IOException {
|
||||
decode(inputStream);
|
||||
|
||||
ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames());
|
||||
ResXmlGen resGen = new ResXmlGen(resStorage, vp);
|
||||
|
||||
ResContainer res = ResContainer.multiFile("res");
|
||||
res.setContent(makeDump());
|
||||
res.getSubFiles().addAll(resGen.makeResourcesXml());
|
||||
return res;
|
||||
}
|
||||
|
||||
public CodeWriter makeDump() throws IOException {
|
||||
CodeWriter writer = new CodeWriter();
|
||||
writer.add("app package: ").add(resStorage.getAppPackage());
|
||||
writer.startLine();
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package jadx.core.xmlgen;
|
||||
|
||||
import jadx.core.codegen.CodeWriter;
|
||||
import jadx.core.utils.StringUtils;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ResXmlGen {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResXmlGen.class);
|
||||
private static final Set<String> SKIP_RES_TYPES = new HashSet<String>(Arrays.asList(
|
||||
"layout",
|
||||
"mipmap",
|
||||
"id"
|
||||
));
|
||||
|
||||
private final ResourceStorage resStorage;
|
||||
private final ValuesParser vp;
|
||||
|
||||
public ResXmlGen(ResourceStorage resStorage, ValuesParser vp) {
|
||||
this.resStorage = resStorage;
|
||||
this.vp = vp;
|
||||
}
|
||||
|
||||
public List<ResContainer> makeResourcesXml() {
|
||||
Map<String, CodeWriter> contMap = new HashMap<String, CodeWriter>();
|
||||
for (ResourceEntry ri : resStorage.getResources()) {
|
||||
if (SKIP_RES_TYPES.contains(ri.getTypeName())) {
|
||||
continue;
|
||||
}
|
||||
String fn = getFileName(ri);
|
||||
CodeWriter cw = contMap.get(fn);
|
||||
if (cw == null) {
|
||||
cw = new CodeWriter();
|
||||
cw.add("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
|
||||
cw.startLine("<resources>");
|
||||
cw.incIndent();
|
||||
contMap.put(fn, cw);
|
||||
}
|
||||
addValue(cw, ri);
|
||||
}
|
||||
|
||||
List<ResContainer> files = new ArrayList<ResContainer>(contMap.size());
|
||||
for (Map.Entry<String, CodeWriter> entry : contMap.entrySet()) {
|
||||
String fileName = entry.getKey();
|
||||
CodeWriter content = entry.getValue();
|
||||
|
||||
content.decIndent();
|
||||
content.startLine("</resources>");
|
||||
content.finish();
|
||||
files.add(ResContainer.singleFile(fileName, content));
|
||||
}
|
||||
Collections.sort(files);
|
||||
return files;
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
cw.startLine();
|
||||
cw.add('<').add(ri.getTypeName()).add(' ');
|
||||
cw.add("name=\"").add(ri.getKeyName()).add("\">");
|
||||
cw.incIndent();
|
||||
for (RawNamedValue value : ri.getNamedValues()) {
|
||||
addItem(cw, value);
|
||||
}
|
||||
cw.decIndent();
|
||||
cw.startLine().add("</").add(ri.getTypeName()).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";
|
||||
}
|
||||
}
|
||||
String valueStr = vp.decodeValue(value.getRawValue());
|
||||
addSimpleValue(cw, "item", keyName, keyValue, valueStr);
|
||||
}
|
||||
|
||||
private void addSimpleValue(CodeWriter cw, String typeName, String attrName, String attrValue, String valueStr) {
|
||||
cw.startLine();
|
||||
cw.add('<').add(typeName);
|
||||
if (attrName != null && attrValue != null) {
|
||||
cw.add(' ').add(attrName).add("=\"").add(attrValue).add('"');
|
||||
}
|
||||
cw.add('>');
|
||||
cw.add(StringUtils.escapeResValue(valueStr));
|
||||
cw.add("</").add(typeName).add('>');
|
||||
}
|
||||
|
||||
private String getFileName(ResourceEntry ri) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String locale = ri.getConfig().getLocale();
|
||||
sb.append("res/values");
|
||||
if (!locale.isEmpty()) {
|
||||
sb.append('-').append(locale);
|
||||
}
|
||||
sb.append('/');
|
||||
sb.append(ri.getTypeName());
|
||||
if (!ri.getTypeName().endsWith("s")) {
|
||||
sb.append('s');
|
||||
}
|
||||
sb.append(".xml");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package jadx.core.xmlgen;
|
||||
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourceType;
|
||||
import jadx.core.codegen.CodeWriter;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
public class ResourcesSaver implements Runnable {
|
||||
private final ResourceFile resourceFile;
|
||||
private File outDir;
|
||||
|
||||
public ResourcesSaver(File outDir, ResourceFile resourceFile) {
|
||||
this.resourceFile = resourceFile;
|
||||
this.outDir = outDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!ResourceType.isSupportedForUnpack(resourceFile.getType())) {
|
||||
return;
|
||||
}
|
||||
ResContainer rc = resourceFile.getContent();
|
||||
if (rc != null) {
|
||||
saveResources(rc);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveResources(ResContainer rc) {
|
||||
if (rc == null) {
|
||||
return;
|
||||
}
|
||||
List<ResContainer> subFiles = rc.getSubFiles();
|
||||
if (subFiles.isEmpty()) {
|
||||
CodeWriter cw = rc.getContent();
|
||||
if (cw != null) {
|
||||
cw.save(new File(outDir, rc.getFileName()));
|
||||
}
|
||||
} else {
|
||||
for (ResContainer subFile : subFiles) {
|
||||
saveResources(subFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,7 @@ public class EntryConfig {
|
||||
return country;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public String getLocale() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (language != null) {
|
||||
sb.append(language);
|
||||
@@ -29,6 +28,13 @@ public class EntryConfig {
|
||||
if (country != null) {
|
||||
sb.append("-r").append(country);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(getLocale());
|
||||
if (sb.length() != 0) {
|
||||
sb.insert(0, " [");
|
||||
sb.append(']');
|
||||
|
||||
@@ -16,4 +16,9 @@ public class RawNamedValue {
|
||||
public RawValue getRawValue() {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RawNamedValue{nameRef=" + nameRef + ", rawValue=" + rawValue + '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourceFileContent;
|
||||
import jadx.api.ResourceType;
|
||||
import jadx.core.codegen.CodeWriter;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
import jadx.gui.utils.OverlayIcon;
|
||||
import jadx.gui.utils.Utils;
|
||||
|
||||
@@ -31,6 +33,7 @@ public class JResource extends JNode implements Comparable<JResource> {
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final String shortName;
|
||||
private final List<JResource> files = new ArrayList<JResource>(1);
|
||||
private final JResType type;
|
||||
private final ResourceFile resFile;
|
||||
@@ -40,12 +43,18 @@ public class JResource extends JNode implements Comparable<JResource> {
|
||||
private Map<Integer, Integer> lineMapping;
|
||||
|
||||
public JResource(ResourceFile resFile, String name, JResType type) {
|
||||
this(resFile, name, name, type);
|
||||
}
|
||||
|
||||
public JResource(ResourceFile resFile, String name, String shortName, JResType type) {
|
||||
this.resFile = resFile;
|
||||
this.name = name;
|
||||
this.shortName = shortName;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public final void update() {
|
||||
loadContent();
|
||||
removeAllChildren();
|
||||
for (JResource res : files) {
|
||||
res.update();
|
||||
@@ -53,6 +62,13 @@ public class JResource extends JNode implements Comparable<JResource> {
|
||||
}
|
||||
}
|
||||
|
||||
protected void loadContent() {
|
||||
getContent();
|
||||
for (JResource res : files) {
|
||||
res.loadContent();
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
@@ -65,16 +81,64 @@ public class JResource extends JNode implements Comparable<JResource> {
|
||||
if (!loaded && resFile != null && type == JResType.FILE) {
|
||||
loaded = true;
|
||||
if (isSupportedForView(resFile.getType())) {
|
||||
CodeWriter cw = resFile.getContent();
|
||||
if (cw != null) {
|
||||
lineMapping = cw.getLineMapping();
|
||||
content = cw.toString();
|
||||
ResContainer rc = resFile.getContent();
|
||||
if (rc != null) {
|
||||
addSubFiles(rc, this, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
protected void addSubFiles(ResContainer rc, JResource root, int depth) {
|
||||
CodeWriter cw = rc.getContent();
|
||||
if (cw != null) {
|
||||
if (depth == 0) {
|
||||
root.lineMapping = cw.getLineMapping();
|
||||
root.content = cw.toString();
|
||||
} else {
|
||||
String name = rc.getName();
|
||||
String[] path = name.split("/");
|
||||
String shortName = path.length == 0 ? name : path[path.length - 1];
|
||||
ResourceFileContent fileContent = new ResourceFileContent(shortName, ResourceType.XML, cw);
|
||||
addPath(path, root, new JResource(fileContent, name, shortName, JResType.FILE));
|
||||
}
|
||||
}
|
||||
List<ResContainer> subFiles = rc.getSubFiles();
|
||||
if (!subFiles.isEmpty()) {
|
||||
for (ResContainer subFile : subFiles) {
|
||||
addSubFiles(subFile, root, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPath(String[] path, JResource root, JResource jResource) {
|
||||
if (path.length == 1) {
|
||||
root.getFiles().add(jResource);
|
||||
return;
|
||||
}
|
||||
int last = path.length - 1;
|
||||
for (int i = 0; i <= last; i++) {
|
||||
String f = path[i];
|
||||
if (i == last) {
|
||||
root.getFiles().add(jResource);
|
||||
} else {
|
||||
root = getResDir(root, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JResource getResDir(JResource root, String dirName) {
|
||||
for (JResource file : root.getFiles()) {
|
||||
if (file.getName().equals(dirName)) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
JResource resDir = new JResource(null, dirName, JResType.DIR);
|
||||
root.getFiles().add(resDir);
|
||||
return resDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getSourceLine(int line) {
|
||||
if (lineMapping == null) {
|
||||
@@ -170,7 +234,7 @@ public class JResource extends JNode implements Comparable<JResource> {
|
||||
|
||||
@Override
|
||||
public String makeString() {
|
||||
return name;
|
||||
return shortName;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user